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
Runtime Name Filter Generate only specific items from CLI: wizard-codegen generate SampleProtoPage
Annotation Filtering Filter messages by custom proto annotations/options (e.g., has_option: "*.page")
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, microcase
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
Options Extraction Access message, field, and enum options in templates
Sample Data Loading Auto-load .samples.json files co-located with protos into template context (item.samples, item.default_sample)
Sample Validation validate command checks .samples.json field names and types against proto schema recursively
Context Explorer Interactive TUI and REPL for exploring the Jinja2 template context (context-explorer command)

๐Ÿš€ 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 (all items)
wizard-codegen generate

# Generate only specific items by name
wizard-codegen generate SampleProtoPage

# Generate multiple specific items
wizard-codegen generate SampleProtoPage UserFormPage

# Use glob patterns to match item names
wizard-codegen generate "*Page"

# 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 and name filter
wizard-codegen --verbose --dry-run generate SampleProtoPage

The optional positional NAMES arguments filter which items are generated at runtime. This works alongside any where clauses in your config โ€” the config filters are applied first, then the name filter narrows the results further. Glob patterns (*Page, Sample*) and regex patterns are supported. When no names are provided, all matching items are generated as usual.

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 }
has_option Check if option/annotation is present "*.page", "annotations.wizard.v1.page"

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
        "samples": [...],  # Loaded from co-located .samples.json (empty list if none)
        "default_sample": {...},  # Resolved default sample (None if no samples)
    },

    # 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"),
    "full_name": "user_form.proto",
    "package": "wizard",
    "package_path": "wizard",
    "imports": ["common/timestamp.proto"],
    "messages": [...],
    "enums": [...],
    "services": [...],
    "options": {
        # Extracted file options (standard and extensions)
        # e.g., {"java_package": "com.example", "go_package": "example/pb"}
    },
}

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 >,
        "options": {},  # Extracted field options
},
...
],
"nested_messages": [...],
"nested_enums": [...],
"options": {
    # Extracted message options (standard and extensions)
    # Example: {"annotations.wizard.v1.page": True}
},
}

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},
        ...
    ],
    "options": {},  # Extracted enum options
}

Service Object

{
    "name": Name("UserService"),
    "full_name": ".wizard.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": {},  # Extracted method options
        },
        ...
    ],
    "options": {},  # Extracted service options
}

Sample Data (.samples.json)

Proto files can have a co-located .samples.json file that provides example data. The file mirrors the proto filename:

service/wizard/pages/sample/v1/
  sample.proto              # the page definition
  sample.samples.json       # sample data

When found, the samples are loaded into the template context on the matching message item. Templates can access them via item.samples (always a list) and item.default_sample (the resolved default, or None).

Single sample

{
  "context": {
    "header": { "title": "Welcome" },
    "primaryButtonText": "Continue"
  },
  "form": { "inputFieldValue": "" }
}

Multiple samples

Use an array. Name one "default" to mark it as the default; otherwise the first entry is used.

[
  { "name": "default", "context": { "header": { "title": "Welcome" } }, "form": {} },
  { "name": "returning_user", "context": { "header": { "title": "Welcome back" } }, "form": {} }
]

Using samples in templates

{% if item.default_sample %}
var defaultContext = {{ item.default_sample.context | tojson }};
{% endif %}

{% for sample in item.samples %}
// Sample: {{ sample.name | default("unnamed") }}
{% endfor %}

Validation

The wizard-codegen validate command checks all found .samples.json files:

  • Invalid JSON is reported with parse details
  • Empty arrays are rejected
  • Every JSON key is checked against the message's proto field json_name values
  • Value types are checked (string, number, bool, object, array) against proto field types
  • Message-type fields are validated recursively, including externally referenced messages
  • Repeated fields must be arrays with correctly typed elements

Samples are optional โ€” the generate command works with or without them, and validate only checks files that exist (no warning for missing samples).

Nested message propagation

Samples are automatically propagated from a top-level message to its nested message types. For example, given a wizard page with nested Context and Form messages and a sample containing {"context": {...}, "form": {...}}, the Context message receives the context sub-object as its sample, and Form receives the form sub-object. The matching is based on the camelCase type name of the nested message. This means item.samples and item.default_sample are available in templates for both the parent and its nested message types.

Context Explorer

The context-explorer command helps template authors discover what data is available in the Jinja2 template context. It supports two modes.

Interactive TUI (default)

The default mode launches a full-screen interactive terminal UI with an expandable/collapsible tree on the left and a detail inspector panel on the right:

# Launch TUI
wizard-codegen context-explorer

# Auto-expand to a specific path
wizard-codegen context-explorer "message[.service.wizard.pages.sample.v1.SampleProtoPage]"
+-------------------------------+-------------------------------+
| Context Explorer              |                               |
+-------------------------------+-------------------------------+
| Context                       | Path: message[.pkg.Foo].name  |
| v proto_root: "/path/..."     |                               |
| v files (3 items)             | Type: Name                    |
|   > [0] sample_page.proto     |                               |
|   > [1] other.proto           | raw         SampleProtoPage   |
| v message (5 keys)            | snake_case  sample_proto_page |
|   v .pkg.SampleProtoPage      | kebab_case  sample-proto-page |
|     > name: SampleProtoPage   | pascal_case SampleProtoPage   |
|     > fields (3 items)        | camel_case  sampleProtoPage   |
|     > nested_messages (2)     | macro_case  SAMPLEPROTOPAGE   |
|     > options (1 keys)        | macro_snake SAMPLE_PROTO_PAGE |
|   > .pkg.Other                | micro_case  sampleprotopage   |
| > enum (2 keys)               |                               |
| > service (1 keys)            |                               |
+-------------------------------+-------------------------------+
| q Quit | space Toggle | arrows Navigate                      |
+-------------------------------+-------------------------------+

Nodes start collapsed and are lazily loaded on expand โ€” keeping startup fast even for large contexts. Use arrow keys to navigate, space/enter to expand/collapse, / to focus the filter input, and q to quit.

The filter input at the top lets you narrow down dict keys across all top-level sections (message, enum, service, types). Type a substring (e.g. sample) to hide keys that don't match, and press Escape to clear the filter. This is especially useful for hiding built-in google protobuf definitions.

REPL mode

Launch with --repl / -r for step-by-step text navigation with tab completion:

wizard-codegen context-explorer -r
Context Explorer - type help for commands

context:/> message
  Key                                                    Type              Preview
  .service.wizard.pages.sample.v1.SampleProtoPage        dict (8 keys)     name, full_name, fields, ...

context:/message> [.service.wizard.pages.sample.v1.SampleProtoPage]
  Key                Type              Preview
  name               Name              SampleProtoPage
  full_name          str               .service.wizard.pages.sample.v1.SampleProtoPage
  fields             list (0 items)
  nested_messages    list (2 items)
  samples            list (2 items)
  default_sample     dict (3 keys)     name, context, form

context:/message[.service.wizard.pages.sample.v1.SampleProtoPage]> name
  SampleProtoPage (raw=SampleProtoPage, snake=sample_proto_page, kebab=sample-proto-page,
  pascal=SampleProtoPage, camel=sampleProtoPage, macro=SAMPLEPROTOPAGE,
  macro_snake=SAMPLE_PROTO_PAGE, micro=sampleprotopage)

REPL commands:

Command Description
<path> Navigate to path (dot-separated, [brackets] for dict/list)
keys Show available keys at current level
.. Go up one level
/ Go to root
tree [depth] Show tree from current position (optional depth limit)
help Show help
quit Exit the explorer

Path syntax uses dots for plain keys and brackets for keys containing dots or list indices: message[.pkg.Foo].fields[0].name.snake_case.

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.micro_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.3.0.tar.gz (56.1 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.3.0-py3-none-any.whl (51.2 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for wizard_codegen-0.3.0.tar.gz
Algorithm Hash digest
SHA256 f85b2092d9a8b173dbbbd846046f97e741ca565d62408de6d28fc329548b50b3
MD5 43b074939086bf691528f7f112e8c7c1
BLAKE2b-256 786d61838dcc4e740b48137da9499f2e9c9b78158e8b871013d24c92aea78721

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for wizard_codegen-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ec558f0d08fce205190e8f3bd4cd6dc33cdb685a665220d5221c43fc3168b138
MD5 640f191cf2ee6b552500c5c74d7e642e
BLAKE2b-256 eeb5e84dacc29f88f64004e182844de778df2cffef9fb7f4c142404c77561e32

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