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.
hof gen -T is code gen from flags
hof gen -G is code gen from config
We will be looking at hof gen -T in this section.
The first example is a walkthrough
in writing a 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
$ 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
# set the data format when needed (cue help filetypes)$ cat data.yaml | hof gen yaml: - 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)
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
packageexample// here we are applying a schema to our input data// note how the label is the same in both filesInput: #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
packageexample// This is our input data, written as CUE// The schema will be applied to validate// the inputs and enrich the data modelInput: { 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
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.
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.
Render multiple templates by using -T more than once
Select a value with :<input-path>
Select a schema with @<schema-path>
Write to file with =<out-path>
Control the output filename with ="{{ .name }}.txt"
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'
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
packageexampleimport ("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.