Taking Full Control: Your Code, Your Way

firestone provides intelligent, opinionated code generation. It aims to give you robust, functional output (OpenAPI specs, Click CLIs, Streamlit UIs) out of the box. However, every project has its unique needs: specific coding standards, custom features, or integration with bespoke tooling.

This is where custom templates become your most powerful tool.

firestone uses the popular Jinja2 templating engine for all its code generation. By providing your own Jinja2 template file, you can completely take over the generation process, allowing you to fine-tune every single line of output.

Why Use Custom Templates?

Custom templates are for when you need to go beyond firestone's default behavior. Here are some common scenarios:

  • Adhering to Specific Coding Standards: Your team might have strict linting rules, naming conventions, or documentation standards that differ from the defaults.
  • Adding Custom Features: Injecting custom logic, helper functions, or integration points into the generated code.
  • Generating Unsupported Formats: If firestone doesn't natively generate a specific output (e.g., a client for a very niche language, or a custom config file), you can write a template for it.
  • Modifying Generated Structure: Changing the directory layout of generated modules or the structure of specific code blocks.
  • Advanced API Customization: Adding OpenAPI extensions, bespoke security definitions, or custom server configuration that isn't directly exposed in the firestone resource blueprint.

How firestone Uses Templates

When you run a firestone generate command, the process looks like this:

  1. firestone reads your resource blueprint(s).
  2. It parses and validates the data, creating a rich Jinja2 context (a Python dictionary containing all your resource data).
  3. It then feeds this context into a Jinja2 template (either firestone's default or your custom one).
  4. The template engine processes the template file, using the data from the context.
  5. The result is the generated code (OpenAPI YAML, Python CLI, etc.).

Your job, when writing a custom template, is to intelligently consume this Jinja2 context and produce the desired output.

The --template Option

You provide your custom template file using the --template, -T option with any of the firestone generate commands.

firestone generate \
  --resources my_api.yaml \
  --title "My API" --description "My API description" --version "1.0.0" \
  openapi --output custom_openapi.yaml --template ./my_custom_openapi.yaml.jinja

The path to your template can be absolute or relative to your current working directory.

Anatomy of a Custom Template

A Jinja2 template is essentially a text file (e.g., .jinja, .jinja2, .py.jinja) that contains special placeholders and logic blocks.

Accessing Resource Data

The entire firestone resource data (and more) is available in the template context. You can access it using dot notation:

# Accessing the API title
{{ project_title }}

# Accessing resource data (if generating a multi-resource file)
{% for resource in resources %}
  Kind: {{ resource.kind }}
  API Version: {{ resource.apiVersion }}
{% endfor %}

firestone injects a few key variables into the context:

  • resources: A list of all processed resource blueprints (each as a dictionary).
  • project_title, project_description, project_version: The overall API metadata provided via CLI options.
  • config: Internal configuration objects.

Looping Through Data

You'll frequently iterate over lists of resources, properties, or methods:

{% for prop_name, prop_schema in resource.schema.items.properties.items() %}
  - {{ prop_name }}: {{ prop_schema.type }}
{% endfor %}

Conditional Logic

You can use if/else statements to generate different code based on your resource's configuration:

{% if resource.versionInPath %}
  # Generate versioned path logic
{% else %}
  # Generate unversioned path logic
{% endif %}

Example: Adding a Custom OpenAPI Extension

Let's say you want to add a custom x-internal-id extension to every OpenAPI path generated by firestone.

my_custom_openapi.yaml.jinja:

# Start with default OpenAPI header, then inject custom field
openapi: 3.0.0
info:
  title: {{ project_title }}
  version: {{ project_version }}
  description: {{ project_description }}
  x-generated-by: "firestone-{{ firestone_version }}" # Custom info extension

paths:
  {% for resource in resources %}
  {% for method in resource.methods.resource %}
  /{{ resource.kind }}:
    {{ method }}:
      summary: {{ resource.descriptions.resource[method] }}
      x-internal-id: "{{ resource.kind | upper }}_{{ method | upper }}" # Your custom field!
      # ... rest of the path definition ...
  {% endfor %}
  {% endfor %}
# ... rest of your OpenAPI structure

Best Practices for Custom Templating

1. Start from Existing Templates

firestone's default templates are a great starting point. Find them in the firestone/spec/templates directory of the firestone source code. Copy the relevant template and modify it.

2. Keep it Modular

For complex templates, break them into smaller, reusable blocks using Jinja2's {% include %} or {% extends %}.

3. Test Thoroughly

Custom templates can introduce bugs. Generate output with various resource configurations and verify that the output is correct and valid.

4. Understand the Jinja2 Context

The more you understand the data structure firestone provides to the template, the more powerful your customizations will be. Use {{ pp.pprint(context) }} within a template for debugging (you might need to enable a pprint filter in your Jinja2 environment if it's not present).


Next Steps

You've learned how to completely control firestone's output. Now, let's explore another advanced topic: how to manage and reuse your schema definitions.