template

package
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 23, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package template stores request bodies a user sends often, and the parameters that vary between sends.

A template is a file, not a row in a database. That is the whole design: the bodies people want to keep are the ones they want to review, diff and commit next to the code that provokes them, and a store you cannot read with `cat` is a store nobody trusts with a payload that changes stock levels.

Nothing here reaches the tenant. A template is rendered to bytes and handed to an existing command's --file, so every validation, every read-only refusal and every exit code stays exactly where it already was.

Index

Constants

View Source
const Version = 1

Version is the schema version written into every template file.

It exists for the same reason [config.Version] does: a file written by a newer fft must be refused with an explanation rather than read as though the fields it does not recognise were absent.

Variables

This section is empty.

Functions

func Apply

func Apply(doc any, path Path, value any) (any, error)

Apply sets value at path inside doc and returns the document.

It returns the root rather than mutating it because appending to a top-level array produces a new slice header, and a function that silently kept the old one would drop the write it just reported making.

func Digest

func Digest(t *Template) (string, error)

Digest identifies what a template says: the SHA-256, in hex, of what Encode writes for it.

It is taken over the encoding rather than the file's bytes so that anything holding the decoded template — `fft template show -o json` decoded again, say — arrives at the same digest as the command that read the file: Encode sorts the keys and keeps every number's digits, so a template and its re-decoded copy encode alike.

func Encode

func Encode(t *Template) ([]byte, error)

Encode renders a template for the disk: indented, key-sorted and newline terminated, so that two saves of the same body produce the same file and a project-local template diffs like the source it sits next to.

func Lookup

func Lookup(doc any, path Path) (any, bool)

Lookup finds the value at path inside doc, by the same reading of a segment Apply uses. The second result is false when there is nothing there.

func Missing

func Missing(t *Template, given map[string]bool) ([]string, error)

Missing lists the required parameters nobody supplied.

given is keyed by canonical path, not by the word the user typed, so setting a required parameter by its full path satisfies it exactly as its short name would — the two are the same place in the document.

It reports all of them at once. Failing on the first turns filling in a four-parameter template into four runs, each telling the user one more thing they could already have been told.

func ParseValue

func ParseValue(raw string) any

ParseValue reads a --set value.

A value that parses as JSON is that JSON — so 3 is a number, true is a boolean, and {"a":1} is an object — and anything else is the string it looks like. Numbers arrive as json.Number, which marshals back as the digits that were typed, so a 19-digit id set here reaches the API unrounded.

The ambiguous case is a string that looks like JSON. `--set qty='"3"'` is the escape hatch, and --set-string is the one that does not need shell quoting.

func ProjectDir

func ProjectDir() (string, error)

ProjectDir is ./.fft/templates: the templates for one repository, which the team shares by committing them.

It is the working directory's, not an ancestor's. Walking up would find a template from a repository the user has merely wandered into, and a body that changes stock is not something to inherit by accident.

func Render

func Render(t *Template, sets []Set) ([]byte, error)

Render applies the declared defaults and then the given overrides, and returns the finished body as JSON.

Everything that can fail is resolved before the first write lands: a mistyped path or a missing required parameter fails against an untouched body, so there is never a half-applied render for a caller to wonder about.

func UserDir

func UserDir(lookup func(string) (string, bool)) (string, error)

UserDir is $XDG_DATA_HOME/fft/templates, else ~/.local/share/fft/templates.

Data, like the components directory and unlike the config: a template is neither something fft can regenerate by asking again nor something it manages on the user's behalf.

func ValidateName

func ValidateName(name string) error

ValidateName refuses a name that cannot safely become a filename.

This is the path-traversal guard, and it runs before any path is joined: `fft template remove ../../.ssh/id_rsa` has to fail on the name, because by the time it is a path the damage is one os.Remove away. Refusing a leading dot takes "." and ".." with it.

func ValidateParamName

func ValidateParamName(name string) error

ValidateParamName refuses a parameter name that --set could not address as itself.

--set splits its argument at the first '=' and trims the name, so a name holding an '=' or starting or ending in white space is never the key a --set arrives with: `--set a=b=x` would set a parameter "a" to "b=x", or a top-level field "a" that the template author never meant. A dot or a backslash makes a name a path. A leading dash reads as a flag wherever the name is typed on its own. Each of these is a name that works in the file and misroutes the value the moment somebody uses it.

Types

type ChangedError

type ChangedError struct {
	Name     string
	Expected string
	Actual   string
}

ChangedError is a template that is no longer the one a caller read: its digest is not the one the caller expected. It exits 7, as a stale version does, and for the same reason: what the caller decided on has moved under it.

func (*ChangedError) Error

func (e *ChangedError) Error() string

func (*ChangedError) ExitCode

func (e *ChangedError) ExitCode() int

ExitCode implements the interface exitcode.FromError looks for.

func (*ChangedError) Hint

func (e *ChangedError) Hint() string

Hint says how to see what it says now.

type Listing

type Listing struct {
	Found    []Saved
	Shadowed []Saved
	Problems []Problem
}

Listing is what the store holds: the templates a name resolves to, the user templates a project template hides, and the files that would not parse.

type MissingParamsError

type MissingParamsError struct {
	Names []string
}

MissingParamsError is a template that cannot be rendered until the user says more. It is a usage problem, and its hint is the command line to type.

func (*MissingParamsError) Error

func (e *MissingParamsError) Error() string

func (*MissingParamsError) ExitCode

func (e *MissingParamsError) ExitCode() int

ExitCode is the usage code, declared here rather than by wrapping the error in an exitcode.UsageError: that wrapper wins over the hint when fft renders the failure, and the hint — the --set line to type — is the whole of the answer.

func (*MissingParamsError) Hint

func (e *MissingParamsError) Hint() string

Hint is the --set line that satisfies the error.

type NotFoundError

type NotFoundError struct {
	Name string

	// Known is every template fft can see, for the hint.
	Known []string
}

NotFoundError is a template nobody saved. It exits 6, like any other thing the user named that is not there.

func (*NotFoundError) Error

func (e *NotFoundError) Error() string

func (*NotFoundError) ExitCode

func (e *NotFoundError) ExitCode() int

ExitCode implements the interface exitcode.FromError looks for.

func (*NotFoundError) Hint

func (e *NotFoundError) Hint() string

Hint names the templates that do exist, which is nearly always enough to spot the typo without a second command.

type Param

type Param struct {
	// Path is where in the body the value goes, in the --set path grammar.
	Path string `json:"path"`

	// Required refuses to render the template until the parameter is given.
	Required bool `json:"required,omitempty"`

	// Default is applied before any --set, so a --set for the same parameter
	// overrides it.
	Default any `json:"default,omitempty"`

	// Description is what `fft template show` prints next to the name.
	Description string `json:"description,omitempty"`
}

Param is one parameter a template declares: a name for a path inside the body, so that the common edit is `--set email=…` rather than `--set order.consumer.email=…`.

Declaring a parameter is optional. --set always accepts a bare path, so a template with no params block is fully usable; what a declaration buys is a short name, a default, and a required check that fires before the body is ever sent anywhere.

type ParamError

type ParamError struct {
	Err error
}

ParamError is a template file whose declared parameters Decode refuses. The body is fine; editing the "params" object in the file is the fix, and a caller that knows where the file is says so.

A file an earlier fft saved can be one: its parameter names were checked for less than they are now.

func (*ParamError) Error

func (e *ParamError) Error() string

func (*ParamError) Unwrap

func (e *ParamError) Unwrap() error

type Path

type Path []string

Path addresses one place inside a request body: a list of segments, outermost first.

The grammar is dotted — order.consumer.email — with a numeric segment addressing an array element. A backslash escapes the next character, so a key that genuinely contains a dot is reachable as customAttributes.order\.source. The API's customAttributes maps carry caller-chosen keys, and "that field cannot be addressed" would be a worse answer than one backslash.

The grammar is deliberately not JSON Pointer. A pointer's leading slash and ~0/~1 escapes are a second thing to learn for a CLI whose users are already typing dotted paths at jq.

func ParsePath

func ParsePath(raw string) (Path, error)

ParsePath reads the --set path grammar.

Every failure here is a typo the user can see, so each one names the position rather than reporting that the path was bad.

func (Path) String

func (p Path) String() string

String renders the path back into the grammar, escapes and all, so that an error message quotes something the user could paste.

type Problem

type Problem struct {
	Path string
	Err  error
}

Problem is a file in a template directory that fft could not read.

It is reported rather than returned as an error because one unreadable file must not make `fft template list` useless — the same reasoning the component registry uses for a component it cannot load.

type Saved

type Saved struct {
	Name  string `json:"name"`
	Scope Scope  `json:"scope"`
	Path  string `json:"path"`

	*Template
}

Saved is a template and where it was found. Resolve and List return it, and it is what `fft template show`'s table form renders. Under -o json/yaml, show instead renders the template file's own encoded bytes — not this struct — so that `show -o json` round-trips through `save --file -`.

type Scope

type Scope string

Scope is where a template lives.

The two are not a hierarchy of importance, they are a hierarchy of audience: a project template belongs to whoever clones the repository, and a user template belongs to one person on one machine.

const (
	// ScopeProject is ./.fft/templates: committed, shared with the team.
	ScopeProject Scope = "project"

	// ScopeUser is $XDG_DATA_HOME/fft/templates: personal, this machine only.
	ScopeUser Scope = "user"
)

type Set

type Set struct {
	Key   string
	Value any
}

Set is one --set as the user gave it: the key they named (a declared parameter or a bare path) and the value it takes.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store is the two directories fft looks in, project first.

func NewStore

func NewStore(lookup func(string) (string, bool)) (*Store, error)

NewStore resolves both scopes. lookup reads the environment; nil means the real one.

func (*Store) Dir

func (s *Store) Dir(scope Scope) string

Dir is where templates of the given scope live.

func (*Store) Exists

func (s *Store) Exists(name string, scope Scope) (bool, error)

Exists reports whether the named template is already in the given scope.

func (*Store) List

func (s *Store) List() (Listing, error)

List reads both scopes.

func (*Store) Load

func (s *Store) Load(name string, scope Scope) (Saved, error)

Load reads one template from one scope.

func (*Store) Path

func (s *Store) Path(name string, scope Scope) (string, error)

Path is where the named template of the given scope lives, whether or not it is there.

func (*Store) Remove

func (s *Store) Remove(name string, scope Scope) (string, error)

Remove deletes a template and reports the file it deleted.

func (*Store) Resolve

func (s *Store) Resolve(name string) (Saved, error)

Resolve finds a template by name, preferring the project scope.

Project first because that is the more specific answer: someone who committed a template to this repository meant it to be the one this repository uses.

func (*Store) Write

func (s *Store) Write(name string, scope Scope, t *Template) (string, error)

Write saves a template, creating the directory if it is not there yet.

The file is 0600 in both scopes. A project template is meant to be shared, but it is git that shares it; widening the mode on this machine first would only widen it for whoever else is logged into this machine.

type Template

type Template struct {
	SchemaVersion int `json:"schemaVersion"`

	Description string `json:"description,omitempty"`

	// OperationID is the operation the body was written for. It is advisory —
	// rendering never requires the operation to still exist, because a
	// regenerated spec must not strand every template a user saved.
	OperationID string `json:"operationId,omitempty"`

	// Project is the project the template was saved under, so that rendering it
	// against a different tenant can say so. Ids in a body rarely survive the
	// move, and sending them silently is the outcome worth preventing.
	Project string `json:"project,omitempty"`

	Params map[string]Param `json:"params,omitempty"`

	// Body is the request body, decoded with UseNumber so that a 64-bit id or a
	// version survives the round trip as the digits the API sent.
	Body any `json:"body"`
}

Template is one saved request body and everything fft knows about it.

The name is deliberately not a field: it is the filename, and two places to look up one fact is one place too many.

func Decode

func Decode(data []byte) (*Template, error)

Decode reads a template file.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL