Code Generation



Code generation is central to hof. By rendering templates with data, we can generate any file for any language or framework.

hof gen accepts CUE, Yaml, and JSON arguments and combines them with templates to generate files and directories of code. This process is highly flexable and controlled through flags and configuration, or what we call generators.

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

There are two main modes for template based code generation.

  1. hof gen -T configures code gen with flags
  2. hof gen -G configures code gen with CUE

We will only be covering hof gen -T in this section. The first example is a walkthrough in writing a full-stack app code generator (the -G flag). The [getting-started/create] section will introduce hof create for running generators directly from git repositories.

The concepts and processing are the same across all of them and each has a use case it is best at:

  • hof gen -T is good for simple cases or when you don’t want dependencies
  • hof gen -G is aimed at reusable and sharable modules with dependencies
  • hof create is intended for interactive one-time setup and bootstrapping

Data + Templates

hof gen interlude.json -T interlude.template is adhoc template rendering. 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.


You can also pipe any data into hof gen by using a “-” (hyphen). This can be helpful when you want to render an API response or process command output.

> 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

Writing to file

Use = after the template name to write to file.

> terminal

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

Use -O to write all outputs to a directory. 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/

The output name can be a template itself so you can control the filename from data. Make sure you “wrap it in quotes”.

> terminal

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

These can be combined so you can control where output goes and how files are named.

Multiple Templates

You can use the -T flag as many times as you want. Each is independent and can have different options applied to 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 -w/--watch to watch for changes and re-render output.

> terminal

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

There are extra watch flags if automatic detection doesn’t fully work.

On Using CUE


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

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 one large CUE value. 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 will accept them. As you will see, hof provides you flexibility and control for how the CUE values are selected, combined, and mapped to templates.

We keep parity with cue so tooling in the wider ecosystem still works on our inputs and reduces context switching costs.

You can safeuly 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

The -T flag has a flexible format so you can control how the input data and schemas are joined with templates and written to files

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 is helpful if we do not control 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 which are used in other templates. You can capture repeated sections like the fields to a struct or the arguments to a function. Unlike regular templates, these do not map to an output file. Partials can also invoke other partials, which makes them ideal for breaking up your 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

Let’s extract field generation into its own template, where we could make it complex. We won’t here, but an example is struct tags for our Go fields. We can also use template helpers in the output filepath.

> 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

We just saw how to loop over data and apply a template fragment. We can also render with repeated templates, which are processed for each element of an iterable (list or struct fields), and which also write a file for each element.

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

> 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
}

-T Flag Details

The -T flag for hof gen has a flexible format:

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

This enables 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 outpath
  -T template.txt:items='[]out/{{ .filepath }}.txt'

You can find more examples in the hof render tests.

What are Generators and Modules

Generators are hof gen flags as configuration, often in CUE modules and git repositories. The next page will overview modules more generally.

To turn your adhoc hof gen ... -T ... commands into a generator by adding --as-module <module name> after the current flags.

> terminal

$ hof gen ... --as-module github.com/username/foo

You will see a few files created. There will be a CUE file that contains your generator and a few others for setting up a CUE module.

generator.cue snippet

package example

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

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

	// input data
	In: _

	Out: [
		// list of files to generate
	]

	// other fields filled by hof
}

The first-example covers creating a generator from scracth in detail.

2022 Hofstadter, Inc