Code Generation



Code generation is central to hof. hof’s ad-hoc code generation feature enables developers to generate files for any programming language or framework by rendering templates with data. This page introduces code generation through hof.

With hof gen, you can combine CUE, Yaml, or JSON arguments with templates to generate files and directories of code automatically. Through flags and configurations, you have complete control over the file generation process and the final content created.

hof gen <cue, data, config>... [--flags...]

hof’s template-based code generation has two approaches: ad-hoc and a more sophisticated configuration or module-based method.

  1. hof gen -T is code gen from flags
  2. hof gen -G is code gen from config

In this section, we will explore the ad-hoc method which used the -T flag. The first example is a step-by-step guide on creating a generator using CUE based configuration. The getting-started/create section will introduce hof create, which enables running generators directly from git repositories.

Each of these methods utilizes the same core concepts and processes, and they are suitable for different use cases:

  • hof gen -T is ideal for simpler scenarios when just a few files are involved.
  • hof gen -G is designed for large-scale code generation and reusable modules.
  • hof create is intended for one-time setup and application bootstrapping.

Code generation topics are discussed in a dedicated section.

Data + Templates

By running the command hof gen interlude.json -T interlude.template, users can perform ad-hoc template rendering to combine any data source with any template.

interlude.json

{
  "name": "Dougie",
  "question": "Did you see all them turtles?",
  "answer": "They went all the way down!"
}

interlude.template

Achelles: {{ .question }}
{{ .name }}: {{ .answer }}

> terminal

$ hof gen interlude.json -T interlude.template

Achelles: Did you see all them turtles?
Dougie: They went all the way down!

hof’s templates are built on Go’s text/template package with extra helpers added.


By using a hyphen symbol “-”, you can stream any data into hof gen. This feature can be helpful when you need to render an API response or process the output of a command.

> terminal

# use '-' to send output from another program to hof
$ curl api.com  | hof gen - -T template.txt

# intermix piped input with other entrypoints
$ cat data.json | hof gen - schema.cue -T template.txt

# set the data format when needed (cue help filetypes)
$ cat data.yaml | hof gen yaml: - schema.cue -T template.txt

Writing to file

Use = (equal) to write to a file by name.

> terminal

$ hof gen data.cue schema.cue -T template.txt=output.txt

If you want to write all outputs to a directory, use the -O flag. Files will have the same name as the template if not set individually.

> terminal

$ hof gen data.cue schema.cue -T template.txt -O out/

You can set the filename from data by using an inline template as the output name. Make sure you “wrap it in quotes”.

> terminal

$ hof gen data.cue schema.cue -T template.txt="{{ .name }}.txt"

You can combine these options to control the output directory and the filename.

> terminal

$ hof gen data.cue schema.cue \
  -O out \
  -T template.txt="{{ .name }}.txt"

Write Data Files

Omit the template path at the beginning and hof will infer the data format from the output file extension

> terminal

# full value to a single data file
$ hof gen data.cue schema.cue -T =data.yaml

# convert between data formats
$ hof gen data.json -T =data.yaml

# data file per item in iterable value
$ hof gen data.cue schema.cue \
  -O out \
  -T :items="[]{{ .name }}.json"

Multiple Templates

You can use the -T flag multiple times. Each is independent and can have different options for the data and schemas from the CUE entrypoints. (we’ll see this below)

> terminal

$ hof gen data.cue schema.cue -T template.txt -T =debug.yaml -O out/

Watching for Changes

Use the -w/--watch flag to watch for changes and re-render output. Think of the -w/--watch flag as a live-reload option that monitors your code and automatically re-renders your output when changes are detected.

> terminal

$ hof gen data.cue schema.cue -T template.txt -T debug.yaml -O out/ --watch

There are additional watch flags available if automatic detection misses files.

On Using CUE


hof’s inputs are cue’s inputs, or “CUE entry points”.

The inputs hold CUE values, which can be intermixed with your data to apply schemas, enrich the data, or transform before rendering. When running commands, the CUE entrypoints are combined into a single CUE value. If you want to place data files in a specific path, use the @path.to.location syntax.

> terminal

$ hof gen data.json@inputs.data schema.cue -T template.txt -T =debug.yaml -O out/

The final data passed to a template must be concrete or fully specified. This means the value needs to be like JSON data before template rendering accepts them. As you will see, hof provides flexibility and control for how the CUE values are selected, combined, and mapped to templates.

We keep parity with cue, so tooling from the broader ecosystem still works on our inputs and reduces context-switching costs. With hof, you can take advantage of all the possibilities and power of CUE for selecting, combining, and mapping the values to templates for rendering.

You can safely use all the possibilities and power of CUE here.

Setup for Examples


We will be using the following inputs in the examples below. We define a schema and write our types as data values in CUE.

Schema & Data

schema.cue

package example

// here we are applying a schema to our input data
// note how the label is the same in both files
Input: #Input

// This is your input schema
#Input: {
	// this is a CUE pattern to apply #Type to every key
	[key=string]: #Type & {
		// here we are enriching the input, mapping key -> Name
		Name: key
	}
}

// Schema for a Type
#Type: {
	// Name to use in target languages
	Name: string

	// This is a CUE pattern for a struct of structs
	// you can set nested fields based on the key name in [key=string]
	Fields: [field=string]: #Field & {Name: field}

	// Enum of relation types with the key being the Name of the other side
	Relations: [other=string]: "BelongsTo" | "HasOne" | "HasMany" | "ManyToMany"
}

// Schema for a Field
#Field: {
	// Name to use in target languages
	Name: string
	// the type as a string, for flexibility
	Type: string
}

// note, if you remove "Input" from all of the files
// this will have the same effect, but less control at the top-level
// Top-level schema are helpful when you don't control the input data format
// [key=string]: #Type & { Name: key }

data.cue

package example

// This is our input data, written as CUE
// The schema will be applied to validate
// the inputs and enrich the data model

Input: {
	User: {
		Fields: {
			id: Type:       "int"
			admin: Type:    "bool"
			username: Type: "string"
			email: Type:    "string"
		}
		Relations: {
			Profile: "HasOne"
			Post:    "HasMany"
		}
	}

	Profile: {
		Fields: {
			displayName: Type: "string"
			status: Type:      "string"
			about: Type:       "string"
		}
		Relations: User: "BelongsTo"
	}

	Post: {
		Fields: {
			title: Type:  "string"
			body: Type:   "string"
			public: Type: "bool"
		}
		Relations: User: "BelongsTo"
	}
}

We can use cue to see what the full data looks like

$ cue export data.cue schema.cue

> terminal

$ cue export schema.cue data.cue

{
    "Input": {
        "User": {
            "Name": "User",
            "Fields": {
                "id": {
                    "Name": "id",
                    "Type": "int"
                },
                "admin": {
                    "Name": "admin",
                    "Type": "bool"
                },
                "username": {
                    "Name": "username",
                    "Type": "string"
                },
                "email": {
                    "Name": "email",
                    "Type": "string"
                }
            },
            "Relations": {
                "Profile": "HasOne",
                "Post": "HasMany"
            }
        },
        "Profile": {
            "Name": "Profile",
            "Fields": {
                "displayName": {
                    "Name": "displayName",
                    "Type": "string"
                },
                "status": {
                    "Name": "status",
                    "Type": "string"
                },
                "about": {
                    "Name": "about",
                    "Type": "string"
                }
            },
            "Relations": {
                "User": "BelongsTo"
            }
        },
        "Post": {
            "Name": "Post",
            "Fields": {
                "title": {
                    "Name": "title",
                    "Type": "string"
                },
                "body": {
                    "Name": "body",
                    "Type": "string"
                },
                "public": {
                    "Name": "public",
                    "Type": "bool"
                }
            },
            "Relations": {
                "User": "BelongsTo"
            }
        }
    }
}

Starting Template

types.go

package types

{{ range .Input }}
type {{ .Name }} struct {
	{{ range .Fields -}}
	{{ camelT .Name }} {{ .Type }}
	{{ end }}
}
{{ end }}

> terminal

$ hof gen data.cue schema.cue -T types.go

package types

type Post struct {
	Body   string
	Public bool
	Title  string
}

type Profile struct {
	About       string
	DisplayName string
	Status      string
}

type User struct {
	Admin    bool
	Email    string
	Id       int
	Username string
}

Controlling Code Generation

You can control how input data and schemas are combined with templates and written to files by using the flexible format of the -T flag for code generation.

Selecting Values and Schemas

Use :<path> to select a value and @<path> to apply a schema

We can remove the .Input from our templates and pick the data and schema with flags. This approach can be helpful when we don’t have control over the input data or if it comes in a data format.

types.go

package types

// this range used to have .Input
{{ range . }}
type {{ .Name }} struct {
	{{ range .Fields -}}
	{{ camelT .Name }} {{ .Type }}
	{{ end }}
}
{{ end }}

> terminal

$ hof gen data.cue schema.cue -T types.go:Input@#Input

package types

type Post struct {
	Body   string
	Public bool
	Title  string
}

type Profile struct {
	About       string
	DisplayName string
	Status      string
}

type User struct {
	Admin    bool
	Email    string
	Id       int
	Username string
}

Partial Templates

Partial templates are fragments that are used in other templates. Unlike regular templates, these do not map to an output file. You can capture repeated sections like the fields of a struct or the arguments to a function.

Additionally, partials can invoke other partials, which is helpful for modularizing templates into logical components.

There are two ways to define and use partial templates:

  • Use the {{ define "name" }} syntax in a regular template
  • User the -P to load them from a file

For example, we could extract the generation of fields into its template. We won’t here, but as an example could include complex tasks like struct tags for Go fields.

> terminal

$ hof gen data.cue schema.cue -P field.go -T types.go -O out/

types.go

package types

{{ range .Input }}
// use a template fragment
{{ template "struct" .}}
{{ end }}

// define a template fragment
{{ define "struct" }}
type {{ .Name }} struct {
	{{ range .Fields }}
	// template from -P flag
	{{ template "field.go" . }}
	{{ end }}
}
{{ end }}

field.go

{{ camelT .Name }} {{ .Type }}

out/types.go

$ hof gen data.cue schema.cue -T types.go

package types

type Post struct {
	Body   string
	Public bool
	Title  string
}

type Profile struct {
	About       string
	DisplayName string
	Status      string
}

type User struct {
	Admin    bool
	Email    string
	Id       int
	Username string
}

Repeated Templates

In addition to looping over data and applying a template fragment, you can use repeated templates to write a file for each element in a CUE or data iterable, such as a list or struct field. This is useful for creating a separate file for each item, be it a type, API route, UI component, or DB migration.

To render a file for each element in the input to a -T flag, use [].

> terminal

$ hof gen types.cue schema.cue -T type.go="[]{{ .Name }}.go" -O out/

type.go

package types

type {{ .Name }} struct {
	{{ range .Fields }}
	{{ camelT .Name }} {{ .Type }}
	{{ end }}
}

out/User.go

package types

type User struct {
	Admin bool

	Email string

	Id int

	Username string
}

out/Post.go

package types

type Post struct {
	Body string

	Public bool

	Title string
}

Understanding the -T flag

The -T flag in hof gen has a flexible format that allows you to customize your templates’ input data, schema, and output path.

-T "<template-path>:<input-path>[@schema-path]=<out-path>"

This flag allows you to

  1. Render multiple templates by using -T more than once
  2. Select a value with :<input-path>
  3. Select a schema with @<schema-path>
  4. Write to file with =<out-path>
  5. Control the output filename with ="{{ .name }}.txt"
  6. Render a single template multiple times with ="[]{{ .filepath }}"

-T variations

hof gen input.cue ...

  # Generate multiple templates at once
  -T templateA.txt -T templateB.txt

  # Select a sub-input value by CUEpath (. for root)
  -T templateA.txt:foo
  -T templateB.txt:sub.val

  # Choose a schema with @
  -T templateA.txt:foo@#foo
  -T templateB.txt:sub.val@schemas.val

  # Writing to file with =
  -T templateA.txt=a.txt
  -T templateB.txt:sub.val@schema=b.txt

  # Templated output path 
  -T templateA.txt='{{ .name | lower }}.txt'

  # Repeated templates are used when
  # 1. the output has a '[]' prefix
  # 2. the input is a list or array
  #   The template will be processed per entry
  #   This also requires using a templated out path
  -T template.txt:items='[]out/{{ .filepath }}.txt'

You can find more examples in the hof ad-hoc render tests.

Generators and Modules

Hof Generators are CUE code that define a hof gen command, defined in CUE modules and shared with Git repositories.

Turn any ad-hoc hof gen ... -T ... args and flags into a generator by adding --as-module <module name> to the end. This will generate the equivalent CUE code version for the command. You can now run `hof gen

> terminal

# turn command into foo module
$ hof gen ... --as-module github.com/username/foo

# run generator without remembering flags
$ hof gen [-G <name>]

Several files are generated, including a CUE file that houses your generator and additional files for configuring a CUE module.

generator.cue snippet

package example

import (
	"github.com/hofstadter-io/hof/schema/gen"
}

foo: gen.Generator & {
	@gen(foo)

	// input data
	In: _

	// normally when writing generators as code
	// you add the CUE to turn In -> Out
	//   - provide project specific config and flags
	//   - dynamically decide what files to generate
	//   - craft schemas and DSLs to create anything

	// list of files to generate
	Out: [...gen.File]

	// other fields filled by hof when you turn adhoc -> reusable
}

The next page will cover modules in general and the the-walkthrough is a walkthrough on creating a full-stack application generator from scratch.