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
- func Apply(doc any, path Path, value any) (any, error)
- func Digest(t *Template) (string, error)
- func Encode(t *Template) ([]byte, error)
- func Lookup(doc any, path Path) (any, bool)
- func Missing(t *Template, given map[string]bool) ([]string, error)
- func ParseValue(raw string) any
- func ProjectDir() (string, error)
- func Render(t *Template, sets []Set) ([]byte, error)
- func UserDir(lookup func(string) (string, bool)) (string, error)
- func ValidateName(name string) error
- func ValidateParamName(name string) error
- type ChangedError
- type Listing
- type MissingParamsError
- type NotFoundError
- type Param
- type ParamError
- type Path
- type Problem
- type Saved
- type Scope
- type Set
- type Store
- func (s *Store) Dir(scope Scope) string
- func (s *Store) Exists(name string, scope Scope) (bool, error)
- func (s *Store) List() (Listing, error)
- func (s *Store) Load(name string, scope Scope) (Saved, error)
- func (s *Store) Path(name string, scope Scope) (string, error)
- func (s *Store) Remove(name string, scope Scope) (string, error)
- func (s *Store) Resolve(name string) (Saved, error)
- func (s *Store) Write(name string, scope Scope, t *Template) (string, error)
- type Template
Constants ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
type Problem ¶
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.
type Set ¶
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 ¶
NewStore resolves both scopes. lookup reads the environment; nil means the real one.
func (*Store) Path ¶
Path is where the named template of the given scope lives, whether or not it is there.
func (*Store) Resolve ¶
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 ¶
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.