Initial commit
This commit is contained in:
129
skills/go-cli-builder/references/cobra-viper-integration.md
Normal file
129
skills/go-cli-builder/references/cobra-viper-integration.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# Cobra + Viper Integration Pattern
|
||||
|
||||
This document explains how Cobra (CLI framework) and Viper (configuration management) are integrated in the generated Go CLI projects.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The integration follows these principles:
|
||||
|
||||
1. **Configuration Priority** (highest to lowest):
|
||||
- Command-line flags
|
||||
- Environment variables
|
||||
- Config file values
|
||||
- Default values
|
||||
|
||||
2. **Lazy Loading**: Configuration is loaded once in `PersistentPreRun`, before any command executes
|
||||
|
||||
3. **Centralized Access**: The `GetConfig()` and `GetLogger()` functions in `cmd/root.go` provide access to configuration and logging
|
||||
|
||||
## Key Components
|
||||
|
||||
### Root Command (`cmd/root.go`)
|
||||
|
||||
The root command sets up the entire configuration system:
|
||||
|
||||
```go
|
||||
var rootCmd = &cobra.Command{
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
initConfig()
|
||||
setupLogging()
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Initialization (`initConfig()`)
|
||||
|
||||
This function:
|
||||
1. Determines config file location (from flag or default)
|
||||
2. Sets default values
|
||||
3. Enables environment variable reading
|
||||
4. Reads the config file (if it exists)
|
||||
|
||||
### Flag Binding
|
||||
|
||||
Flags are bound to Viper keys using `viper.BindPFlag()`:
|
||||
|
||||
```go
|
||||
rootCmd.PersistentFlags().StringP("verbose", "v", false, "verbose output")
|
||||
viper.BindPFlag("verbose", rootCmd.PersistentFlags().Lookup("verbose"))
|
||||
```
|
||||
|
||||
This creates the hierarchy: CLI flag → Viper key → Config struct
|
||||
|
||||
## Adding New Configuration
|
||||
|
||||
To add a new configuration option:
|
||||
|
||||
1. **Add to config struct** (`internal/config/config.go`):
|
||||
```go
|
||||
type Config struct {
|
||||
MyNewOption string
|
||||
}
|
||||
```
|
||||
|
||||
2. **Add flag** (`cmd/root.go` or command-specific file):
|
||||
```go
|
||||
rootCmd.PersistentFlags().String("my-option", "default", "description")
|
||||
viper.BindPFlag("my_option", rootCmd.PersistentFlags().Lookup("my-option"))
|
||||
```
|
||||
|
||||
3. **Set default** (`cmd/root.go` in `initConfig()`):
|
||||
```go
|
||||
viper.SetDefault("my_option", "default_value")
|
||||
```
|
||||
|
||||
4. **Add to config example** (`.yaml.example`):
|
||||
```yaml
|
||||
my_option: "default_value"
|
||||
```
|
||||
|
||||
5. **Access in commands**:
|
||||
```go
|
||||
cfg := GetConfig()
|
||||
value := cfg.MyNewOption
|
||||
// or directly from viper:
|
||||
value := viper.GetString("my_option")
|
||||
```
|
||||
|
||||
## Command-Specific Configuration
|
||||
|
||||
For configuration specific to a single command:
|
||||
|
||||
1. Add the flag to the command's `init()` function, not the root command
|
||||
2. Use a nested structure in the config struct:
|
||||
```go
|
||||
type Config struct {
|
||||
Fetch struct {
|
||||
Concurrency int
|
||||
Timeout time.Duration
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Bind with a namespaced key:
|
||||
```go
|
||||
viper.BindPFlag("fetch.concurrency", fetchCmd.Flags().Lookup("concurrency"))
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Viper automatically maps environment variables when you call `viper.AutomaticEnv()`.
|
||||
|
||||
By default, environment variables are matched by converting the key to uppercase and replacing `.` with `_`:
|
||||
|
||||
- Config key: `fetch.concurrency`
|
||||
- Environment variable: `FETCH_CONCURRENCY`
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use PersistentFlags for global options**: Options that apply to all commands should be on `rootCmd.PersistentFlags()`
|
||||
|
||||
2. **Use command-specific Flags for local options**: Options specific to one command should be on that command's `Flags()`
|
||||
|
||||
3. **Provide sensible defaults**: Always set defaults in `initConfig()` so the tool works without a config file
|
||||
|
||||
4. **Document in .yaml.example**: Keep the example config file up to date
|
||||
|
||||
5. **Keep flag names kebab-case**: Use hyphens in CLI flags (`--my-option`) and underscores in Viper keys (`my_option`)
|
||||
|
||||
6. **Use GetConfig() for structured access**: Prefer accessing configuration through the typed Config struct rather than calling viper.Get* directly in commands
|
||||
231
skills/go-cli-builder/references/internal-organization.md
Normal file
231
skills/go-cli-builder/references/internal-organization.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# Internal Package Organization
|
||||
|
||||
This document explains how to organize code in the `internal/` directory of Go CLI projects.
|
||||
|
||||
## The `internal/` Directory
|
||||
|
||||
The `internal/` directory is a special Go convention. Packages inside `internal/` can only be imported by code in the parent tree. This enforces encapsulation and prevents external projects from depending on internal implementation details.
|
||||
|
||||
## Standard Package Structure
|
||||
|
||||
A typical Go CLI project has this structure:
|
||||
|
||||
```
|
||||
project/
|
||||
├── cmd/ # Command definitions (public API of the CLI)
|
||||
├── internal/ # Private implementation
|
||||
│ ├── config/ # Configuration structures
|
||||
│ ├── database/ # Database access layer
|
||||
│ └── [domain packages] # Business logic packages
|
||||
├── main.go # Entry point
|
||||
└── go.mod # Dependencies
|
||||
```
|
||||
|
||||
## Package Guidelines
|
||||
|
||||
### `cmd/` Package
|
||||
|
||||
**Purpose**: Define the CLI commands and their flags
|
||||
|
||||
**Contents**:
|
||||
- `root.go`: Root command and configuration initialization
|
||||
- `version.go`: Version command
|
||||
- `constants.go`: CLI-level constants
|
||||
- One file per command (e.g., `fetch.go`, `export.go`)
|
||||
|
||||
**Responsibilities**:
|
||||
- Parse and validate user input
|
||||
- Set up configuration and logging
|
||||
- Call into `internal/` packages to do the work
|
||||
- Format and display output
|
||||
|
||||
**Anti-patterns**:
|
||||
- Heavy business logic in command handlers
|
||||
- Direct database access
|
||||
- Complex algorithms
|
||||
|
||||
### `internal/config/` Package
|
||||
|
||||
**Purpose**: Define configuration structures
|
||||
|
||||
**Contents**:
|
||||
- `config.go`: Config struct definitions
|
||||
|
||||
**Example**:
|
||||
```go
|
||||
package config
|
||||
|
||||
type Config struct {
|
||||
Database string
|
||||
Verbose bool
|
||||
|
||||
Fetch struct {
|
||||
Concurrency int
|
||||
Timeout time.Duration
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `internal/database/` Package
|
||||
|
||||
**Purpose**: Encapsulate all database operations
|
||||
|
||||
**Contents**:
|
||||
- `database.go`: Connection management, initialization
|
||||
- `migrations.go`: Migration system
|
||||
- `schema.sql`: Initial schema (embedded)
|
||||
- Optional: `queries.go` for complex queries
|
||||
|
||||
**Responsibilities**:
|
||||
- Database connection lifecycle
|
||||
- Schema initialization and migrations
|
||||
- Data access methods
|
||||
- Transaction management
|
||||
|
||||
**Anti-patterns**:
|
||||
- Business logic in database layer
|
||||
- Exposing `*sql.DB` directly
|
||||
- SQL in command files
|
||||
|
||||
### Domain-Specific Packages
|
||||
|
||||
Create additional packages in `internal/` for each major domain or feature:
|
||||
|
||||
```
|
||||
internal/
|
||||
├── feeds/ # Feed parsing and processing
|
||||
├── fetcher/ # HTTP fetching logic
|
||||
├── renderer/ # Output rendering
|
||||
└── exporter/ # Export functionality
|
||||
```
|
||||
|
||||
**Guidelines**:
|
||||
- One package per cohesive responsibility
|
||||
- Packages should be importable by `cmd/` and by each other
|
||||
- Keep packages focused and single-purpose
|
||||
- Use clear, descriptive names
|
||||
|
||||
## Layering and Dependencies
|
||||
|
||||
Follow these dependency rules:
|
||||
|
||||
```
|
||||
main.go
|
||||
└─> cmd/
|
||||
└─> internal/config/
|
||||
└─> internal/database/
|
||||
└─> internal/[domain]/
|
||||
└─> internal/[other domains]/
|
||||
```
|
||||
|
||||
**Rules**:
|
||||
1. `cmd/` can import any `internal/` package
|
||||
2. `internal/` packages can import each other as needed
|
||||
3. Avoid circular dependencies between `internal/` packages
|
||||
4. Keep `cmd/` thin - it orchestrates but doesn't implement
|
||||
|
||||
## Example: Adding a New Feature
|
||||
|
||||
Let's say you want to add feed fetching functionality:
|
||||
|
||||
1. **Create the package**:
|
||||
```
|
||||
internal/fetcher/
|
||||
├── fetcher.go # Main fetching logic
|
||||
└── fetcher_test.go # Tests
|
||||
```
|
||||
|
||||
2. **Define the API**:
|
||||
```go
|
||||
package fetcher
|
||||
|
||||
type Fetcher struct {
|
||||
client *http.Client
|
||||
// ...
|
||||
}
|
||||
|
||||
func New(opts ...Option) *Fetcher { ... }
|
||||
func (f *Fetcher) Fetch(url string) ([]byte, error) { ... }
|
||||
```
|
||||
|
||||
3. **Use in command**:
|
||||
```go
|
||||
// cmd/fetch.go
|
||||
package cmd
|
||||
|
||||
import "yourproject/internal/fetcher"
|
||||
|
||||
var fetchCmd = &cobra.Command{
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
f := fetcher.New()
|
||||
data, err := f.Fetch(url)
|
||||
// ...
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Option Pattern for Configuration
|
||||
|
||||
```go
|
||||
type Fetcher struct {
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
type Option func(*Fetcher)
|
||||
|
||||
func WithTimeout(d time.Duration) Option {
|
||||
return func(f *Fetcher) {
|
||||
f.timeout = d
|
||||
}
|
||||
}
|
||||
|
||||
func New(opts ...Option) *Fetcher {
|
||||
f := &Fetcher{timeout: 30 * time.Second}
|
||||
for _, opt := range opts {
|
||||
opt(f)
|
||||
}
|
||||
return f
|
||||
}
|
||||
```
|
||||
|
||||
### Embedding Resources
|
||||
|
||||
For SQL, templates, or other resources:
|
||||
|
||||
```go
|
||||
import _ "embed"
|
||||
|
||||
//go:embed schema.sql
|
||||
var schemaSQL string
|
||||
```
|
||||
|
||||
### Error Wrapping
|
||||
|
||||
Always wrap errors with context:
|
||||
|
||||
```go
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch feed %s: %w", url, err)
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
- Put tests in `_test.go` files alongside the code
|
||||
- Use table-driven tests for multiple cases
|
||||
- Consider using `internal/database/database_test.go` with in-memory SQLite for database tests
|
||||
|
||||
## When to Create a New Package
|
||||
|
||||
Create a new `internal/` package when:
|
||||
- You have a cohesive set of related functionality
|
||||
- The code would make commands cleaner and more focused
|
||||
- You want to unit test logic separately from CLI interaction
|
||||
- Multiple commands need to share the same functionality
|
||||
|
||||
Don't create a package when:
|
||||
- It would only have one small function
|
||||
- It's tightly coupled to a single command
|
||||
- It would create circular dependencies
|
||||
410
skills/go-cli-builder/references/template-patterns.md
Normal file
410
skills/go-cli-builder/references/template-patterns.md
Normal file
@@ -0,0 +1,410 @@
|
||||
# Template Patterns for Go CLI Tools
|
||||
|
||||
This guide covers patterns for implementing customizable output templates in Go CLI tools, based on successful patterns from `linkding-to-markdown` and `mastodon-to-markdown`.
|
||||
|
||||
## Overview
|
||||
|
||||
CLI tools that generate formatted output (Markdown, HTML, XML, etc.) benefit from:
|
||||
1. **Embedded default templates** - Work out of the box, single binary
|
||||
2. **User customization** - Users can modify templates for their needs
|
||||
3. **Init command** - Easy way to get started with configuration and templates
|
||||
|
||||
## Architecture
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
my-cli-tool/
|
||||
├── cmd/
|
||||
│ ├── init.go # Init command to bootstrap config/templates
|
||||
│ └── fetch.go # Command that uses templates
|
||||
├── internal/
|
||||
│ ├── templates/
|
||||
│ │ ├── templates.go # Template loader with embedded defaults
|
||||
│ │ └── default.md # Default template (embedded via //go:embed)
|
||||
│ └── generator/ # Or markdown/, formatter/, etc.
|
||||
│ └── generator.go # Template renderer and data structures
|
||||
```
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### 1. Create Template Package
|
||||
|
||||
**File: `internal/templates/templates.go`**
|
||||
|
||||
```go
|
||||
package templates
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed default.md
|
||||
var defaultTemplate string
|
||||
|
||||
// GetDefaultTemplate returns the embedded default template content
|
||||
func GetDefaultTemplate() (string, error) {
|
||||
return defaultTemplate, nil
|
||||
}
|
||||
```
|
||||
|
||||
**File: `internal/templates/default.md`**
|
||||
|
||||
Create your default template using Go's `text/template` syntax:
|
||||
|
||||
```markdown
|
||||
# {{ .Title }}
|
||||
|
||||
_Generated: {{ .Generated }}_
|
||||
|
||||
---
|
||||
|
||||
{{ range .Items -}}
|
||||
## {{ .Name }}
|
||||
|
||||
{{ .Description }}
|
||||
|
||||
{{ if .Tags -}}
|
||||
Tags: {{ join .Tags ", " }}
|
||||
{{ end -}}
|
||||
|
||||
---
|
||||
{{ end -}}
|
||||
```
|
||||
|
||||
### 2. Create Generator/Renderer
|
||||
|
||||
**File: `internal/generator/generator.go`**
|
||||
|
||||
```go
|
||||
package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"yourproject/internal/templates"
|
||||
)
|
||||
|
||||
type Generator struct {
|
||||
template *template.Template
|
||||
}
|
||||
|
||||
// NewGenerator creates a generator with the default embedded template
|
||||
func NewGenerator() (*Generator, error) {
|
||||
defaultTmpl, err := templates.GetDefaultTemplate()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get default template: %w", err)
|
||||
}
|
||||
return NewGeneratorWithTemplate(defaultTmpl)
|
||||
}
|
||||
|
||||
// NewGeneratorFromFile creates a generator from a template file
|
||||
func NewGeneratorFromFile(templatePath string) (*Generator, error) {
|
||||
content, err := os.ReadFile(templatePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read template file: %w", err)
|
||||
}
|
||||
return NewGeneratorWithTemplate(string(content))
|
||||
}
|
||||
|
||||
// NewGeneratorWithTemplate creates a generator with a custom template string
|
||||
func NewGeneratorWithTemplate(tmplStr string) (*Generator, error) {
|
||||
// Define template functions
|
||||
funcMap := template.FuncMap{
|
||||
"formatDate": func(t time.Time, format string) string {
|
||||
return t.Format(format)
|
||||
},
|
||||
"join": strings.Join,
|
||||
"hasContent": func(s string) bool {
|
||||
return strings.TrimSpace(s) != ""
|
||||
},
|
||||
}
|
||||
|
||||
tmpl, err := template.New("output").Funcs(funcMap).Parse(tmplStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse template: %w", err)
|
||||
}
|
||||
|
||||
return &Generator{template: tmpl}, nil
|
||||
}
|
||||
|
||||
// TemplateData holds data passed to templates
|
||||
type TemplateData struct {
|
||||
Title string
|
||||
Generated string
|
||||
Items []Item
|
||||
// Add your domain-specific fields here
|
||||
}
|
||||
|
||||
// Generate executes the template with data and writes to writer
|
||||
func (g *Generator) Generate(w io.Writer, data TemplateData) error {
|
||||
if err := g.template.Execute(w, data); err != nil {
|
||||
return fmt.Errorf("failed to execute template: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Create Init Command
|
||||
|
||||
Use the `init.go.template` from the skill, customizing the `defaultConfigContent` for your project's needs.
|
||||
|
||||
Key features:
|
||||
- Creates config file with documented options
|
||||
- Creates customizable template file from embedded default
|
||||
- Supports `--force` to overwrite
|
||||
- Supports `--template-file` to specify custom filename
|
||||
- Provides helpful next steps
|
||||
|
||||
### 4. Integrate with Commands
|
||||
|
||||
**In your command that generates output:**
|
||||
|
||||
```go
|
||||
func runFetch(cmd *cobra.Command, args []string) error {
|
||||
logger := GetLogger()
|
||||
|
||||
// ... fetch your data ...
|
||||
|
||||
// Create generator with custom template or default
|
||||
templatePath := viper.GetString("fetch.template")
|
||||
var generator *generator.Generator
|
||||
var err error
|
||||
|
||||
if templatePath != "" {
|
||||
logger.Infof("Using custom template: %s", templatePath)
|
||||
generator, err = generator.NewGeneratorFromFile(templatePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load custom template: %w", err)
|
||||
}
|
||||
} else {
|
||||
generator, err = generator.NewGenerator()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create generator: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare template data
|
||||
data := generator.TemplateData{
|
||||
Title: viper.GetString("fetch.title"),
|
||||
Generated: time.Now().Format(time.RFC3339),
|
||||
Items: fetchedItems,
|
||||
}
|
||||
|
||||
// Determine output destination
|
||||
outputPath := viper.GetString("fetch.output")
|
||||
var output *os.File
|
||||
if outputPath != "" {
|
||||
output, err = os.Create(outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create output file: %w", err)
|
||||
}
|
||||
defer output.Close()
|
||||
logger.Infof("Writing output to %s", outputPath)
|
||||
} else {
|
||||
output = os.Stdout
|
||||
}
|
||||
|
||||
// Generate output
|
||||
if err := generator.Generate(output, data); err != nil {
|
||||
return fmt.Errorf("failed to generate output: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Add Configuration Support
|
||||
|
||||
**In `internal/config/config.go`:**
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
// ... other config ...
|
||||
|
||||
Fetch struct {
|
||||
Output string
|
||||
Title string
|
||||
Template string // Path to custom template file
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**In your command's flags:**
|
||||
|
||||
```go
|
||||
fetchCmd.Flags().String("template", "", "Custom template file (default: built-in template)")
|
||||
_ = viper.BindPFlag("fetch.template", fetchCmd.Flags().Lookup("template"))
|
||||
```
|
||||
|
||||
**In config YAML:**
|
||||
|
||||
```yaml
|
||||
fetch:
|
||||
output: "output.md"
|
||||
title: "My Output"
|
||||
template: "my-custom-template.md" # Optional
|
||||
```
|
||||
|
||||
## Template Functions
|
||||
|
||||
Provide helpful template functions for common operations:
|
||||
|
||||
```go
|
||||
funcMap := template.FuncMap{
|
||||
// Date formatting
|
||||
"formatDate": func(t time.Time, format string) string {
|
||||
return t.Format(format)
|
||||
},
|
||||
|
||||
// String operations
|
||||
"join": strings.Join,
|
||||
"hasContent": func(s string) bool {
|
||||
return strings.TrimSpace(s) != ""
|
||||
},
|
||||
"truncate": func(s string, length int) string {
|
||||
if len(s) <= length {
|
||||
return s
|
||||
}
|
||||
return s[:length] + "..."
|
||||
},
|
||||
|
||||
// Conditional helpers
|
||||
"default": func(defaultVal, val interface{}) interface{} {
|
||||
if val == nil || val == "" {
|
||||
return defaultVal
|
||||
}
|
||||
return val
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## User Workflow
|
||||
|
||||
### First-Time Setup
|
||||
|
||||
```bash
|
||||
# User initializes config and template
|
||||
$ my-tool init
|
||||
✅ Initialization complete!
|
||||
|
||||
Next steps:
|
||||
1. Edit my-tool.yaml and add your configuration
|
||||
2. (Optional) Customize my-tool.md for your preferred output format
|
||||
3. Run: my-tool fetch --help for usage information
|
||||
```
|
||||
|
||||
### Using Default Template
|
||||
|
||||
```bash
|
||||
# Just works with embedded default
|
||||
$ my-tool fetch --output result.md
|
||||
```
|
||||
|
||||
### Using Custom Template
|
||||
|
||||
```bash
|
||||
# After editing my-tool.md
|
||||
$ my-tool fetch --template my-tool.md --output result.md
|
||||
|
||||
# Or via config file
|
||||
$ cat my-tool.yaml
|
||||
fetch:
|
||||
template: "my-tool.md"
|
||||
|
||||
$ my-tool fetch --output result.md
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always provide a sensible default template** - Tool should work without customization
|
||||
2. **Document template variables** - In README and/or generated template comments
|
||||
3. **Validate templates early** - Parse template when creating generator, not during execution
|
||||
4. **Provide helpful error messages** - Template parse errors should show line numbers
|
||||
5. **Include examples** - Show template snippets in documentation
|
||||
6. **Support both stdout and file output** - Enables piping and integration
|
||||
7. **Make template optional** - Config file should work without template field set
|
||||
|
||||
## Template Documentation
|
||||
|
||||
In your README, document:
|
||||
|
||||
### Available Variables
|
||||
|
||||
```markdown
|
||||
### Template Variables
|
||||
|
||||
- `.Title` - Document title (string)
|
||||
- `.Generated` - Generation timestamp (string)
|
||||
- `.Items` - Array of items to include
|
||||
|
||||
### Item Fields
|
||||
|
||||
Each item has:
|
||||
- `.Name` - Item name (string)
|
||||
- `.Description` - Item description (string)
|
||||
- `.Tags` - Array of tags ([]string)
|
||||
```
|
||||
|
||||
### Available Functions
|
||||
|
||||
```markdown
|
||||
### Template Functions
|
||||
|
||||
- `formatDate <time> <format>` - Format time.Time with Go time format
|
||||
- `join <slice> <separator>` - Join string slice
|
||||
- `hasContent <string>` - Check if string is non-empty
|
||||
```
|
||||
|
||||
### Example Template
|
||||
|
||||
Include a complete working example users can copy/paste.
|
||||
|
||||
## Testing Templates
|
||||
|
||||
```go
|
||||
func TestTemplateExecution(t *testing.T) {
|
||||
tmpl := `{{ .Title }}
|
||||
{{ range .Items }}{{ .Name }}{{ end }}`
|
||||
|
||||
gen, err := NewGeneratorWithTemplate(tmpl)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create generator: %v", err)
|
||||
}
|
||||
|
||||
data := TemplateData{
|
||||
Title: "Test",
|
||||
Items: []Item{{Name: "Item1"}, {Name: "Item2"}},
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := gen.Generate(&buf, data); err != nil {
|
||||
t.Fatalf("failed to generate: %v", err)
|
||||
}
|
||||
|
||||
expected := "Test\nItem1Item2"
|
||||
if buf.String() != expected {
|
||||
t.Errorf("expected %q, got %q", expected, buf.String())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Not using `//go:embed`** - Requires users to distribute template files separately
|
||||
2. **No template validation** - Errors appear late during execution
|
||||
3. **Poor error messages** - Template errors can be cryptic, add context
|
||||
4. **Forgetting `defer file.Close()`** - When writing to files
|
||||
5. **Not supporting stdout** - Reduces composability with other tools
|
||||
6. **Hardcoded paths** - Use relative paths or make configurable
|
||||
|
||||
## Examples in the Wild
|
||||
|
||||
- **linkding-to-markdown** - Bookmarks to Markdown with grouping options
|
||||
- **mastodon-to-markdown** - Posts to Markdown with media handling
|
||||
- **feedspool-go** - RSS/Atom feed processing with custom templates
|
||||
Reference in New Issue
Block a user