Generate All The Things



Cue really shines with configuration management and solves a lot of fundamental problems we have with current options. And while this is going to be Cue’s primary use case for most people, you can actually generate pretty much anything.

How? By using text/templates to render any file we wish.

Generating with Cue

first.cue

package gen

import "text/template"

data: {
	// meta info
	name: *"tasks" | string @tag(name)
	msg:  "Hello \(name), welcome to Cuetorials"

	// task list
	tasks: [
		{name: "t1", effort: 1, complete: true},
		{name: "t2", effort: 4, complete: true},
		{name: "t3", effort: 3, complete: false},
		{name: "t4", effort: 2, complete: true},
		{name: "t5", effort: 3, complete: false},
	]

	// grouped tasks
	complete: [ for t in tasks if t.complete == true {t}]
	incomplete: [ for t in tasks if t.complete == false {t}]
}

// Templates which use Go's template engine
templates: [
	{
		filename: "\(data.name)-todo.txt"
		contents: """
			{{ .msg }}

			--- TODO ---
			{{ range $T := .incomplete -}}
			{{ printf "%-4s%v" $T.name $T.effort }}
			{{ end }}
			"""
	}, {
		filename: "\(data.name)-done.txt"
		contents: """
		Here's what you have finished \(data.name). Good job!

		--- DONE ---
		{{ range $T := .complete -}}
		{{ $T.name }}
		{{ end }}
		"""
	},
]

// The rendered output from data+templates
rendered: [ for T in templates {
	filename: T.filename
	contents: template.Execute(T.contents, data)
}]

first_tool.cue

package gen

import (
	"tool/file"
)

command: "gen": {
	for i, R in rendered {
		// make a unique name when comprehending
		"write-\(i)": file.Create & {
			filename: R.filename
			contents: R.contents
		}
	}
}
cue cmd -t "name=bob" gen

Cue’s template system is Go’s template system, so all of the capabilities and rules are the same. We’ve used both list and field comprehension to define render and write our templates respectively. With sophisticated Cue definitions, values, and templates you can generate any output that Cue does not map onto natively.

Generating with Hof

At Hofstadter, we built hof as a custom tool for sophisticated code generation. We wanted a single source of truth for our models which we could turn into the source code for databases, servers, and frontend which implement a full stack app. We think of it as a “high code” (low code) solution for developers. Cue was chosen as the UX/DX for writing the input (designs) and the generators which hof processes.

The two problem with previous systems are:

  1. You always end up writing custom code in the generated output
  2. Your designs evolve but you’ve already generated the boilerplate

The solution is actually pretty simple, keep a shadow copy of the generated code and use a 3-way diff to merge design updates and custom code.

With the following two files in a directory, run hof mod vendor cue and hof gen

cue.mods

module cuetorials.com/gen-with-hof

cue 0.4.0

require (
	github.com/hofstadter-io/hof v0.5.17
)

hof.cue

package gen

import (
	// import hof's generator schema
	"github.com/hofstadter-io/hof/schema/gen"
)

// A schema for our generator's input
#Input: {
	name: string
	todos: [...{
		name:     string
		effort:   int
		complete: bool
	}]
}

// create a generator
#Gen: gen.#HofGenerator & {
	// We often have some input values for the user to provide.
	// Use a Cue definition to enforce a schema
	Input: #Input

	// Required filed for generator definitions, details can be found in the hof docs
	PackageName: "dummy"

	// Required field for a generator to work, the list of files to generate
	Out: [...gen.#HofGeneratorFile] & [
		todo,
		done,
		debug,
	]

	// In is supplied as the root data object to every template
	// pass user inputs to the tempaltes here, possibly modified, enhanced, or transformed
	In: {
		INPUT:      Input
		Completed:  _C
		Incomplete: _I
	}

	// calculate some internal data from the input
	_C: [ for t in Input.todos if t.complete == true {t}]
	_I: [ for t in Input.todos if t.complete == false {t}]

	// the template files
	todo: {
		Template: """
			Hello {{ .INPUT.name }}.
			
			The items still on your todo list:

			{{ range $T := .Incomplete -}}
			{{ printf "%-4s%v" $T.name $T.effort }}
			{{ end }}
			"""
		// The output filename, using string interpolation
		Filepath: "\(Input.name)-todo.txt"
	}
	done: {
		Template: """
			Here's what you have finished {{ .INPUT.name }}. Good job!

			{{ range $T := .Completed -}}
			{{ $T.name }}
			{{ end }}
			"""
		Filepath: "\(Input.name)-done.txt"
	}

	// useful helper
	debug: {
		Template: """
			{{ yaml . }}
			"""
		Filepath: "debug.yaml"
	}
}

// Add the @gen(<name>,<name>,...) to denote usage of a generator
Gen: _ @gen(todos)
// Construct the generator
Gen: #Gen & {
	Input: {
		name: "tasks"
		todos: [
			{name: "t1", effort: 1, complete: true},
			{name: "t2", effort: 4, complete: true},
			{name: "t3", effort: 3, complete: false},
			{name: "t4", effort: 2, complete: true},
			{name: "t5", effort: 3, complete: false},
		]
	}
}

You can learn more about hof from these links:

There are also a number of “hofmod” repos for generating CLIs, APIs, and more. The separation of hofmod’s from the hof tool means that you can create your own without needing to change our code. Because they are also Cue modules, you have all the power of Cue and can import them for reusing or extending your own generators and projects.

We'll never share your email with anyone else.
2024 Hofstadter, Inc