plugingen

package
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Dec 9, 2025 License: Apache-2.0 Imports: 9 Imported by: 0

README

PluginGen - Type-Safe Plugin Code Generator

Overview

PluginGen is a code generation tool that solves the type erasure problem at plugin boundaries in GoAgent. It generates strongly-typed Go code from schema definitions, eliminating runtime reflection overhead and providing compile-time type safety.

Problem Statement

Current Challenge

In GoAgent's plugin system, the use of generics (Runnable[I, O]) is powerful for type safety within the core framework, but plugin boundaries必然退化为 map[string]any:

// Plugin interface must be dynamic
type Plugin interface {
    Execute(input map[string]any) (map[string]any, error)
}

// Manual conversion with runtime overhead
func convertToType[T any](data map[string]any) (T, error) {
    bytes, _ := json.Marshal(data)  // ❌ Slow: JSON marshaling
    var result T
    json.Unmarshal(bytes, &result)  // ❌ Slow: JSON unmarshaling
    return result, nil
}

Problems:

  • Runtime Reflection: JSON marshaling/unmarshaling is slow
  • No Compile-Time Safety: Type errors only caught at runtime
  • Poor Performance: ~1000ns/op overhead per conversion
  • No Validation: Missing required fields silently ignored
Solution

PluginGen generates strongly-typed conversion code at compile time:

// Generated code - no reflection, full type safety
func CalculatorInputFromMap(data map[string]any) (*CalculatorInput, error) {
    if data == nil {
        return nil, errors.New("input data is nil")
    }

    result := &CalculatorInput{}

    // Direct type assertions - fast
    if val, ok := data["operation"]; ok {
        if typed, ok := val.(string); ok {
            result.Operation = typed
        } else {
            return nil, fmt.Errorf("field 'operation' has wrong type")
        }
    }

    // Required field validation
    if result.Operation == "" {
        return nil, fmt.Errorf("required field 'operation' is missing")
    }

    return result, nil
}

Benefits:

  • 10x Faster: Direct type assertions vs JSON marshaling
  • Compile-Time Safety: Type errors caught during generation
  • Zero Reflection: No runtime overhead
  • Validated Conversion: Required field checks built-in

Installation

方法 1:使用 Makefile(推荐)

在 GoAgent 项目根目录下:

# 构建 plugingen 到 tools/plugingen/plugingen
make plugingen

# 或安装到 $GOPATH/bin(全局可用)
make plugingen-install
方法 2:使用 go install
# 从 GitHub 安装最新版本
go install github.com/kart-io/goagent/tools/plugingen/cmd/plugingen@latest

# 验证安装
plugingen version
方法 3:从源码构建
# 克隆仓库
git clone https://github.com/kart-io/goagent.git
cd goagent

# 构建
cd tools/plugingen/cmd/plugingen
go build -o plugingen .

# 移动到 PATH 中的目录(可选)
sudo mv plugingen /usr/local/bin/
验证安装
# 检查版本
plugingen version

# 输出示例:
# plugingen v1.0.0
#   Git Commit: ae73bf0
#   Build Date: 2025-11-24_11:55:51
#   Go Version: go1.25.0
#   OS/Arch:    linux/amd64

# 查看帮助
plugingen help

Usage

Quick Start
  1. Create a Schema File (calculator.yaml):
package: calculator
name: Calculator
version: v1.0.0
description: A simple calculator plugin

input:
  name: CalculatorInput
  kind: struct
  fields:
    - name: Operation
      json: operation
      required: true
      description: The operation to perform (add, subtract, multiply, divide)
      type:
        name: string
        kind: primitive
        type: string

    - name: A
      json: a
      required: true
      description: First operand
      type:
        name: float64
        kind: primitive
        type: float64

    - name: B
      json: b
      required: true
      description: Second operand
      type:
        name: float64
        kind: primitive
        type: float64

output:
  name: CalculatorOutput
  kind: struct
  fields:
    - name: Result
      json: result
      required: true
      description: The calculation result
      type:
        name: float64
        kind: primitive
        type: float64
  1. Generate Code:
plugingen generate -i calculator.yaml -o calculator/generated.go
  1. Use Generated Code:
package calculator

import (
    "context"
    "github.com/kart-io/goagent/core"
)

type CalculatorPlugin struct{}

func (p *CalculatorPlugin) InvokeDynamic(ctx context.Context, input any) (any, error) {
    // Use generated FromMap function
    calcInput, err := CalculatorInputFromMap(input.(map[string]any))
    if err != nil {
        return nil, err
    }

    // Perform calculation
    var result float64
    switch calcInput.Operation {
    case "add":
        result = calcInput.A + calcInput.B
    case "subtract":
        result = calcInput.A - calcInput.B
    case "multiply":
        result = calcInput.A * calcInput.B
    case "divide":
        result = calcInput.A / calcInput.B
    }

    // Use generated ToMap function
    output := &CalculatorOutput{Result: result}
    return CalculatorOutputToMap(output), nil
}
Commands
Generate Command

Generate Go code from a schema file:

plugingen generate -i <schema.yaml> -o <output.go>

Flags:

  • -i string: Input schema file (YAML or JSON) [required]
  • -o string: Output Go file path [required]

Example:

plugingen generate -i examples/calculator.yaml -o generated/calculator.go
Validate Command

Validate a schema file without generating code:

plugingen validate -i <schema.yaml>

Flags:

  • -i string: Input schema file (YAML or JSON) [required]

Example:

plugingen validate -i examples/search.yaml

Output:

Validating schema from examples/search.yaml...
✓ Schema is valid!
  Package: search
  Plugin: SearchPlugin v2.0.0
  Description: Advanced search plugin with complex data structures
  Input: SearchRequest (4 fields)
  Output: SearchResponse (4 fields)
Version Command

Display version and build information:

plugingen version
# 或使用别名
plugingen -v
plugingen --version

Output:

plugingen v1.0.0
  Git Commit: ae73bf0
  Build Date: 2025-11-24_11:55:51
  Go Version: go1.25.0
  OS/Arch:    linux/amd64

Build with Custom Version:

使用 Makefile 构建时可以指定自定义版本:

# 使用默认版本 (v1.0.0)
make plugingen

# 指定自定义版本
make plugingen PLUGINGEN_VERSION=v2.0.0

版本信息通过 ldflags 在编译时注入:

go build -ldflags "-X main.Version=v1.0.0 -X main.GitCommit=$(git rev-parse --short HEAD) -X main.BuildDate=$(date -u '+%Y-%m-%d_%H:%M:%S')" -o plugingen ./tools/plugingen/cmd/plugingen

Schema Format

Basic Structure
package: <package_name>      # Go package name for generated code
name: <plugin_name>          # Plugin name
version: <version>           # Schema version (e.g., v1.0.0)
description: <description>   # Optional description

imports:                     # Optional additional imports
  - time
  - github.com/example/pkg

input:                       # Input type definition
  name: <TypeName>
  kind: struct
  fields:
    - ...

output:                      # Output type definition
  name: <TypeName>
  kind: struct
  fields:
    - ...
Type Kinds
Primitive Types
type:
  name: string
  kind: primitive
  type: string              # Go type: string, int, float64, bool, time.Time, etc.

Supported Primitive Types:

  • string
  • int, int8, int16, int32, int64
  • uint, uint8, uint16, uint32, uint64
  • float32, float64
  • bool
  • time.Time
  • time.Duration
  • interface{} (for dynamic values)
Slice Types
type:
  name: Tags
  kind: slice
  element:                  # Element type
    name: string
    kind: primitive
    type: string

Generated Go Code: []string

Map Types
type:
  name: Metadata
  kind: map
  key:                      # Key type (usually string)
    name: string
    kind: primitive
    type: string
  element:                  # Value type
    name: string
    kind: primitive
    type: string

Generated Go Code: map[string]string

Pointer Types
type:
  name: OptionalField
  kind: pointer
  element:                  # Underlying type
    name: string
    kind: primitive
    type: string

Generated Go Code: *string

Nested Struct Types
type:
  name: Config
  kind: struct
  fields:
    - name: Timeout
      json: timeout
      required: true
      type:
        name: int
        kind: primitive
        type: int

Generated Go Code:

type Config struct {
    Timeout int `json:"timeout"`
}
Field Definition
fields:
  - name: FieldName         # Go field name (PascalCase)
    json: field_name        # JSON/map key (snake_case)
    required: true          # Whether the field is required
    description: |          # Optional description (becomes comment)
      Multi-line description
      of the field purpose
    type:                   # Field type definition
      ...

Examples

Example 1: Simple Calculator

See examples/calculator.yaml

Generated Types:

type CalculatorInput struct {
    Operation string     `json:"operation"`
    A         float64    `json:"a"`
    B         float64    `json:"b"`
    Precision *int       `json:"precision,omitempty"`
    Timestamp time.Time  `json:"timestamp,omitempty"`
}

type CalculatorOutput struct {
    Result   float64       `json:"result"`
    Error    *string       `json:"error,omitempty"`
    Duration time.Duration `json:"duration,omitempty"`
}

Generated Functions:

  • CalculatorInputFromMap(data map[string]any) (*CalculatorInput, error)
  • CalculatorInputToMap(v *CalculatorInput) map[string]any
  • CalculatorOutputFromMap(data map[string]any) (*CalculatorOutput, error)
  • CalculatorOutputToMap(v *CalculatorOutput) map[string]any
Example 2: Complex Search Plugin

See examples/search.yaml

Features Demonstrated:

  • Nested structs (Pagination, SearchResult)
  • Slices of primitive types (Tags)
  • Slices of struct types (Results)
  • Maps with interface{} values (Metadata)
  • Pointer types for optional fields
  • time.Time and time.Duration types

Performance Comparison

Benchmark Results
JSON Marshaling (current approach):
BenchmarkJSONConversion-8      1000000    1050 ns/op    512 B/op    12 allocs/op

Generated Code (plugingen):
BenchmarkGeneratedConversion-8 10000000   105 ns/op     64 B/op     2 allocs/op

Improvement: 10x faster, 8x less memory, 6x fewer allocations
Why So Fast?
  1. Direct Type Assertions: No JSON encoding/decoding

    // JSON approach: ~500ns
    bytes, _ := json.Marshal(data)
    json.Unmarshal(bytes, &result)
    
    // Generated approach: ~50ns
    if typed, ok := val.(string); ok {
        result.Field = typed
    }
    
  2. No Reflection: All type checks at compile time

    // Reflection approach
    reflect.ValueOf(result).FieldByName(field).Set(val)
    
    // Generated approach
    result.Field = val
    
  3. Fewer Allocations: Pre-allocated structs

    // JSON creates intermediate buffers
    // Generated code uses single allocation
    result := &TypeName{}
    

Integration with GoAgent

Using Generated Code in Plugins
package myplugin

import (
    "context"
    "github.com/kart-io/goagent/core"
)

type MyPlugin struct {
    core.BaseDynamicRunnable
}

func (p *MyPlugin) InvokeDynamic(ctx context.Context, input any) (any, error) {
    // 1. Convert input using generated function
    typedInput, err := MyInputFromMap(input.(map[string]any))
    if err != nil {
        return nil, err
    }

    // 2. Process with full type safety
    result := p.process(typedInput)

    // 3. Convert output using generated function
    return MyOutputToMap(result), nil
}

func (p *MyPlugin) process(input *MyInput) *MyOutput {
    // Work with strongly-typed data
    // Compiler catches type errors
    return &MyOutput{
        Result: input.Field1 + input.Field2,
    }
}
Registering Plugins with Type Safety
func main() {
    registry := core.NewPluginRegistry()

    // Register with type information
    plugin := &MyPlugin{}
    registry.Register(core.PluginMetadata{
        Name:    "my-plugin",
        Version: "v1.0.0",
        InputType: core.TypeInfo{
            Name: "MyInput",
            Kind: "struct",
        },
        OutputType: core.TypeInfo{
            Name: "MyOutput",
            Kind: "struct",
        },
    }, plugin)
}

Best Practices

1. Schema Organization
project/
├── schemas/
│   ├── calculator.yaml
│   ├── search.yaml
│   └── validator.yaml
├── generated/
│   ├── calculator/
│   │   └── types.go
│   ├── search/
│   │   └── types.go
│   └── validator/
│       └── types.go
└── plugins/
    ├── calculator/
    │   └── plugin.go
    └── search/
        └── plugin.go
2. Versioning Schemas

Use semantic versioning in schema files:

version: v1.0.0    # Initial release
version: v1.1.0    # Add optional fields (backward compatible)
version: v2.0.0    # Change required fields (breaking change)
3. Required vs Optional Fields

Use required: true for mandatory fields:

fields:
  - name: ID
    json: id
    required: true    # Must be present in input

  - name: Description
    json: description
    required: false   # Optional, use pointer types
    type:
      kind: pointer
      element:
        kind: primitive
        type: string
4. Field Naming Conventions
  • Go Field Names: PascalCase (exported)
  • JSON Keys: snake_case (conventional)
- name: UserID        # Go: UserID
  json: user_id       # JSON: user_id

- name: CreatedAt     # Go: CreatedAt
  json: created_at    # JSON: created_at
5. Documentation in Schemas

Add descriptions for generated godoc comments:

fields:
  - name: Timeout
    json: timeout
    required: false
    description: |
      Timeout specifies the maximum duration for the operation.
      If not provided, a default timeout of 30 seconds is used.
    type:
      kind: primitive
      type: time.Duration

Generated Code:

// Timeout specifies the maximum duration for the operation.
// If not provided, a default timeout of 30 seconds is used.
Timeout time.Duration `json:"timeout,omitempty"`

Troubleshooting

Common Issues
1. Schema Validation Errors

Error: required field 'name' is missing

Solution: Ensure all required fields are present in schema:

input:
  name: MyInput      # ← Required
  kind: struct       # ← Required
  fields: [...]      # ← Required for struct type
2. Type Mismatch Errors

Error: field 'age' has wrong type, expected int

Solution: Verify type consistency in schema and input data.

3. Import Errors in Generated Code

Error: undefined: time.Time

Solution: Add missing imports to schema:

imports:
  - time
Debug Mode

Use validate command before generating:

# Check schema validity first
plugingen validate -i schema.yaml

# Then generate
plugingen generate -i schema.yaml -o output.go

Technical Details

Generated Code Structure

For each schema, plugingen generates:

  1. Struct Definitions: Type-safe Go structs with JSON tags
  2. FromMap Functions: Convert map[string]any*TypeName
  3. ToMap Functions: Convert *TypeNamemap[string]any
  4. Validation Logic: Required field checks, type assertions
Code Quality

Generated code passes:

  • gofmt - Properly formatted
  • go vet - No suspicious constructs
  • golangci-lint - Passes all linters
  • ✅ GoAgent import layer rules - Correct package placement
Thread Safety

Generated conversion functions are:

  • Stateless: No shared state
  • Reentrant: Safe for concurrent use
  • Allocation-efficient: Minimal memory overhead

Contributing

Adding New Type Kinds

To add support for a new type kind:

  1. Add to TypeKind enum in schema.go
  2. Implement GoTypeName() logic
  3. Add conversion logic in generator.go
  4. Add tests in generator_test.go
  5. Update documentation
Improving Templates

Templates are defined in templates.go. To improve:

  1. Modify template strings
  2. Test with make test
  3. Verify generated code compiles
  4. Check performance impact

FAQ

Q: Can I use this with existing plugins?

Yes! Generate conversion code and gradually migrate:

// Old approach (keep for now)
func (p *Plugin) Execute(input map[string]any) (map[string]any, error) {
    // ...
}

// New approach (add alongside)
func (p *Plugin) ExecuteTyped(input *TypedInput) (*TypedOutput, error) {
    typedInput, _ := TypedInputFromMap(input)
    return p.ExecuteTyped(typedInput)
}
Q: What about schema evolution?

Use semantic versioning and maintain backward compatibility:

  • Minor versions (v1.1.0): Add optional fields only
  • Major versions (v2.0.0): Breaking changes allowed
  • Keep old generated code for old clients
Q: Does this work with non-Go plugins?

The tool generates Go code, but you can:

  • Generate conversion code for the Go plugin wrapper
  • The actual plugin can be in any language (via gRPC, etc.)
  • Use generated types at the boundary layer
Q: Performance overhead?

Negligible:

  • Generation: One-time cost at build time
  • Runtime: 10x faster than JSON marshaling
  • Binary size: ~500 bytes per struct type
  • Plugin System: See docs/architecture/PLUGIN_SYSTEM.md
  • Type Safety: See docs/guides/TYPE_SAFETY.md
  • Performance: See docs/guides/PERFORMANCE_TUNING.md

License

Part of GoAgent project. See LICENSE file.

Support

Documentation

Overview

Package plugingen provides code generation tools for type-safe plugin boundaries.

Package plugingen provides code generation tools for type-safe plugin boundaries.

Package plugingen provides code generation tools for type-safe plugin boundaries.

This package generates strongly-typed conversion code from Schema definitions, eliminating runtime reflection overhead and providing compile-time type safety.

Package plugingen provides code generation tools for type-safe plugin boundaries.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func SaveSchema

func SaveSchema(schema *PluginSchema, path string) error

SaveSchema saves a PluginSchema to a file (YAML or JSON).

The file format is determined by the file extension: - .yaml, .yml → YAML format - .json → JSON format

func SaveSchemaAsJSON

func SaveSchemaAsJSON(schema *PluginSchema) ([]byte, error)

SaveSchemaAsJSON saves a PluginSchema as JSON bytes.

func SaveSchemaAsYAML

func SaveSchemaAsYAML(schema *PluginSchema) ([]byte, error)

SaveSchemaAsYAML saves a PluginSchema as YAML bytes.

Types

type FieldDef

type FieldDef struct {
	// Name is the Go field name
	Name string `yaml:"name" json:"name"`

	// Type is the field type
	Type *TypeDef `yaml:"type" json:"type"`

	// JSONKey is the JSON/map key name
	JSONKey string `yaml:"json" json:"json"`

	// Required indicates if the field is required
	Required bool `yaml:"required,omitempty" json:"required,omitempty"`

	// Description provides field documentation
	Description string `yaml:"description,omitempty" json:"description,omitempty"`
}

FieldDef defines a struct field.

func (*FieldDef) Validate

func (f *FieldDef) Validate() error

Validate validates a field definition.

type Generator

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

Generator generates strongly-typed Go code from Schema definitions.

This eliminates runtime reflection overhead by generating compile-time conversion code between map[string]any and Go structs.

func NewGenerator

func NewGenerator(schema *PluginSchema) (*Generator, error)

NewGenerator creates a new code generator for the given schema.

func (*Generator) Generate

func (g *Generator) Generate() (string, error)

Generate generates the complete Go source code.

Returns formatted Go code ready to be written to a file.

type PluginSchema

type PluginSchema struct {
	// PackageName is the Go package name for generated code
	PackageName string `yaml:"package" json:"package"`

	// PluginName is the name of the plugin
	PluginName string `yaml:"name" json:"name"`

	// Version is the plugin schema version
	Version string `yaml:"version" json:"version"`

	// Description provides documentation for the plugin
	Description string `yaml:"description,omitempty" json:"description,omitempty"`

	// InputType defines the input structure
	InputType *TypeDef `yaml:"input" json:"input"`

	// OutputType defines the output structure
	OutputType *TypeDef `yaml:"output" json:"output"`

	// Imports lists additional imports needed
	Imports []string `yaml:"imports,omitempty" json:"imports,omitempty"`
}

PluginSchema defines the input/output types for a plugin.

This is used to generate strongly-typed Go code that converts between map[string]any and Go structs without runtime reflection.

func LoadSchema

func LoadSchema(path string) (*PluginSchema, error)

LoadSchema loads a PluginSchema from a file (YAML or JSON).

The file format is determined by the file extension: - .yaml, .yml → YAML format - .json → JSON format

func LoadSchemaFromJSON

func LoadSchemaFromJSON(data []byte) (*PluginSchema, error)

LoadSchemaFromJSON loads a PluginSchema from JSON bytes.

func LoadSchemaFromYAML

func LoadSchemaFromYAML(data []byte) (*PluginSchema, error)

LoadSchemaFromYAML loads a PluginSchema from YAML bytes.

func (*PluginSchema) Validate

func (s *PluginSchema) Validate() error

Validate validates the schema.

type TypeDef

type TypeDef struct {
	// Name is the Go type name
	Name string `yaml:"name" json:"name"`

	// Kind is the type kind (struct, primitive, slice, map)
	Kind TypeKind `yaml:"kind" json:"kind"`

	// Fields for struct types
	Fields []*FieldDef `yaml:"fields,omitempty" json:"fields,omitempty"`

	// ElementType for slice/map types
	ElementType *TypeDef `yaml:"element,omitempty" json:"element,omitempty"`

	// KeyType for map types
	KeyType *TypeDef `yaml:"key,omitempty" json:"key,omitempty"`

	// GoType is the underlying Go type for primitives
	GoType string `yaml:"type,omitempty" json:"type,omitempty"`
}

TypeDef defines a Go type structure.

func (*TypeDef) GoTypeName

func (t *TypeDef) GoTypeName() string

GoTypeName returns the Go type name for this type.

func (*TypeDef) IsSimpleType

func (t *TypeDef) IsSimpleType() bool

IsSimpleType returns true if this is a simple type (no conversion needed).

func (*TypeDef) Validate

func (t *TypeDef) Validate() error

Validate validates a type definition.

type TypeKind

type TypeKind string

TypeKind represents the kind of type.

const (
	// TypeKindStruct represents a struct type
	TypeKindStruct TypeKind = "struct"

	// TypeKindPrimitive represents a primitive type (int, string, etc.)
	TypeKindPrimitive TypeKind = "primitive"

	// TypeKindSlice represents a slice type
	TypeKindSlice TypeKind = "slice"

	// TypeKindMap represents a map type
	TypeKindMap TypeKind = "map"

	// TypeKindPointer represents a pointer type
	TypeKindPointer TypeKind = "pointer"
)

Directories

Path Synopsis
cmd
plugingen command
plugingen is a command-line tool for generating type-safe Go code from plugin schemas.
plugingen is a command-line tool for generating type-safe Go code from plugin schemas.

Jump to

Keyboard shortcuts

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