Skip to main content

A powerful, template-driven code generation tool for Protocol Buffers

Project description

๐Ÿง™ Wizard Codegen

CircleCI codecov Python 3.10+

A powerful, template-driven code generation tool for Protocol Buffers

Wizard Codegen transforms your .proto definitions into typed code for multiple programming languages using customizable Jinja2 templates. Whether you're building React components, Swift structs, Kotlin data classes, or Go handlers โ€” this tool has you covered.


๐Ÿ“‘ Table of Contents


โœจ Features

Feature Description
Multi-Language Support Generate code for TypeScript, Swift, Kotlin, Go, and more
Jinja2 Templates Full power of Jinja2 templating with custom filters
Flexible Filtering Target specific messages, enums, or services with where clauses
Git Proto Sources Fetch proto definitions directly from Git repositories
Multiple Write Modes overwrite, append, or write-once file generation
Custom Hooks Extend with your own Jinja filters and helpers
Name Transformations Auto-convert between snake_case, kebab-case, PascalCase, camelCase, MACROCASE, MACRO_SNAKE_CASE
Dry Run Mode Preview changes without writing files
Verbose Output Debug with detailed proto and context inspection
Nested Type Support Full support for nested messages and enums

๐Ÿš€ Quick Start

1. Install Prerequisites

Protocol Buffer Compiler (protoc):

# macOS
brew install protobuf

# Ubuntu/Debian
sudo apt install -y protobuf-compiler

# Verify installation
protoc --version

uv (Python package manager):

# macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh

# Or via Homebrew
brew install uv

# Verify installation
uv --version

2. Install Wizard Codegen

uv tool install wizard-codegen

3. Create Configuration

Create wizard/codegen.yaml in your project:

proto:
  root: "path/to/your/protos"    # Local path to proto files
  cache_dir: ".cache/protos"
  files:
    - "**/*.proto"

targets:
  typescript:
    templates: "wizard/templates/ts"
    out: "src/generated/ts"
    render:
      - template: "form.j2"
        for_each: "message"
        where:
          all:
            - name: "*Form"
        output: "components/{{ item.name.kebab_case }}/index.tsx"

4. Generate Code

# Generate code
wizard-codegen generate

# Preview changes without writing files
wizard-codegen --dry-run generate

# Verbose output for debugging
wizard-codegen --verbose generate

๐Ÿ“ฆ Installation

Prerequisites

Requirement Description Installation
Python 3.10+ Runtime python.org
uv Fast Python package manager See Quick Start or docs.astral.sh/uv
protoc Protocol Buffer Compiler See Quick Start or grpc.io
Git For fetching proto sources Usually pre-installed

Install via uv (Recommended)

uv tool install wizard-codegen

Upgrade

uv tool upgrade wizard-codegen

Install from Source (for development)

git clone <internal-repo-url>
cd wizard-codegen
make deps      # Install dependencies
make install   # Install CLI tool

Upgrade from Source

make upgrade

Dependencies

Core dependencies (managed in pyproject.toml):

Package Version Purpose
typer 0.20.1 CLI framework
pydantic 2.12.5 Configuration validation
jinja2 3.1.6 Template engine
rich 14.2.0 Terminal output
protobuf 6.33.2 Proto descriptor handling
pyyaml 6.0.3 YAML configuration

๐Ÿ–ฅ๏ธ CLI Usage

The CLI is built with Typer and provides rich, colorful output.

Commands

generate โ€” Generate code from protos

# Basic generation
wizard-codegen generate

# With custom config file
wizard-codegen --config path/to/codegen.yaml generate

# Dry run (preview without writing)
wizard-codegen --dry-run generate

# Verbose mode (detailed output)
wizard-codegen --verbose generate

# Combine flags
wizard-codegen --verbose --dry-run --config custom.yaml generate

list-protos โ€” List discovered proto files

wizard-codegen list-protos

Output:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚         Available proto schemas             โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Name            โ”‚ Path                      โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ user            โ”‚ user/user.proto           โ”‚
โ”‚ user_form       โ”‚ user/user_form.proto      โ”‚
โ”‚ order           โ”‚ order/order.proto         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

validate โ€” Validate templates and configuration

wizard-codegen validate

Validates:

  • Configuration file syntax
  • Proto file discovery
  • Template syntax and variable resolution
  • Descriptor set generation

Global Options

Option Short Description
--version -V Show version and exit
--config -c Path to codegen YAML configuration (default: wizard/codegen.yaml)
--verbose -v Enable detailed logging and context printing
--dry-run Preview actions without writing files
--local -l Use local proto.root instead of git source
--help Show help message

โš™๏ธ Configuration

Configuration is defined in YAML format. The default location is wizard/codegen.yaml.

Complete Example

proto:
  # Git source configuration (used by default)
  source:
    git: "git@github.com:YourOrg/proto-definitions.git"
    ref: "latest-tag"                # Default: auto-resolves to latest semver tag
    # fds: "build/descriptor.pb"     # Optional: use pre-built descriptor
    include_info: true               # Include source info in descriptors

  # Local path (used with --local flag for development)
  root: "../local-protos"

  # Cache directory for git clones
  cache_dir: ".cache/proto-common"

  # Include paths (relative to proto root)
  includes:
    - "{proto_root}/shared"
    - "{proto_root}/service/wizard"

  # File patterns to discover
  files:
    - "pages/**/*.proto"
    - "models/*.proto"

targets:
  typescript:
    templates: "wizard/templates/ts"
    out: "src/generated/ts"
    render:
      # Generate form components for *Form messages
      - template: "form.j2"
        for_each: "message"
        mode: "write-once"
        where:
          all:
            - name: "*Form"
          not:
            - file: "*/shared/design_system/*"
        output: "components/{{ item.name.kebab_case }}-form/index.tsx"

      # Generate context providers for *Context messages
      - template: "context.j2"
        for_each: "message"
        where:
          any:
            - name: "*Context"
          not:
            - file: "*/shared/design_system/*"
        output: "contexts/{{ item.name.kebab_case }}/index.tsx"

  swift:
    templates: "wizard/templates/swift"
    out: "ios/Generated"
    render:
      - template: "struct.j2"
        for_each: "message"
        output: "{{ item.name.pascal_case }}.swift"

  kotlin:
    templates: "wizard/templates/kotlin"
    out: "android/generated"
    render:
      - template: "data_class.j2"
        for_each: "message"
        output: "{{ item.name.pascal_case }}.kt"

hooks:
  root: "wizard"
  module: "hook_sample"   # Optional: custom filters module

Proto Configuration

Key Type Description
root string Local path to proto files (used with --local flag)
source.git string Git repository URL for proto source
source.ref string Git ref: tag, branch, commit SHA, or latest-tag (default: latest-tag)
source.fds string Path to pre-built FileDescriptorSet (optional)
source.include_info bool Include source info in protoc output (default: true)
cache_dir string Directory for caching git clones
includes list[string] Include paths for proto imports
files list[string] Glob patterns for proto file discovery

Special Ref: latest-tag

Use ref: "latest-tag" to automatically checkout the latest semver tag from the repository:

proto:
  source:
    git: "git@github.com:YourOrg/proto-definitions.git"
    ref: "latest-tag"  # Automatically resolves to latest semver tag (e.g., v2.1.0)

This feature:

  • Lists all tags in the repository
  • Filters for valid semver tags (e.g., v1.2.3, 1.0.0, v2.0.0-beta)
  • Prefers stable releases over prereleases (e.g., v1.0.0 > v1.0.0-beta)
  • Checks out the highest version according to semver ordering

Proto Source Resolution

You can configure both a local path (proto.root) and a git source (proto.source) simultaneously. This allows developers to easily switch between tagged releases and local development:

Default behavior (no flags):

  1. proto.source.git โ€” clone/fetch from git repository
  2. Falls back to proto.root โ€” if no git source is configured
  3. Error โ€” if neither is available

With --local flag:

  1. proto.root โ€” use local filesystem path
  2. Error โ€” if proto.root is not configured or doesn't exist

This design enables a common workflow:

  • CI/Production: Uses git source with latest-tag (the default ref)
  • Local Development: Use --local flag to test against local proto changes
# Use git source (default - fetches latest tag)
wizard-codegen generate

# Use local protos for development
wizard-codegen --local generate

Targets Configuration

Each target represents a language/output configuration.

Key Type Description
templates string Path to Jinja2 template directory
out string Output directory for generated files
render list List of render rules

Render Rules

Key Type Description
template string Template filename (relative to templates dir)
output string Output path pattern (Jinja2 template string)
for_each enum Iteration mode: file, message, enum, service
mode enum Write mode: overwrite, append, write-once
where object Filter predicates (optional)

Write Modes

Mode Description
overwrite Always replace existing files (default)
append Add content to end of existing files
write-once Only create if file doesn't exist

Where Clauses (Filtering)

Filter which items to generate for:

where:
  all:          # AND conditions (all must match)
    - name: "*Form"
    - package: "wizard.*"
  any:          # OR conditions (at least one must match)
    - name: "*Context"
    - name: "*Provider"
  not:          # Exclude matching items
    - file: "*/design_system/*"
    - name: "*Internal"
Predicate Fields
Field Description Example
name Message/enum/service name "*Form", "User*", "Order"
package Proto package name "wizard.*", "com.example.*"
file Proto file path "*/shared/*", "user/*.proto"
full_name Fully qualified name ".wizard.UserForm"
option.equals Match proto options { key: "deprecated", value: true }

Patterns support:

  • Glob patterns: *, ?, [abc]
  • Regex patterns: Any valid Python regex

Hooks Configuration

Key Type Description
root string Root directory for hooks module (default: "wizard")
module string Python module name with register() function

๐Ÿ“ Template Authoring

Templates use Jinja2 syntax with custom extensions.

Template Context

When a template is rendered, it receives a rich context:

{
    "proto_root": "/path/to/protos",

    # Current item (when using for_each)
    "item": {
        "name": Name("UserForm"),          # Name object with case transformations
        "full_name": ".wizard.UserForm",
        "file": "user/user_form.proto",
        "package": "wizard",
        "fields": [...],                   # List of field objects
        "nested_messages": [...],          # Nested message definitions
        "nested_enums": [...],             # Nested enum definitions
    },

    # All files (for cross-referencing)
    "files": [...],

    # Indexes for quick lookup
    "message": { ".package.MessageName": {...}, ... },
    "enum": { ".package.EnumName": {...}, ... },
    "service": { ".package.ServiceName": {...}, ... },
    "types": { ".package.TypeName": {...}, ... },
}

File Object

{
    "name": Name("user_form.proto"),
    "basename": "user_form",
    "package": "wizard",
    "package_path": "wizard",
    "imports": ["common/timestamp.proto"],
    "messages": [...],
    "enums": [...],
    "services": [...],
    "options": <FileOptions>,
}

Message Object

{
    "name": Name("UserForm"),
    "full_name": ".wizard.UserForm",
    "file": "user/user_form.proto",
    "package": "wizard",
    "fields": [
        {
            "name": Name("user_name"),
            "number": 1,
            "label": 1,               # 1=optional, 2=required, 3=repeated
            "type": 9,                # Proto type number (9=string)
            "type_name": "",          # For message/enum refs: ".package.Type"
            "json_name": "userName",
            "field": <FieldDescriptor>,
        },
        ...
    ],
    "nested_messages": [...],
    "nested_enums": [...],
}

Enum Object

{
    "name": Name("Status"),
    "full_name": ".wizard.Status",
    "file": "common/status.proto",
    "package": "wizard",
    "enum_values": [
        {"name": "STATUS_UNKNOWN", "number": 0},
        {"name": "STATUS_ACTIVE", "number": 1},
        ...
    ],
}

Service Object

{
    "name": Name("UserService"),
    "file": "user/user_service.proto",
    "package": "wizard",
    "methods": [
        {
            "name": Name("GetUser"),
            "input_type": ".wizard.GetUserRequest",
            "output_type": ".wizard.User",
            "client_streaming": False,
            "server_streaming": False,
            "options": <MethodOptions>,
        },
        ...
    ],
}

Name Transformations

Every name in the context is wrapped in a Name object that provides automatic case transformations:

{{ item.name.raw }}              โ†’  UserFormRequest
{{ item.name.snake_case }}       โ†’  user_form_request
{{ item.name.kebab_case }}       โ†’  user-form-request
{{ item.name.pascal_case }}      โ†’  UserFormRequest
{{ item.name.camel_case }}       โ†’  userFormRequest
{{ item.name.macro_case }}       โ†’  USERFORMREQUEST
{{ item.name.macro_snake_case }} โ†’  USER_FORM_REQUEST

Use in output paths:

output: "{{ item.name.kebab_case }}/index.tsx"
# Generates: user-form-request/index.tsx

Use in templates:

export interface {{ item.name.pascal_case }} {
{% for field in item.fields %}
  {{ field.name.camel_case }}: {{ field | ts_type }};
{% endfor %}
}

Built-in Filters

The following filters are available in all templates:

Common Filters

Filter Description Example
replace String replacement {{ name | replace("_", "-") }}

TypeScript Filters (via hooks)

Filter Description Example
ts_type Proto to TS type {{ field | ts_type }} โ†’ string, number, User
ts_type_optional TS type with undefined {{ field | ts_type_optional }} โ†’ string | undefined
is_ts_optional Check if optional {% if field | is_ts_optional %}?{% endif %}
is_repeated Check if array {% if field | is_repeated %}[]{% endif %}

Swift Filters (via hooks)

Filter Description Example
swift_type Proto to Swift type {{ field | swift_type }} โ†’ String, Int32, User
swift_default Swift default value {{ field | swift_default }} โ†’ "", 0, []

Kotlin Filters (via hooks)

Filter Description Example
kotlin_type Proto to Kotlin type {{ field | kotlin_type }} โ†’ String, Int, User
kotlin_default Kotlin default value {{ field | kotlin_default }} โ†’ "", 0, emptyList()

Go Filters (via hooks)

Filter Description Example
go_type Proto to Go type {{ field | go_type }} โ†’ string, int32, *User
go_zero Go zero value {{ field | go_zero }} โ†’ "", 0, nil
go_json_tag Go JSON struct tag {{ field | go_json_tag }} โ†’ `json:"userName,omitempty"`

Template Example

// {{ item.name.pascal_case }}.swift
// Generated from: {{ item.file }}
// Package: {{ item.package }}

import Foundation

/// {{ item.name.pascal_case }} - Auto-generated from protobuf.
public struct {{ item.name.pascal_case }}: Codable, Equatable, Sendable {
{% for field in item.fields %}
    /// {{ field.name.raw }} - Proto type: {{ field.type }}
    public var {{ field.name.camel_case }}: {{ field | swift_type }}
{% endfor %}

    public init(
{% for field in item.fields %}
        {{ field.name.camel_case }}: {{ field | swift_type }} = {{ field | swift_default }}{% if not loop.last %},{% endif %}
{% endfor %}
    ) {
{% for field in item.fields %}
        self.{{ field.name.camel_case }} = {{ field.name.camel_case }}
{% endfor %}
    }
}

๐Ÿ”Œ Custom Hooks

Extend wizard-codegen with custom Jinja2 filters and helpers.

Creating a Hooks Module

  1. Create a Python file in your hooks root (default: wizard/):
# wizard/my_hooks.py

from jinja2 import Environment
from core import CodegenConfig

def custom_filter(value):
    """Your custom filter logic."""
    return str(value).upper()

def format_field_doc(field):
    """Generate documentation for a field."""
    return f"@param {field['name'].camel_case} - {field.get('json_name', '')}"

def register(env: Environment, *, target: str, config: CodegenConfig) -> None:
    """
    Register custom filters and globals.

    Args:
        env: Jinja2 environment to extend
        target: Current target name (e.g., "typescript", "swift")
        config: Full codegen configuration
    """
    # Register filters
    env.filters["uppercase"] = custom_filter
    env.filters["field_doc"] = format_field_doc

    # Register globals (optional)
    env.globals["TARGET"] = target

    # Target-specific filters
    if target == "typescript":
        env.filters["ts_custom"] = lambda x: f"TS_{x}"
  1. Reference it in your configuration:
hooks:
  root: "wizard"
  module: "my_hooks"   # Loads wizard/my_hooks.py
  1. Use in templates:
{{ item.name.raw | uppercase }}
{{ field | field_doc }}
Current target: {{ TARGET }}

Type Mapping Example

Here's a complete example of type mapping hooks:

# wizard/type_mappings.py

from jinja2 import Environment

# Proto type constants
TYPE_STRING = 9
TYPE_BOOL = 8
TYPE_INT32 = 5
TYPE_INT64 = 3

TS_TYPES = {
    TYPE_STRING: "string",
    TYPE_BOOL: "boolean",
    TYPE_INT32: "number",
    TYPE_INT64: "bigint",
}

SWIFT_TYPES = {
    TYPE_STRING: "String",
    TYPE_BOOL: "Bool",
    TYPE_INT32: "Int32",
    TYPE_INT64: "Int64",
}

def ts_type(field):
    if field.get("type_name"):
        return field["type_name"].split(".")[-1]
    return TS_TYPES.get(field.get("type"), "unknown")

def swift_type(field):
    if field.get("type_name"):
        return field["type_name"].split(".")[-1]
    return SWIFT_TYPES.get(field.get("type"), "Any")

def register(env: Environment, *, target: str, config) -> None:
    if target == "typescript":
        env.filters["lang_type"] = ts_type
    elif target == "swift":
        env.filters["lang_type"] = swift_type

๐Ÿ—๏ธ Architecture

wizard-codegen/
โ”œโ”€โ”€ cli/                   # CLI package
โ”‚   โ”œโ”€โ”€ __init__.py        # Re-exports for backwards compatibility
โ”‚   โ””โ”€โ”€ main.py            # CLI entry point (Typer app)
โ”œโ”€โ”€ core/                  # Core business logic
โ”‚   โ”œโ”€โ”€ config.py          # Configuration models (Pydantic)
โ”‚   โ”œโ”€โ”€ context_builder.py # Proto โ†’ Jinja context transformation
โ”‚   โ”œโ”€โ”€ filter.py          # Where clause filtering
โ”‚   โ”œโ”€โ”€ renderer.py        # Jinja2 template rendering
โ”‚   โ””โ”€โ”€ writer.py          # File writing with modes
โ”œโ”€โ”€ proto/                 # Protocol Buffer handling
โ”‚   โ”œโ”€โ”€ discover.py        # Proto file discovery
โ”‚   โ”œโ”€โ”€ fds_loader.py      # Descriptor set loading
โ”‚   โ”œโ”€โ”€ proto_source.py    # Git checkout handling
โ”‚   โ””โ”€โ”€ protoc_runner.py   # protoc execution
โ”œโ”€โ”€ hooks/                 # Plugin system
โ”‚   โ””โ”€โ”€ hooks.py           # Hook loading and protocol
โ”œโ”€โ”€ utils/                 # Utilities
โ”‚   โ””โ”€โ”€ name.py            # Name transformations
โ””โ”€โ”€ wizard/                # Example configuration
    โ”œโ”€โ”€ codegen.yaml       # Sample config
    โ”œโ”€โ”€ hook_sample.py     # Sample hooks
    โ””โ”€โ”€ templates/         # Sample templates

Pipeline Flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                         CLI (cli/main.py)                            โ”‚
โ”‚                    parse args, load config                           โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                โ”‚
                                โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    Proto Resolution (proto/)                         โ”‚
โ”‚     discover files โ†’ resolve git source โ†’ build descriptor set       โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                โ”‚
                                โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                  Context Building (core/context_builder.py)          โ”‚
โ”‚          FileDescriptorSet โ†’ Jinja-friendly dictionaries             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                โ”‚
                                โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                     Rendering (core/renderer.py)                     โ”‚
โ”‚    for each target โ†’ for each rule โ†’ filter items โ†’ render template  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                โ”‚
                                โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                      Writing (core/writer.py)                        โ”‚
โ”‚           apply write mode โ†’ hash comparison โ†’ write files           โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

๐Ÿ‘ฉโ€๐Ÿ’ป Developer Guide

Project Structure

wizard-codegen/
โ”œโ”€โ”€ cli/                # CLI package (main entry point)
โ”œโ”€โ”€ core/               # Core modules
โ”œโ”€โ”€ proto/              # Proto handling
โ”œโ”€โ”€ hooks/              # Plugin system
โ”œโ”€โ”€ utils/              # Utilities
โ”œโ”€โ”€ wizard/             # Example config and templates
โ”œโ”€โ”€ tests/              # Test suite
โ”‚   โ”œโ”€โ”€ fixtures/       # Test fixtures
โ”‚   โ”‚   โ”œโ”€โ”€ protos/     # Sample proto files
โ”‚   โ”‚   โ”œโ”€โ”€ templates/  # Test templates
โ”‚   โ”‚   โ”œโ”€โ”€ hooks/      # Test hooks
โ”‚   โ”‚   โ””โ”€โ”€ expected_outputs/  # Golden files
โ”‚   โ”œโ”€โ”€ test_*.py       # Unit tests
โ”‚   โ””โ”€โ”€ test_e2e.py     # End-to-end tests
โ”œโ”€โ”€ Makefile            # Build automation
โ”œโ”€โ”€ pyproject.toml      # Project config & dependencies
โ””โ”€โ”€ uv.lock             # Locked dependencies

Running Tests

# Run all tests
make test

# Run with coverage
make coverage

# Run specific test file
make test PYTEST_ARGS=tests/test_context_builder.py

# Run specific test
make test PYTEST_ARGS="tests/test_e2e.py::TestFullPipelineGeneration -v"

Code Coverage

codecov

Test Categories

Category Description Files
Unit Tests Test individual functions test_config.py, test_filter.py, test_name.py
Integration Tests Test module interactions test_context_builder.py, test_renderer.py
E2E Tests Full pipeline tests test_e2e.py
Fixture Tests Golden file comparisons test_fixtures.py

Adding New Target Languages

  1. Create templates in wizard/templates/<language>/:
{# wizard/templates/rust/struct.j2 #}
// {{ item.name.pascal_case }}.rs

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct {{ item.name.pascal_case }} {
{% for field in item.fields %}
    pub {{ field.name.snake_case }}: {{ field | rust_type }},
{% endfor %}
}
  1. Add type mapping hooks (optional):
# wizard/rust_hooks.py

RUST_TYPES = {
    9: "String",
    8: "bool",
    5: "i32",
    3: "i64",
}

def rust_type(field):
    if field.get("type_name"):
        return field["type_name"].split(".")[-1]
    return RUST_TYPES.get(field.get("type"), "Unknown")

def register(env, *, target, config):
    if target == "rust":
        env.filters["rust_type"] = rust_type
  1. Add target configuration:
targets:
  rust:
    templates: "wizard/templates/rust"
    out: "src/generated"
    render:
      - template: "struct.j2"
        for_each: "message"
        output: "{{ item.name.snake_case }}.rs"

Contributing

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/my-feature
  3. Write tests for new functionality
  4. Run the test suite: make test
  5. Submit a pull request

Code Style

  • Python 3.10+ type hints
  • Black formatting (88 char line length)
  • Docstrings for public functions
  • Comprehensive tests for new features

๐Ÿ“š Examples

TypeScript React Form Component

{# templates/ts/form.j2 #}
import React, { useState } from 'react';

export interface {{ item.name.pascal_case }}Data {
{% for field in item.fields %}
  {{ field.name.camel_case }}{% if field | is_ts_optional %}?{% endif %}: {{ field | ts_type }};
{% endfor %}
}

export const {{ item.name.pascal_case }}: React.FC = () => {
{% for field in item.fields %}
  const [{{ field.name.camel_case }}, set{{ field.name.pascal_case }}] = useState<{{ field | ts_type }}>();
{% endfor %}

  return (
    <form>
{% for field in item.fields %}
      <input
        name="{{ field.name.snake_case }}"
        value={ {{ field.name.camel_case }} ?? ''}
        onChange={(e) => set{{ field.name.pascal_case }}(e.target.value)}
      />
{% endfor %}
    </form>
  );
};

Kotlin Data Class

{# templates/kotlin/data_class.j2 #}
package {{ item.package }}

import kotlinx.serialization.Serializable

@Serializable
data class {{ item.name.pascal_case }}(
{% for field in item.fields %}
    val {{ field.name.camel_case }}: {{ field | kotlin_type }} = {{ field | kotlin_default }}{% if not loop.last %},{% endif %}
{% endfor %}
)

Go Struct with JSON Tags

{# templates/go/struct.j2 #}
package {{ item.package | replace(".", "_") }}

// {{ item.name.pascal_case }} - Generated from {{ item.file }}
type {{ item.name.pascal_case }} struct {
{% for field in item.fields %}
	{{ field.name.pascal_case }} {{ field | go_type }} {{ field | go_json_tag }}
{% endfor %}
}

๐Ÿ”ง Troubleshooting

Common Issues

"protoc is not available in PATH"

# macOS
brew install protobuf

# Ubuntu/Debian
apt install -y protobuf-compiler

# Verify installation
protoc --version

"Config error: proto.root not found"

Either specify a local proto.root or configure proto.source.git:

proto:
  root: "../my-protos"  # Local path
  # OR
  source:
    git: "git@github.com:Org/protos.git"
    ref: "main"

"Failed to import hooks module"

Ensure your hooks module:

  1. Is in the correct directory (hooks.root config)
  2. Has a register(env, *, target, config) function
  3. Has no import errors
# Test manually
uv run python -c "import wizard.my_hooks"

"Template variable undefined"

  • Use --verbose to inspect the context
  • Check your for_each setting matches the expected context
  • Verify field names in the proto file

Made with ๐Ÿง™ magic

Report Bug ยท Request Feature

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

wizard_codegen-0.1.8.tar.gz (32.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

wizard_codegen-0.1.8-py3-none-any.whl (27.3 kB view details)

Uploaded Python 3

File details

Details for the file wizard_codegen-0.1.8.tar.gz.

File metadata

  • Download URL: wizard_codegen-0.1.8.tar.gz
  • Upload date:
  • Size: 32.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.8.22

File hashes

Hashes for wizard_codegen-0.1.8.tar.gz
Algorithm Hash digest
SHA256 c899c49e7735bbf82db208643039537d7a90bc461083379a9fd35f12758747b7
MD5 3861513a4a75d3b1b05dddc268c5ab8e
BLAKE2b-256 a8e6b3b3324d3ce17e98c962d0989991aa8a55db9641bb466e01a57102e10766

See more details on using hashes here.

File details

Details for the file wizard_codegen-0.1.8-py3-none-any.whl.

File metadata

File hashes

Hashes for wizard_codegen-0.1.8-py3-none-any.whl
Algorithm Hash digest
SHA256 f4090e4815b5335a9522614bf82cad27186c551d0b167542673ce49a46776157
MD5 431c29096ec2c83c677ff5958524ac9c
BLAKE2b-256 655c20290b19b14b017c90b13c6a0719b92c4b202765d332d822fd03e48795fe

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page