gotmpl

package
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 6, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

README

gotmpl

The gotmpl package provides a service-oriented wrapper around Go's standard text/template package with enhanced features including custom delimiters, string replacements, template function management, and template data reference extraction.

Features

  • Service Pattern: Reusable service instances with default configurations
  • Template Caching: Thread-safe LRU cache for compiled templates with SHA-256 content hashing
  • Custom Delimiters: Support for any delimiter pair (e.g., [[, ]] or {%, %})
  • String Replacements: Protect literal strings from template parsing with UUID placeholders
  • Custom Functions: Add custom template functions with flexible override capabilities
  • Missing Key Handling: Configurable behavior for missing map keys (default, zero, error)
  • Context Integration: Full context support for logging and cancellation
  • Structured Logging: Detailed logging at multiple verbosity levels
  • Reference Extraction: Extract data field references from templates for dependency analysis
  • Sprig Functions: 100+ built-in utility functions via Masterminds/sprig
  • Extension System: Pluggable architecture for custom Go template functions (see ext/ sub-packages)
  • Cache Metrics: Per-template hit tracking with configurable limits

Installation

import "github.com/oakwood-commons/scafctl/pkg/gotmpl"

Quick Start

Basic Template Execution
ctx := context.Background()

result, err := gotmpl.Execute(ctx, gotmpl.TemplateOptions{
    Name:    "greeting",
    Content: "Hello, {{.Name}}!",
    Data:    map[string]string{"Name": "World"},
})
if err != nil {
    log.Fatal(err)
}

fmt.Println(result.Output) // Output: Hello, World!
Using the Service Pattern

For repeated template execution with shared default functions:

// Create a service with default functions
svc := gotmpl.NewService(template.FuncMap{
    "upper": strings.ToUpper,
    "lower": strings.ToLower,
})

// Execute multiple templates
result, err := svc.Execute(ctx, gotmpl.TemplateOptions{
    Name:    "formatted",
    Content: "{{upper .Text}}",
    Data:    map[string]string{"Text": "hello"},
})

API Reference

Types
TemplateOptions

Configuration for template execution:

type TemplateOptions struct {
    // Content is the template content as a string (required)
    Content string

    // Name is the reference name/identifier for the template
    // Used in logging and error messages (optional, defaults to "unnamed-template")
    Name string

    // Data is the data source passed to the template during execution
    Data any

    // LeftDelim sets the left action delimiter (default: "{{")
    LeftDelim string

    // RightDelim sets the right action delimiter (default: "}}")
    RightDelim string

    // Replacements is a slice of strings to replace before template execution
    // The key is replaced with a UUID placeholder, then restored after execution
    Replacements []Replacement

    // Funcs is a map of custom template functions
    Funcs template.FuncMap

    // MissingKey controls the behavior when a map key is missing
    // Options: MissingKeyDefault, MissingKeyZero, MissingKeyError
    MissingKey MissingKeyOption

    // DisableBuiltinFuncs disables the built-in template functions
    DisableBuiltinFuncs bool
}
MissingKeyOption

Defines behavior for missing map keys:

type MissingKeyOption string

const (
    // MissingKeyDefault prints "<no value>" for missing keys (default)
    MissingKeyDefault MissingKeyOption = "default"

    // MissingKeyZero returns the zero value for the type
    MissingKeyZero MissingKeyOption = "zero"

    // MissingKeyError stops execution with an error
    MissingKeyError MissingKeyOption = "error"
)
ExecuteResult

Result of template execution:

type ExecuteResult struct {
    // Output is the rendered template content
    Output string

    // TemplateName is the name/identifier of the template
    TemplateName string

    // ReplacementsMade is the number of replacements that were applied
    ReplacementsMade int
}
TemplateReference

Represents a data field reference found in a template:

type TemplateReference struct {
    // Path is the dot-notation path to the field (e.g., ".User.Name")
    Path string

    // Position is the location in the template (e.g., "line:col")
    Position string
}
Functions
Execute

Convenience function for one-off template execution:

func Execute(ctx context.Context, opts TemplateOptions) (*ExecuteResult, error)
NewService

Creates a new template service with optional default functions:

func NewService(defaultFuncs template.FuncMap) *Service
Service.Execute

Renders a template with the provided options:

func (s *Service) Execute(ctx context.Context, opts TemplateOptions) (*ExecuteResult, error)
Service.GetReferences

Extracts data field references from a template:

func (s *Service) GetReferences(ctx context.Context, opts TemplateOptions) ([]TemplateReference, error)
GetGoTemplateReferences

Convenience function for extracting references without creating a service:

func GetGoTemplateReferences(content, leftDelim, rightDelim string) ([]string, error)

Usage Examples

Custom Delimiters

Use Jinja2-style delimiters:

svc := gotmpl.NewService(nil)

result, err := svc.Execute(ctx, gotmpl.TemplateOptions{
    Name:      "jinja-style",
    Content:   "{% for item in .Items %}{{ item }}{% endfor %}",
    LeftDelim: "{%",
    RightDelim: "%}",
    Data: map[string][]string{
        "Items": {"apple", "banana", "cherry"},
    },
})
Custom Functions

Add and override template functions:

svc := gotmpl.NewService(template.FuncMap{
    "default": func(def, val string) string {
        if val == "" {
            return def
        }
        return val
    },
})

result, err := svc.Execute(ctx, gotmpl.TemplateOptions{
    Name:    "with-functions",
    Content: "{{default \"N/A\" .Value}}",
    Data:    map[string]string{"Value": ""},
    Funcs: template.FuncMap{
        "upper": strings.ToUpper, // Per-execution function
    },
})
String Replacements

Protect literal template syntax from parsing:

result, err := gotmpl.Execute(ctx, gotmpl.TemplateOptions{
    Name:    "with-replacements",
    Content: "The syntax {{.Name}} uses REPLACE_ME delimiters",
    Data:    map[string]string{"Name": "Go"},
    Replacements: []gotmpl.Replacement{
        {Find: "REPLACE_ME", Replace: "{{...}}"},
    },
})
// Output: The syntax Go uses {{...}} delimiters
Missing Key Handling

Configure behavior for undefined map keys:

// Error on missing keys
result, err := gotmpl.Execute(ctx, gotmpl.TemplateOptions{
    Name:       "strict",
    Content:    "{{.Missing}}",
    Data:       map[string]string{},
    MissingKey: gotmpl.MissingKeyError,
})
// Returns error: "template: strict:1:2: executing \"strict\" at <.Missing>: map has no entry for key \"Missing\""

// Use zero value
result, err = gotmpl.Execute(ctx, gotmpl.TemplateOptions{
    Name:       "zero-value",
    Content:    "{{.Missing}}",
    Data:       map[string]string{},
    MissingKey: gotmpl.MissingKeyZero,
})
// Output: "" (empty string, the zero value for string type)

// Default behavior (print "<no value>")
result, err = gotmpl.Execute(ctx, gotmpl.TemplateOptions{
    Name:       "default",
    Content:    "{{.Missing}}",
    Data:       map[string]string{},
    MissingKey: gotmpl.MissingKeyDefault,
})
// Output: "<no value>"
Extracting Template References

Analyze template dependencies by extracting data field references:

svc := gotmpl.NewService(nil)

refs, err := svc.GetReferences(ctx, gotmpl.TemplateOptions{
    Content: `
        {{.User.Name}}
        {{range .Items}}
            {{.Price}}
        {{end}}
    `,
})

for _, ref := range refs {
    fmt.Printf("%s at %s\n", ref.Path, ref.Position)
}
// Output:
// .User.Name at 1:8
// .Items at 2:15
// .Price at 3:14
Complex Example

Combine multiple features:

svc := gotmpl.NewService(template.FuncMap{
    "upper": strings.ToUpper,
})

result, err := svc.Execute(ctx, gotmpl.TemplateOptions{
    Name:      "complex",
    Content:   "[[ upper .Title ]]\n[[range .Items]]\n- [[.Name]]: $[[.Price]]\n[[end]]\nLiteral: TEMPLATE_SYNTAX",
    LeftDelim: "[[",
    RightDelim: "]]",
    Data: map[string]any{
        "Title": "products",
        "Items": []map[string]any{
            {"Name": "Apple", "Price": "1.50"},
            {"Name": "Banana", "Price": "0.75"},
        },
    },
    Replacements: []gotmpl.Replacement{
        {Find: "TEMPLATE_SYNTAX", Replace: "[[...]]"},
    },
    MissingKey: gotmpl.MissingKeyError,
})

// Output:
// PRODUCTS
// - Apple: $1.50
// - Banana: $0.75
// Literal: [[...]]

Logging

The package integrates with the github.com/oakwood-commons/scafctl/pkg/logger package for structured logging:

  • V(1): High-level operations (template execution start/complete)
  • V(2): Detailed steps (parsing, replacements, function registration)

Enable verbose logging to debug template issues:

import "github.com/oakwood-commons/scafctl/pkg/logger"

// Create a logger with verbosity level 2
lgr := logger.Get(2)
ctx := logger.WithLogger(context.Background(), lgr)

result, err := gotmpl.Execute(ctx, gotmpl.TemplateOptions{
    Name:    "debug",
    Content: "{{.Value}}",
    Data:    map[string]string{"Value": "test"},
})

Error Handling

The package provides detailed error messages with context:

result, err := gotmpl.Execute(ctx, gotmpl.TemplateOptions{
    Name:    "invalid",
    Content: "{{.Value",  // Missing closing delimiter
    Data:    map[string]string{},
})
if err != nil {
    // Error: failed to create template 'invalid': parse error: ...
    fmt.Println(err)
}

Common error scenarios:

  • Empty content: Returns "template content cannot be empty"
  • Parse errors: Invalid template syntax
  • Execution errors: Type mismatches, undefined functions, missing keys (with MissingKeyError)

Template Reference Extraction

The package can analyze templates to extract data field references, useful for:

  • Validating that required data is provided
  • Building dependency graphs
  • Documentation generation
  • Static analysis

The extraction:

  • ✅ Includes field accesses (.User.Name, .Items)
  • ✅ Works with custom delimiters
  • ✅ Handles nested structures and control flow
  • ❌ Excludes function calls ({{upper .Text}} only extracts .Text)
  • ❌ Excludes template variables ({{$var := .Value}} extracts .Value but not $var)
// Using the service method
svc := gotmpl.NewService(nil)
refs, err := svc.GetReferences(ctx, gotmpl.TemplateOptions{
    Content:   "{{.User.Name}} {{range .Orders}}{{.ID}}{{end}}",
    LeftDelim: "[[",
    RightDelim: "]]",
})

// Using the convenience function
refs, err := gotmpl.GetGoTemplateReferences(
    "{{.User.Name}}",
    "{{",
    "}}",
)

Constants

const (
    // DefaultLeftDelim is the default left delimiter for templates
    DefaultLeftDelim = "{{"

    // DefaultRightDelim is the default right delimiter for templates
    DefaultRightDelim = "}}"
)

Testing

The package includes comprehensive test coverage:

# Run all tests
go test ./pkg/gotmpl

# Run with verbose output
go test -v ./pkg/gotmpl

# Run examples
go test -v ./pkg/gotmpl -run Example

# Run with coverage
go test -cover ./pkg/gotmpl

See gotmpl_test.go, refs_test.go, and example_test.go for detailed usage examples.

Design Rationale

Service Pattern

The service pattern allows for:

  • Reusability: Create once, execute many times
  • Default configurations: Share common functions across executions
  • Testability: Easy to mock and test
  • Consistency: Same pattern as other packages in the project
Context Support

Context integration enables:

  • Cancellation: Stop long-running template execution
  • Logging: Structured logging with verbosity levels
  • Tracing: Future support for distributed tracing
  • Deadline propagation: Respect timeouts from upstream callers
Replacement System

The replacement system solves the problem of embedding literal template syntax:

  1. Before parsing, specified strings are replaced with UUID placeholders
  2. Template is parsed and executed (placeholders pass through unmodified)
  3. After execution, placeholders are restored to original strings

This is useful when:

  • Documenting template syntax (showing {{...}} examples)
  • Embedding other template formats in output
  • Protecting special characters from template parsing
Typed MissingKey Options

Using a custom type instead of strings provides:

  • Compile-time safety: Invalid options caught by the compiler
  • IDE autocomplete: Better developer experience
  • Self-documentation: Clear available options
  • Future extensibility: Easy to add new options

Performance Considerations

  • Template caching: Compiled templates are cached in an LRU cache keyed by SHA-256 hash of content + configuration. The default cache holds up to 10,000 entries.
  • Replacements: Linear scan of content (O(n) per replacement)
  • Reference extraction: Parse tree traversal (O(nodes) in template)
  • Logging overhead: Minimal when verbosity is disabled

For high-performance scenarios:

  • Reuse Service instances to avoid function map duplication
  • Minimize replacements (only protect necessary strings)
  • Disable verbose logging in production
  • Tune the cache size via goTemplate.cacheSize in your app config (default: 10,000)
Template Cache

The package includes a thread-safe LRU template cache (TemplateCache) that avoids re-parsing identical templates across executions. Cache keys are SHA-256 hashes of: template content, delimiters, missingKey option, and function map keys.

// Access the default cache (singleton, lazily initialized)
cache := gotmpl.GetDefaultCache()

// Check cache stats
stats := cache.Stats()
fmt.Printf("Size: %d/%d, Hit rate: %.1f%%\n", stats.Size, stats.MaxSize, stats.HitRate)

// Detailed stats with top-N most accessed templates
detailed := cache.GetDetailedStats(10)
for _, ts := range detailed.TopTemplates {
    fmt.Printf("  %s: %d hits\n", ts.TemplateName, ts.Hits)
}
App-level configuration

The cache size is configured via application config:

goTemplate:
  cacheSize: 10000    # Max compiled templates to cache (default: 10000)
  enableMetrics: true  # Enable template cache metrics

At startup, call InitFromAppConfig to wire the cache to the config:

gotmpl.InitFromAppConfig(ctx, gotmpl.GoTemplateConfigInput{
    CacheSize:     cfg.GoTemplate.CacheSize,
    EnableMetrics: true,
})

The function is idempotent — subsequent calls are no-ops.

Extensions

The gotmpl/ext package provides an extension system for registering additional template functions, mirroring the pattern in pkg/celexp/ext. Extensions are automatically available to all template executions via NewService() and Execute().

Built-in Extensions
Sprig Functions

Over 100 utility functions from Masterminds/sprig v3 are automatically available in all templates:

// String functions
{{ "hello" | upper }}        // "HELLO"
{{ "HELLO" | lower }}        // "hello"
{{ "  hello  " | trim }}     // "hello"
{{ "hello" | repeat 3 }}     // "hellohellohello"

// Data format functions
{{ dict "key" "value" | toJson }}           // {"key":"value"}
{{ dict "key" "value" | toPrettyJson }}     // formatted JSON

// Math functions
{{ add 1 2 }}                // 3
{{ max 5 3 }}                // 5

// Date functions
{{ now | date "2006-01-02" }}  // today's date

// List functions
{{ list 1 2 3 | join "," }}    // "1,2,3"

See the sprig documentation for the full list.

Custom Extensions
toHcl

Converts a Go object into HCL (HashiCorp Configuration Language) format:

{{ dict "name" "myapp" "port" 8080 | toHcl }}
// Output:
// name = "myapp"
// port = 8080

// Nested objects become HCL blocks:
{{ dict "server" (dict "host" "localhost" "port" 443) | toHcl }}
// Output:
// server {
//   host = "localhost"
//   port = 443
// }
toYaml

Encodes a Go value as a YAML string:

{{ dict "name" "myapp" "port" 8080 | toYaml }}
// Output:
// name: myapp
// port: 8080

// Combined with indent for nested YAML:
{{ .config | toYaml | indent 4 }}
fromYaml

Decodes a YAML string into a map[string]any:

{{ $parsed := fromYaml "name: myapp\nport: 8080" }}
{{ $parsed.name }}  // "myapp"
{{ $parsed.port }}  // 8080
mustToYaml / mustFromYaml

Identical to toYaml and fromYaml respectively. In Go templates, errors always propagate, so behavior is the same. These exist for Helm naming convention compatibility.

Discovering Available Functions
CLI
# List all available functions
scafctl get go-template-functions

# List only custom (non-sprig) functions
scafctl get go-template-functions --custom

# List only sprig functions
scafctl get go-template-functions --sprig

# Get details for a specific function
scafctl get go-template-functions toHcl

# Output as JSON
scafctl get go-template-functions -o json
MCP Server

The list_go_template_functions MCP tool exposes the same information:

Parameter Description
name Filter by function name (substring match)
custom_only Show only custom extensions
sprig_only Show only sprig functions
Adding New Extensions

To add a new custom template function:

  1. Create a sub-package under pkg/gotmpl/ext/ (e.g., ext/myfunc/):
package myfunc

import (
    "github.com/oakwood-commons/scafctl/pkg/gotmpl"
    "text/template"
)

func MyFunc(input string) (string, error) {
    // implementation
    return result, nil
}

func MyFuncDef() gotmpl.ExtFunction {
    return gotmpl.ExtFunction{
        Name:        "myFunc",
        Description: "Describes what myFunc does",
        Custom:      true,
        Links: []string{
            "https://example.com/docs",
        },
        Examples: []gotmpl.Example{
            {
                Description: "Basic usage",
                Template:    `{{ "input" | myFunc }}`,
            },
        },
        Func: template.FuncMap{
            "myFunc": MyFunc,
        },
    }
}
  1. Register in the extension registry (pkg/gotmpl/ext/ext.go):
func Custom() gotmpl.ExtFunctionList {
    return gotmpl.ExtFunctionList{
        hcl.ToHclFunc(),
        extyaml.ToYamlFunc(),
        extyaml.FromYamlFunc(),
        extyaml.MustToYamlFunc(),
        extyaml.MustFromYamlFunc(),
        myfunc.MyFuncDef(), // Add here
    }
}
  1. Add tests in ext/myfunc/myfunc_test.go

The function will be automatically available in all templates, the MCP server, and the CLI.

Architecture

Extensions use a factory pattern to avoid import cycles:

cmd/scafctl/scafctl.go
  └─ gotmpl.SetExtensionFuncMapFactory(gotmplext.AllFuncMap)  // Wire at init

pkg/gotmpl/gotmpl.go
  └─ NewService() calls factory → gets merged FuncMap
  └─ Execute() calls NewService(nil) → extensions included automatically

pkg/gotmpl/ext/ext.go
  └─ All() = Sprig() + Custom()
  └─ AllFuncMap() merges all into template.FuncMap
  • text/template: Standard Go template engine (underlying implementation)
  • text/template/parse: Template AST parsing (used for reference extraction)
  • github.com/oakwood-commons/scafctl/pkg/logger: Structured logging integration
  • github.com/oakwood-commons/scafctl/pkg/celexp: CEL expression evaluation (complementary templating)

License

See the main project LICENSE file.

Documentation

Index

Examples

Constants

View Source
const (
	// DefaultLeftDelim is the default left delimiter for templates
	DefaultLeftDelim = "{{"

	// DefaultRightDelim is the default right delimiter for templates
	DefaultRightDelim = "}}"
)

Variables

View Source
var (

	// DefaultTemplateCacheSize is the default size for the package-level cache.
	DefaultTemplateCacheSize = settings.DefaultGoTemplateCacheSize
)

Package-level default cache (mirrors the CEL cache pattern)

Functions

func InitFromAppConfig added in v0.6.0

func InitFromAppConfig(ctx context.Context, cfg GoTemplateConfigInput)

InitFromAppConfig initializes the Go template subsystem with application configuration. This should be called once during application startup, before any Go templates are evaluated.

This function:

  • Creates a new template cache with the specified size
  • Registers the cache as the default cache via SetCacheFactory

The function is idempotent - subsequent calls after the first are no-ops.

Example:

gotmpl.InitFromAppConfig(ctx, gotmpl.GoTemplateConfigInput{
    CacheSize:     10000,
    EnableMetrics: true,
})

func ResetAppConfigForTesting added in v0.6.0

func ResetAppConfigForTesting()

ResetAppConfigForTesting resets the app config state for testing purposes. This should only be called from tests.

func ResetDefaultCache added in v0.6.0

func ResetDefaultCache()

ResetDefaultCache clears and recreates the default cache. This is intended for testing only.

WARNING: This is not thread-safe with respect to ongoing template compilations. Only call this from test setup functions, not from production code.

func SetAllowEnvFunctions added in v0.6.0

func SetAllowEnvFunctions(allow bool)

SetAllowEnvFunctions controls whether the sprig 'env' and 'expandenv' Go template functions are available. Call this once after loading application config. Defaults to false (deny) — env functions are stripped unless explicitly enabled.

func SetCacheFactory added in v0.6.0

func SetCacheFactory(factory func() *TemplateCache)

SetCacheFactory sets the factory function used to get the global template cache. This should be called once during application initialization (e.g. from InitFromAppConfig). It allows the default cache to be configured from application config without circular dependencies.

This function is thread-safe and uses sync.Once to ensure it's only set once.

func SetContextFuncBinderFactory added in v0.6.0

func SetContextFuncBinderFactory(factory func(ctx context.Context) template.FuncMap)

SetContextFuncBinderFactory registers a factory that produces context-aware template.FuncMap entries. It is called once per template execution with the current context so that functions like `cel` can respect cancellation and timeouts. Call this during application initialization to avoid import cycles.

func SetDefaultCacheSize added in v0.6.0

func SetDefaultCacheSize(size int)

SetDefaultCacheSize sets the size of the default cache. This must be called before the first call to GetDefaultCache() or Service.Execute(). Once the cache is initialized, this function has no effect.

func SetExtensionFuncMapFactory added in v0.6.0

func SetExtensionFuncMapFactory(factory func() template.FuncMap)

SetExtensionFuncMapFactory sets the factory function used to provide extension template functions (sprig + custom). This should be called once during application initialization to wire in extension functions without creating import cycles.

This function is thread-safe and uses sync.Once to ensure it's only set once.

Example (called during application initialization):

gotmpl.SetExtensionFuncMapFactory(ext.AllFuncMap)

func ValidateSyntax added in v0.6.0

func ValidateSyntax(content, leftDelim, rightDelim string) error

ValidateSyntax checks a Go template for parse errors without executing it. It returns nil if the template is syntactically valid. Use leftDelim/rightDelim to override the default "{{" / "}}" delimiters (pass empty strings for defaults).

Types

type Example added in v0.6.0

type Example struct {
	// Description explains what the example demonstrates
	Description string `` /* 138-byte string literal not displayed */

	// Template is the Go template snippet showing usage
	Template string `` /* 138-byte string literal not displayed */

	// Links contains reference URLs for the example
	Links []string `json:"links,omitempty" yaml:"links,omitempty" doc:"Reference URLs for the example" maxItems:"10"`
}

Example describes a usage example for a Go template function.

type ExecuteResult

type ExecuteResult struct {
	// Output is the rendered template content
	Output string `json:"output" yaml:"output" doc:"Rendered template content" maxLength:"10485760"`

	// TemplateName is the name/identifier of the template
	TemplateName string `json:"templateName" yaml:"templateName" doc:"Name/identifier of the template" maxLength:"512" example:"greeting.tmpl"`

	// ReplacementsMade is the number of replacements that were applied
	ReplacementsMade int `json:"replacementsMade" yaml:"replacementsMade" doc:"Number of replacements that were applied" maximum:"1000" example:"3"`
}

ExecuteResult contains the result of template execution

func Execute

func Execute(ctx context.Context, opts TemplateOptions) (*ExecuteResult, error)

Execute is a convenience function that creates a service and executes a template For one-off template execution without custom default functions

Example

ExampleExecute demonstrates basic template execution

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/oakwood-commons/scafctl/pkg/gotmpl"
	"github.com/oakwood-commons/scafctl/pkg/logger"
)

func main() {
	ctx := logger.WithLogger(context.Background(), logger.Get(0))

	result, err := gotmpl.Execute(ctx, gotmpl.TemplateOptions{
		Name:    "greeting.tmpl",
		Content: "Hello, {{.Name}}! You are {{.Age}} years old.",
		Data: map[string]any{
			"Name": "Alice",
			"Age":  30,
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(result.Output)
}
Output:
Hello, Alice! You are 30 years old.

type ExtFunction added in v0.6.0

type ExtFunction struct {
	// Name is the function name as used in templates (e.g., "toHcl", "upper")
	Name string `json:"name,omitempty" yaml:"name,omitempty" doc:"Function name as used in templates" maxLength:"128" example:"toHcl"`

	// Description is a human-readable description of the function
	Description string `` /* 164-byte string literal not displayed */

	// Links contains reference URLs (documentation, source code, etc.)
	Links []string `json:"links,omitempty" yaml:"links,omitempty" doc:"Reference URLs for documentation" maxItems:"10"`

	// Examples provides usage examples for documentation and discoverability
	Examples []Example `json:"examples,omitempty" yaml:"examples,omitempty" doc:"Usage examples for documentation" maxItems:"20"`

	// Custom indicates whether this is a scafctl-specific function (true)
	// or a third-party/built-in function (false, e.g., sprig functions)
	Custom bool `json:"custom,omitempty" yaml:"custom,omitempty" doc:"Whether this is a scafctl-specific function"`

	// Func is the template.FuncMap entry for this function.
	// Excluded from JSON/YAML serialization.
	Func template.FuncMap `json:"-" yaml:"-" doc:"Template FuncMap entry for this function"`
}

ExtFunction describes a Go template function extension with metadata for discoverability via MCP tools and CLI commands.

func (ExtFunction) GetName added in v0.6.0

func (f ExtFunction) GetName() string

GetName returns the function name, implementing the named interface for use with generic filter helpers.

type ExtFunctionList added in v0.6.0

type ExtFunctionList []ExtFunction

ExtFunctionList is a list of ExtFunction entries.

func (ExtFunctionList) FuncMap added in v0.6.0

func (l ExtFunctionList) FuncMap() template.FuncMap

FuncMap merges all individual Func entries into a single template.FuncMap. Later entries override earlier ones if they share the same function name.

type GoTemplateConfigInput added in v0.6.0

type GoTemplateConfigInput struct {
	// CacheSize is the maximum number of compiled templates to cache
	CacheSize int `json:"cacheSize" yaml:"cacheSize" doc:"Maximum number of compiled templates to cache" maximum:"100000" example:"10000"`
	// EnableMetrics enables template cache metrics collection
	EnableMetrics bool `json:"enableMetrics" yaml:"enableMetrics" doc:"Enable template cache metrics collection"`
	// AllowEnvFunctions enables the sprig env/expandenv functions.
	// When false (default), these functions are stripped from the template func map
	// to prevent accidental or malicious env-var exfiltration.
	AllowEnvFunctions bool `json:"allowEnvFunctions" yaml:"allowEnvFunctions" doc:"Allow sprig env/expandenv functions (default: false)"`
}

GoTemplateConfigInput holds the configuration values for Go template initialization. This mirrors config.GoTemplateConfig but avoids circular dependencies.

type GoTemplatingContent

type GoTemplatingContent string

type MissingKeyOption

type MissingKeyOption string

MissingKeyOption defines the behavior when a map key is missing during template execution

const (
	// MissingKeyDefault continues execution and prints "<no value>" for missing keys
	// This is the default behavior
	MissingKeyDefault MissingKeyOption = "default"

	// MissingKeyZero returns the zero value for the map type's element
	MissingKeyZero MissingKeyOption = "zero"

	// MissingKeyError stops execution immediately with an error
	MissingKeyError MissingKeyOption = "error"
)

type Replacement

type Replacement struct {
	// Find is the string to search for in the template content
	Find string `json:"find" yaml:"find" doc:"String to search for in the template content" maxLength:"4096" example:"${LITERAL}"`

	// Replace is the temporary replacement value
	// If empty, a UUID will be generated automatically
	Replace string `` /* 131-byte string literal not displayed */
}

Replacement defines a string replacement to perform before/after templating

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service provides template execution capabilities

func NewService

func NewService(additionalFuncs template.FuncMap) *Service

NewService creates a new template service with all registered extension functions (sprig + custom scafctl extensions) available by default. If additionalFuncs is provided, those functions are merged on top of the extensions, allowing callers to override any extension function.

Extension functions are provided via SetExtensionFuncMapFactory, which should be called during application initialization.

Use NewServiceRaw to create a service without auto-registered extensions.

func NewServiceRaw added in v0.6.0

func NewServiceRaw(defaultFuncs template.FuncMap) *Service

NewServiceRaw creates a new template service without any auto-registered extension functions. Only the explicitly provided functions will be available. Use this when you want a bare template service without sprig or custom extensions.

func NewServiceWithCache added in v0.6.0

func NewServiceWithCache(additionalFuncs template.FuncMap, cache *TemplateCache) *Service

NewServiceWithCache creates a new template service with a custom cache. This is useful for testing or when you want isolated cache instances.

func (*Service) Execute

func (s *Service) Execute(ctx context.Context, opts TemplateOptions) (*ExecuteResult, error)

Execute renders a template with the provided options

Example (Complex)

ExampleService_Execute_complex demonstrates combining multiple features

package main

import (
	"context"
	"fmt"
	"log"
	"text/template"

	"github.com/oakwood-commons/scafctl/pkg/gotmpl"
	"github.com/oakwood-commons/scafctl/pkg/logger"
)

func main() {
	ctx := logger.WithLogger(context.Background(), logger.Get(0))

	// Create service with helper functions
	svc := gotmpl.NewService(template.FuncMap{
		"default": func(defaultVal, val string) string {
			if val == "" {
				return defaultVal
			}
			return val
		},
	})

	result, err := svc.Execute(ctx, gotmpl.TemplateOptions{
		Name:       "report.tmpl",
		Content:    "User: [[.Name]], Status: [[default \"inactive\" .Status]], Code: LITERAL_CODE",
		LeftDelim:  "[[",
		RightDelim: "]]",
		Data: map[string]any{
			"Name":   "David",
			"Status": "",
		},
		Replacements: []gotmpl.Replacement{
			{Find: "LITERAL_CODE", Replace: "TEMP_12345"},
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(result.Output)
}
Output:
User: David, Status: inactive, Code: LITERAL_CODE
Example (CustomDelimiters)

ExampleService_Execute_customDelimiters shows using custom delimiters

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/oakwood-commons/scafctl/pkg/gotmpl"
	"github.com/oakwood-commons/scafctl/pkg/logger"
)

func main() {
	ctx := logger.WithLogger(context.Background(), logger.Get(0))
	svc := gotmpl.NewService(nil)

	result, err := svc.Execute(ctx, gotmpl.TemplateOptions{
		Name:       "jinja-style.tmpl",
		Content:    "Welcome, [[.User]]!",
		LeftDelim:  "[[",
		RightDelim: "]]",
		Data: map[string]any{
			"User": "Bob",
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(result.Output)
}
Output:
Welcome, Bob!
Example (CustomFunctions)

ExampleService_Execute_customFunctions demonstrates using custom template functions

package main

import (
	"context"
	"fmt"
	"log"
	"strings"
	"text/template"

	"github.com/oakwood-commons/scafctl/pkg/gotmpl"
	"github.com/oakwood-commons/scafctl/pkg/logger"
)

func main() {
	ctx := logger.WithLogger(context.Background(), logger.Get(0))

	// Create service with default functions
	svc := gotmpl.NewService(template.FuncMap{
		"upper": strings.ToUpper,
		"lower": strings.ToLower,
	})

	result, err := svc.Execute(ctx, gotmpl.TemplateOptions{
		Name:    "formatted.tmpl",
		Content: "{{upper .FirstName}} {{lower .LastName}}",
		Data: map[string]any{
			"FirstName": "john",
			"LastName":  "DOE",
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(result.Output)
}
Output:
JOHN doe
Example (LoopAndConditionals)

ExampleService_Execute_loopAndConditionals shows template control structures

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/oakwood-commons/scafctl/pkg/gotmpl"
	"github.com/oakwood-commons/scafctl/pkg/logger"
)

func main() {
	ctx := logger.WithLogger(context.Background(), logger.Get(0))
	svc := gotmpl.NewService(nil)

	content := `{{if .Title}}Title: {{.Title}}
{{end}}Items:{{range .Items}}
- {{.}}{{end}}`

	result, err := svc.Execute(ctx, gotmpl.TemplateOptions{
		Name:    "list.tmpl",
		Content: content,
		Data: map[string]any{
			"Title": "Shopping List",
			"Items": []string{"Apples", "Bananas", "Oranges"},
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(result.Output)
}
Output:
Title: Shopping List
Items:
- Apples
- Bananas
- Oranges
Example (MissingKeyHandling)

ExampleService_Execute_missingKeyHandling demonstrates handling missing keys

package main

import (
	"context"
	"fmt"

	"github.com/oakwood-commons/scafctl/pkg/gotmpl"
	"github.com/oakwood-commons/scafctl/pkg/logger"
)

func main() {
	ctx := logger.WithLogger(context.Background(), logger.Get(0))
	svc := gotmpl.NewService(nil)

	// Default behavior: prints "<no value>"
	result1, _ := svc.Execute(ctx, gotmpl.TemplateOptions{
		Name:       "default.tmpl",
		Content:    "Status: {{.Status}}",
		Data:       map[string]any{}, // Status key missing
		MissingKey: gotmpl.MissingKeyDefault,
	})
	fmt.Println(result1.Output)

	// Zero behavior: returns zero value (empty string for string type)
	result2, _ := svc.Execute(ctx, gotmpl.TemplateOptions{
		Name:       "zero.tmpl",
		Content:    "Count: {{.Count}}",
		Data:       map[string]any{}, // Count key missing
		MissingKey: gotmpl.MissingKeyZero,
	})
	fmt.Println(result2.Output)

}
Output:
Status: <no value>
Count: <no value>
Example (Replacements)

ExampleService_Execute_replacements shows protecting literal strings from template processing

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/oakwood-commons/scafctl/pkg/gotmpl"
	"github.com/oakwood-commons/scafctl/pkg/logger"
)

func main() {
	ctx := logger.WithLogger(context.Background(), logger.Get(0))
	svc := gotmpl.NewService(nil)

	// Template contains both template syntax and literal template syntax
	content := `Name: {{.Name}}
Literal template syntax: {{KEEP_THIS_LITERAL}}`

	result, err := svc.Execute(ctx, gotmpl.TemplateOptions{
		Name:    "mixed.tmpl",
		Content: content,
		Data: map[string]any{
			"Name": "Charlie",
		},
		Replacements: []gotmpl.Replacement{
			{Find: "{{KEEP_THIS_LITERAL}}"},
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(result.Output)
}
Output:
Name: Charlie
Literal template syntax: {{KEEP_THIS_LITERAL}}

func (*Service) GetReferences

func (s *Service) GetReferences(ctx context.Context, opts TemplateOptions) ([]TemplateReference, error)

GetReferences extracts all references to Data from a Go template This method parses the template and extracts variable references (e.g., .User, .Items) It excludes function calls and Go template control variables (like $var)

Example

ExampleService_GetReferences demonstrates extracting references using the Service

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/oakwood-commons/scafctl/pkg/gotmpl"
	"github.com/oakwood-commons/scafctl/pkg/logger"
)

func main() {
	ctx := logger.WithLogger(context.Background(), logger.Get(0))
	svc := gotmpl.NewService(nil)

	refs, err := svc.GetReferences(ctx, gotmpl.TemplateOptions{
		Name:       "config.tmpl",
		Content:    "[[.App.Name]] version [[.App.Version]]",
		LeftDelim:  "[[",
		RightDelim: "]]",
	})
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("Template references:")
	for _, ref := range refs {
		fmt.Printf("  %s\n", ref.Path)
	}
}
Output:
Template references:
  .App.Name
  .App.Version

type TemplateCache added in v0.6.0

type TemplateCache struct {
	// contains filtered or unexported fields
}

TemplateCache is a thread-safe LRU cache for compiled Go templates. It caches parsed *template.Template objects by a hash of the template content and configuration, allowing reuse of expensive parse operations.

The cache also tracks detailed template-level metrics for monitoring and debugging purposes.

func GetAppConfigCache added in v0.6.0

func GetAppConfigCache() *TemplateCache

GetAppConfigCache returns the cache created by InitFromAppConfig. Returns nil if InitFromAppConfig has not been called.

func GetDefaultCache added in v0.6.0

func GetDefaultCache() *TemplateCache

GetDefaultCache returns the package-level cache instance. If a cache factory has been registered via SetCacheFactory, that factory is used. Otherwise, the cache is lazily initialized on first access with DefaultTemplateCacheSize entries. All calls return the same cache instance (singleton pattern).

Use this when you need to access cache statistics:

stats := gotmpl.GetDefaultCache().Stats()
fmt.Printf("Cache hit rate: %.1f%%\n", stats.HitRate)

func NewTemplateCache added in v0.6.0

func NewTemplateCache(maxSize int) *TemplateCache

NewTemplateCache creates a new template cache with the specified maximum size. When the cache reaches maxSize, the least recently used entry will be evicted. A maxSize of 0 or negative value defaults to 100.

Template-level metrics are tracked for the top 1000 most accessed templates to avoid unbounded memory growth.

func (*TemplateCache) Clear added in v0.6.0

func (c *TemplateCache) Clear()

Clear removes all entries from the cache but preserves statistics. Use ClearWithStats() to also reset hit/miss counters.

func (*TemplateCache) ClearWithStats added in v0.6.0

func (c *TemplateCache) ClearWithStats()

ClearWithStats removes all entries and resets all statistics to zero.

func (*TemplateCache) Get added in v0.6.0

func (c *TemplateCache) Get(key string) (*template.Template, bool)

Get retrieves a template from the cache if it exists. Returns a clone of the cached template and true if found, nil and false otherwise. The clone is safe for concurrent execution with different data.

func (*TemplateCache) GetDetailedStats added in v0.6.0

func (c *TemplateCache) GetDetailedStats(topN int) TemplateCacheStats

GetDetailedStats returns cache statistics including template-level metrics. If topN is 0, returns all tracked templates. Otherwise returns the top N most accessed templates sorted by hit count (descending).

func (*TemplateCache) Put added in v0.6.0

func (c *TemplateCache) Put(key string, tmpl *template.Template, templateName string)

Put adds a template to the cache with its name for metrics tracking. If the cache is full, it evicts the least recently used entry before adding the new one.

func (*TemplateCache) ResetStats added in v0.6.0

func (c *TemplateCache) ResetStats()

ResetStats resets cache statistics (hits, misses, evictions) without removing cached entries.

func (*TemplateCache) Stats added in v0.6.0

func (c *TemplateCache) Stats() TemplateCacheStats

Stats returns cache statistics.

type TemplateCacheStats added in v0.6.0

type TemplateCacheStats struct {
	Size          int            `json:"size" yaml:"size" doc:"Current number of cached templates" maximum:"100000" example:"42"`
	MaxSize       int            `json:"max_size" yaml:"max_size" doc:"Maximum cache capacity" maximum:"100000" example:"1000"`
	Hits          uint64         `json:"hits" yaml:"hits" doc:"Total cache hits"`
	Misses        uint64         `json:"misses" yaml:"misses" doc:"Total cache misses"`
	Evictions     uint64         `json:"evictions" yaml:"evictions" doc:"Total cache evictions"`
	HitRate       float64        `json:"hit_rate" yaml:"hit_rate" doc:"Cache hit rate percentage"`
	TotalAccesses uint64         `json:"total_accesses" yaml:"total_accesses" doc:"Total number of cache accesses"`
	TopTemplates  []TemplateStat `json:"top_templates,omitempty" yaml:"top_templates,omitempty" doc:"Most frequently accessed templates" maxItems:"50"`
}

TemplateCacheStats contains cache performance statistics.

type TemplateOptions

type TemplateOptions struct {
	// Content is the template content as a string
	Content string `json:"content" yaml:"content" doc:"Template content as a string" maxLength:"1048576" example:"Hello {{ .Name }}"`

	// Name is the reference name/identifier for the template (e.g., file path)
	// Used in logging and error messages
	Name string `json:"name" yaml:"name" doc:"Reference name/identifier for the template" maxLength:"512" example:"greeting.tmpl"`

	// Data is the data source passed to the template during execution
	Data any `json:"data,omitempty" yaml:"data,omitempty" doc:"Data source passed to the template during execution"`

	// LeftDelim sets the left action delimiter (default: "{{")
	LeftDelim string `json:"leftDelim,omitempty" yaml:"leftDelim,omitempty" doc:"Left action delimiter" maxLength:"8" example:"{{"`

	// RightDelim sets the right action delimiter (default: "}}")
	RightDelim string `json:"rightDelim,omitempty" yaml:"rightDelim,omitempty" doc:"Right action delimiter" maxLength:"8" example:"}}"`

	// Replacements is a map of strings to replace before template execution
	// The key is replaced with a UUID placeholder, then restored after execution
	// This helps avoid template parsing errors for content that should be literal
	Replacements []Replacement `` /* 143-byte string literal not displayed */

	// Funcs is a map of custom template functions to make available
	// These are added to the template's function map
	Funcs template.FuncMap `json:"-" yaml:"-" doc:"Custom template functions to make available"`

	// MissingKey controls the behavior when a map key is missing
	// Default: MissingKeyDefault (prints "<no value>")
	// Options: MissingKeyDefault, MissingKeyZero, MissingKeyError
	MissingKey MissingKeyOption `` /* 129-byte string literal not displayed */

	// DisableBuiltinFuncs disables the built-in template functions
	// By default, basic functions like "html", "js", etc. are available
	DisableBuiltinFuncs bool `json:"disableBuiltinFuncs,omitempty" yaml:"disableBuiltinFuncs,omitempty" doc:"Disables the built-in template functions"`
}

TemplateOptions contains configuration for template execution

type TemplateReference

type TemplateReference struct {
	// Path is the dot-notation path to the data (e.g., ".User.Name", ".Items")
	Path string

	// Position is the line:column position in the template (if available)
	Position string
}

TemplateReference represents a reference to data in a template

func GetGoTemplateReferences

func GetGoTemplateReferences(templateContent, leftDelim, rightDelim string) ([]TemplateReference, error)

GetGoTemplateReferences is a convenience function that creates a service and extracts references For one-off reference extraction without needing to create a service

Example

ExampleGetGoTemplateReferences demonstrates extracting data references from templates using the convenience function

package main

import (
	"fmt"
	"log"

	"github.com/oakwood-commons/scafctl/pkg/gotmpl"
)

func main() {
	template := `
{{if .User.IsAdmin}}
	Welcome, {{.User.Name}}!
	{{range .User.Permissions}}
		- {{.}}
	{{end}}
{{end}}
`

	refs, err := gotmpl.GetGoTemplateReferences(template, "", "")
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("Found references:")
	for _, ref := range refs {
		fmt.Printf("  %s\n", ref.Path)
	}
}
Output:
Found references:
  .User.IsAdmin
  .User.Name
  .User.Permissions

type TemplateStat added in v0.6.0

type TemplateStat struct {
	TemplateName string    `json:"template_name" yaml:"template_name" doc:"Template name/identifier" maxLength:"512" example:"greeting.tmpl"`
	Hits         uint64    `json:"hits" yaml:"hits" doc:"Number of cache hits for this template"`
	LastAccess   time.Time `json:"last_access" yaml:"last_access" doc:"Time of last access"`
}

TemplateStat contains statistics for a specific template.

Directories

Path Synopsis
Package detail provides functions for building structured output representations of Go template extension functions.
Package detail provides functions for building structured output representations of Go template extension functions.
ext
Package ext provides the Go template extension function registry.
Package ext provides the Go template extension function registry.
celeval
Package celeval provides a Go template extension function for evaluating CEL (Common Expression Language) expressions inline within Go templates.
Package celeval provides a Go template extension function for evaluating CEL (Common Expression Language) expressions inline within Go templates.
collections
Package collections provides Go template extension functions for filtering and projecting lists of maps, enabling common data transformation patterns directly within Go templates.
Package collections provides Go template extension functions for filtering and projecting lists of maps, enabling common data transformation patterns directly within Go templates.
dns
Package dns provides Go template extension functions for converting arbitrary strings into DNS-safe label format (RFC 1123).
Package dns provides Go template extension functions for converting arbitrary strings into DNS-safe label format (RFC 1123).
hcl
Package hcl provides a Go template extension function for converting Go objects into HCL (HashiCorp Configuration Language) format.
Package hcl provides a Go template extension function for converting Go objects into HCL (HashiCorp Configuration Language) format.
yaml
Package yaml provides Go template extension functions for YAML serialization and deserialization.
Package yaml provides Go template extension functions for YAML serialization and deserialization.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL