repo

package
v0.0.1 Latest Latest
Warning

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

Go to latest
Published: Aug 24, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

README

templates-repo

The templates repository is a cache for collecting golang text templates.

It compiles a set of Go text templates into an immutable repository. It reads assets from an io/fs.FS, a directory or a []byte, parses them in a single pass so that every template can call every other, and returns them ready to execute from Get.

A code generator ships a default set of templates and may allow its users to override some of them. The repository holds that set, and takes overlay options to override from further sources.

Features

  • expose a namespace for the whole tree of templates, including {{ define }} macros
  • safe for a concurrent use
  • automatic check and dependencies resolution
  • support for composition and overrides with overlays
  • cache compiled templates from assets on a file system, possibly embedded

Experimental features

  • self-check audit: unused or empty templates, likely errors
  • generates documentation for your templates from data introspection and comments in source
  • instruments templates for test coverage reporting

The types these three produce live in the reports sub-package, so the main API stays to what executing templates needs.

Getting started

go get github.com/go-openapi/codegen
import repo "github.com/go-openapi/codegen/templates-repo"

Add the sub-package only to describe a repository, never to run one:

import "github.com/go-openapi/codegen/templates-repo/reports"

Building

A repository is built once, from sources declared as options, and is sealed from then on.

All dependencies and templates are resolved eagerly: any compilation or dependency error is known at build time.

templates, err := repo.New(
    repo.FromFS(assets, ""),                                      // load from an io/fs.FS
    repo.FromDir("./mytemplates", ""),                            // load from local disk
    repo.FromTemplate("addon", []byte("{{ printf \"%#v\" . }}")), // load from []byte
    repo.WithFuncMap(funcs),
)

tpl, err := templates.Get("validationPrimitive")
err = tpl.Execute(w, data)
...

Sources are read in the order they are declared, so a template declared twice comes from the last one. Use Audit to report on overridden templates.

Each source decides what it reads. SkipDirectories leaves a directory unread, along with everything below it:

repo.FromFS(assets, "", repo.SkipDirectories("contrib"))  // the alternate sets are stacked, not loaded wholesale

It matches a directory by its own name, the last segment of its path, at any depth: contrib at the root and server/legacy/contrib alike. It matches neither a path nor a template name, since it decides what a source reads before anything is named. Nothing is skipped by default, and what one source skips says nothing about any other.

New reports an error when it cannot build the set of templates. This may be because of an unreadable source, a template that does not parse, a reference that reaches nothing, a root that no source declares, or an override that would be silently ignored.

That last one needs a word. text/template.Template.AddParseTree keeps the existing definition when the new parse tree is empty, so an override holding nothing but white space and comments would leave the earlier definition in place. New rejects it rather than let the override pass unnoticed.

To override a template with one that renders nothing, give it an action to run:

{{ "" }}

Concurrency

A Repository is immutable and lock-free. It is safe for concurrent use, as are the returned templates.

Clone is the only way to derive a new Repository from one already built. It only reads the repository it derives from, so cloning is safe while that repository is in use.

New sources may be added at cloning time. The clone rebuilds the entire set, and may error.

patched, err := repo.Clone(repository, repo.FromTemplate("model.gotmpl", mine))

The test coverage counters are the exception to immutability: the templates are frozen, the counters are not. They are atomic, so rendering in parallel needs no lock.

A namespace for your templates

Addresses and names

A template is known by three strings: the asset path it was read from, the address it was declared at, and the name it answers to. Only the name executes it, and the address reaches the same template through Lookup, so pick whichever your caller already holds.

asset path   server/parameter.gotmpl        the file it came from
address      server/parameter               where its author declared it
name         serverParameter                what it answers to

The address is the original path to the template source, where its author declared it.

An asset is addressed at its own path, extension trimmed. Separators are normalized to /, so a caller may write server\parameter on Windows and get the same address as everyone else. A define statement is addressed under it like so:

server/parameter.gotmpl                     ->  server/parameter
{{ define "bind-primitive" }} within it     ->  server/parameter/bind-primitive

Lookup and MustLookup retrieve a template by address:

tpl, err := templates.Lookup("server/parameter")

The name is the identity Get takes. It camel-cases the address, / counting as a word boundary like any other:

server/parameter                ->  serverParameter
server/parameter/bindPrimitive  ->  serverParameterBindPrimitive
tpl, err := templates.Get("serverParameter")

Four methods translate between the three:

NameOf("server/parameter") the name an address answers to
AddressOf("serverParameter") the address behind a name
AssetOf("serverParameter") the file a name was read from
Addresses() every address and name the repository holds

TemplateName computes a name without a repository, for a caller that has to choose its sources by name before there is a repository to ask.

The godoc carries a runnable example for each of these, over a two-template repository. Start there if the three words have not settled yet.

Resolving relative references

{{ template "x" }} means something relative to where an author wrote it. Two sets of templates can therefore each have a body macro, and each reaches its own.

Take this tree:

server/fred.gotmpl                   declares  {{ define "inner-macro" }}
server/claude.gotmpl
server/operations/operation.gotmpl
client/swagger.gotmpl

A reference is looked up outward from the template holding it. Starting at server/claude.gotmpl, that means: templates under server/claude itself, then under server/, then under the root. At each step two things can match, and the first match wins:

  • a template addressed under that step, named by recasing its address relative to that step
  • a define declared by an asset sitting directly in that step, named as its author wrote it

So server/claude.gotmpl reaches four things four ways:

{{ template "inner-macro" }}          {{/* a define of a neighbour, by its own name */}}
{{ template "fred" }}                 {{/* server/fred, relative to server/         */}}
{{ template "operationsOperation" }}  {{/* server/operations/operation, relative    */}}
{{ template "serverFredInnerMacro" }} {{/* anything at all, by its name             */}}

From client/swagger.gotmpl, only the last one works. client/ encloses none of those templates, so nothing there is in reach except by name. A define never travels beyond the directory holding it, so one set cannot capture another set's macro.

The repository reports an error for two situations rather than guessing:

  • one name matching both a template addressed under a level and a define of that level
  • two assets of a directory declaring the same bare name

The build resolves every reference once, writing the name it addresses into the parse tree, so nothing is resolved again while a template runs.

Scoping a run

A generator ships every template it may ever need, and a single run might need only a part of them.

Scope the repository so that a run carries only the templates it executes.

client, err := repo.Clone(repository, repo.WithRoots("clientClient", "model")) // resolves dependencies from these roots

The repository then holds the roots and whatever they reach, and nothing else.

A root is a name, the identity Get takes, and never the address a template was declared at. Scoping is the one place that accepts names alone: Lookup takes either, so convert with NameOf if your caller holds addresses.

scoped, err := repo.Clone(repository, repo.WithRoots(repository.NameOf("client/client")))

A root that no source declares is an error: a filter naming a template that does not exist would build a repository that quietly generates nothing. Naming an address instead of a name reports that same error, and says so.

WithRoots sets the scope. WithExtraRoots widens it. Both take names.

WithExtraRoots changes nothing on a repository that already keeps everything, so a caller adding a template writes the same call either way.

client, _ := repo.Clone(repository, repo.WithRoots("clientClient"))

// "mine" is reachable from no root, so the scope has to admit it
mine, err := repo.Clone(client,
    repo.FromTemplate("mine.gotmpl", body),
    repo.WithExtraRoots("mine"),
)

Against a repository that keeps everything, the second call changes nothing, since mine is already there. Write it the same way either way, without checking which kind of repository you hold.

Roots returns the current scope, and is empty when the repository kept everything it read.

Composition

A package shipping templates publishes sources, not a repository.

A repository can be built only when everything it refers to is there.

// assembling two repos exposed by other packages
templates, err := repo.New(
    genmodels.Sources(repo.Rebased("models")),
    genclient.Sources(repo.Rebased("client")),
    repo.FromDir("./mine", ""),
)

Rebased mounts a source under a base, on top of wherever it already mounts, so the package that ships it chooses none of that. Mounting moves every address under the mount point, and the references between templates move with them: a set resolves the same wherever it lands, and two sets that each declare a body macro no longer collide.

One caveat. A template that calls into another set names the address that set was mounted at, so a package whose templates do that has an expected mount point. Document it alongside the root templates the package exports and the data they are executed on.

Rebase, Merge and Coalesce do the same to repositories that are already built. Merge lets the last repository to declare an address win, Coalesce the first, and each combines the func maps the same way.

Experimental features

These describe a repository rather than run it, and their types live in the reports sub-package:

import (
    repo "github.com/go-openapi/codegen/templates-repo"
    "github.com/go-openapi/codegen/templates-repo/reports"
)

Ten types describe a repository; executing its templates needs none of them. A program that only renders imports repo alone.

Self-healthcheck

Audit reads the assets again and returns a reports.Audit, listing what compiles and runs but still deserves a look:

  • templates that more than one asset declared, and which definition stands
  • templates that no other template calls
  • templates that render nothing
  • templates that call a function carried by their data
  • funcmap entries that no template calls

None of it is an error. New rejects what it cannot resolve, so everything the audit reports already compiles and runs.

Overrides and shadowed templates

Stacking sources is how a set replaces what it needs to, so an override is intended far more often than not and is never an error. It is still worth seeing:

report, err := repository.Audit()
if err != nil {
    return err
}

for _, override := range report.Overridden {
    log.Printf("%s comes from %s, replacing %v",
        override.Name, override.Standing, override.Replaced)
}

Nothing else reveals a set that replaced a template by accident.

Unused templates and functions

Unused lists the templates that no other template calls. UnusedFuncs lists the func map entries that no template calls.

Neither is a verdict. Nothing calls a generator's entry points either, so a repository that keeps every template it read cannot distinguish an entry point from a template that outlived its callers.

Scope the repository with WithRoots and Unused comes back empty, since everything left is a root or is reached from one. To find dead templates, audit the unscoped repository and subtract the entry points you know about; what remains is worth a look.

UnusedFuncs reads the same way. A generator that gives its templates a general-purpose library will find most of it unused, which is expected rather than a defect.

Spot dynamic calls

The call builtin invokes a function carried by the data, and only at execution time. A repository resolves everything else before a run starts, so neither the audit nor the documentation can report what these calls reach.

Dynamic lists the templates that use call, which at least bounds the blind spot.

A function that no funcmap provides never reaches the audit: templates are parsed against the funcmap, so calling a function nothing provides fails the build.

Self-documentation

Templates are part of the interface a generator exposes, so a repository can document them: the comments on each template, the data paths it reads, the functions it calls, and the templates it calls with the data passed to each.

err = repository.Dump(w)                             // markdown, the common way to ask
documentation, err := repository.Documentation()     // or the reports.Documentation behind it

reports.Dump renders a documentation on its own, which suits a document built once and laid out several ways:

err = reports.Dump(w, documentation, reports.WithTemplate(myLayout))

The analysis runs on demand rather than when the repository is built, so a caller that only executes templates does not pay for it. The output is ordered throughout, so the same templates produce the same document every time, and that document can be committed and checked in CI.

It reports the data as a closure over every branch: what the data must be able to answer, not a list of what it must hold.

Test coverage
counting, err := repo.Clone(repository, repo.WithCoverage("example.com/gen/templates"))
...
err = counting.Coverage().Flush(profile)

go tool cover -html renders the result. A line that never ran appears in the profile at zero, and a line holding nothing but a define, an end or an else is left out, so it greys out the way a Go declaration does. Instrumentation has to be set when the repository is built, because the templates that execute must be the ones holding the counters.

Two branches on one line share one counter: {{if .A}}x{{else}}y{{end}} reports the line covered when either ran. Telling them apart needs column positions, which the template parser does not report.

Design notes

A record of the decisions that were not obvious.

Why a repository retains its sources rather than its compiled state

Clone re-parses everything and copies no compiled object. An override therefore reaches the templates that already referred to it, which earlier designs got wrong in both directions: one mutated a shared cache and contaminated every holder, the other isolated so thoroughly that the override never took effect.

The price is a full parse per derivation. Derive for the settings of a program, decided once, not per operation.

Why references are rewritten into the parse trees

Authors write names relative to where they are. text/template executes against one flat namespace. The build reconciles the two by resolving each reference once and writing the resolved name into the node.

The alternative, a namespace per template with its dependencies grafted in, costs a copy per dependency. Rewriting costs one pass: about 0.1 ms over the go-swagger set, against about 22 ms to parse it.

This works only because the scope is lexical. A reference resolves by where its template was declared, never by who invoked it. Dynamic scope would force namespaces back.

Why a doubly answered reference is refused

One name can match both a template addressed under a scope and a define of that scope. Picking either silently sends the reference somewhere the author did not mean.

We tried precedence first, addressed-before-define. It sent a call to the wrong template and the test suite hung on the recursion that followed. The repository now reports the ambiguity and leaves the author to rename one of the two.

Why skipping directories belongs to the source

Which directories to skip describes the file system being walked, not the repository. As a repository-wide setting it also skipped the same directory name in template sets brought by users, and an override placed there did nothing at all, silently.

Why the analysis re-parses the assets

Execution trees drop comments, and their references have already been rewritten to names. Neither the docstrings nor the names an author typed survive there. Assets are retained anyway, so the analysis reads them again and maps what it finds back onto the addresses the repository holds.

Why composition rebuilds

Re-addressing compiled trees would run about six times faster than parsing again. Rebase and Merge still rebuild from the retained assets, because relative resolution then costs nothing extra: a set that resolved on its own resolves identically once rebased, and no reference needs recomputing.

Take the faster path only if assembling ever lands on a hot path.

Documentation

Overview

Package repo compiles a set of Go text templates into an immutable repository.

A code generator ships a default set of templates and lets its users override some of them. This package holds that set. It reads assets from an io/fs.FS, a directory or a byte slice, parses them in a single pass so that every template can call every other, and returns them ready to execute from Repository.Get.

Usage

A repository is built once, from sources declared as options:

repository, err := repo.New(
	repo.FromFS(assets, ""),
	repo.WithFuncMap(funcs),
)
if err != nil {
	return err
}

tpl, err := repository.Get("validationPrimitive")
if err != nil {
	return err
}

err = tpl.Execute(w, data)

A repository is immutable once New returns. To change the set, derive a new repository with Clone:

patched, err := repo.Clone(repository, repo.FromTemplate("validation/primitive", mine))

Clone re-parses every asset, so a template that calls the overridden one calls the new definition. The two repositories share no state.

Every error this package reports matches ErrTemplateRepo and wraps its cause, so a caller may also match a parse error or an io/fs error with errors.Is and errors.As.

Naming

A template is known by three strings: the asset path it was read from, the address it was declared at, and the name it answers to. The address is the asset path with the extension trimmed, slash-separated and otherwise untouched. The name is that address recased, "/" counting as a word boundary like any other.

validation/primitive.gotmpl  ->  address validation/primitive,  name validationPrimitive
server/parameter.gotmpl      ->  address server/parameter,      name serverParameter
model.gotmpl                 ->  address model,                 name model

Repository.Get takes a name and Repository.Lookup takes an address. Repository.NameOf maps an address to a name, Repository.AddressOf maps it back, and Repository.Addresses iterates over both. TemplateName computes a name before any repository exists, which a caller needs when the templates it is about to declare are themselves chosen by name.

A "define" statement declares a further template, addressed under the asset that holds it:

server/fred.gotmpl holding {{define "inner-macro"}}
  ->  address server/fred/inner-macro,  name serverFredInnerMacro

Paths are slash-separated whatever the platform, so a caller may write "server\parameter" on Windows and reach the same template as everyone else.

Two assets of the same source declaring one name is an error, since no source is read after the other. Across sources, the last declaration wins.

Overriding

Sources are read in the order they are declared, and the last declaration of a name wins. There is no other precedence rule: a source cannot mark a template as final.

Stacking whole sets of templates is a file system concern rather than a repository one. Merge the sets into one io/fs.FS with github.com/go-openapi/swag/fileutils.NewOverlayFS, then pass the result to FromFS.

SkipDirectories attaches to one source, not to the build. Skipping "internal" in your own assets leaves an "internal" directory in a set someone else brings fully readable.

It matches a directory by its own name, the last segment of its path, at any depth. It matches neither a path nor a template name: it decides what a source reads, before anything is named.

Repository.Audit lists every name that more than one asset declared, with the definition that stands and the ones it replaced. Run it where a build can fail, so that a contrib set which shadows a template by accident is caught before it ships. It returns a github.com/go-openapi/codegen/templates-repo/reports.Audit, which covers more than overrides.

Scoping

A generator ships every template it may ever need, and one run uses a fraction of them. WithRoots keeps the named templates and everything they call, and prunes the rest:

client, err := repo.Clone(repository, repo.WithRoots("clientClient", "model"))

A root is a name, the identity Repository.Get takes, and never the address a template was declared at. Scoping is the one place that accepts names alone: Repository.Lookup takes either, so a caller holding addresses converts them with Repository.NameOf first. Naming an address reports an error rather than building an empty repository.

A pruned template is gone from the repository: Repository.Names does not list it, Repository.Documentation does not describe it, and Repository.Coverage does not count it. Every asset is still read and parsed, because a template only announces its name once parsed. The assets are retained whole, so a later Clone with WithExtraRoots widens the scope again.

Repository.Roots returns the current scope, and is empty when the repository kept everything it read.

Assembling

A repository may itself be a source. FromRepository reads what one holds and mounts it at a chosen point, so two sets written independently are assembled without either knowing about the other:

templates, err := repo.New(
	repo.FromDir("./scaffolding", ""),
	repo.FromRepository(modelTemplates, "models"),
	repo.FromRepository(serverTemplates, "server"),
)

Mounting moves every address under the mount point, and the references between templates move with them, so a set that resolved on its own resolves the same mounted. Two sets that each define a macro called "header" no longer collide, because their addresses now differ.

A package that ships templates should therefore export sources rather than a repository. A scaffolding that calls into the parts it is assembled with cannot be built on its own, so those parts have to be sources of the same build:

// what the package exports, knowing nothing of where it lands
func Sources(opts ...repo.SourceOption) repo.Option {
	return repo.Sources(
		repo.FromFS(templates, "", opts...),
		repo.FromFS(filepaths, "paths", opts...),
	)
}

One caveat is worth stating. A template that calls into another set names the address that set was mounted at, so a package whose templates do that has an expected mount point. Document it alongside the templates the package exports and the data they are executed on. A set that calls into nothing may be mounted anywhere.

Rebase, Merge and Coalesce do the same to repositories that are already built. Rebase moves every address under a base. Merge lets the last repository to declare an address win, and Coalesce lets the first. All three need each part to build on its own.

Documentation and audit

Templates are part of the interface a generator exposes, so a repository can document them. Repository.Documentation reads the assets again, comments included, and reports:

  • the comments documenting each template, attached as a Go author expects, a comment group placed immediately before a "define" statement documenting that template
  • the data paths each template reads, and which of them it reaches through the root
  • the functions it calls, and the templates it calls, with the data passed to each

Repository.Dump renders that model as markdown:

err = repository.Dump(w)

The analysis runs on demand rather than when the repository is built, so a caller that only executes templates does not pay for it. The output is ordered throughout, so the same templates produce the same document every time, and that document can be committed and checked in CI.

The types these methods return live in a package of their own, github.com/go-openapi/codegen/templates-repo/reports. Describing a repository takes ten types; executing its templates takes none of them, so a program that only renders imports neither the documentation model nor the audit report. That package also renders a document on its own, through reports.Dump, which suits a document built once and laid out several ways.

Coverage

WithCoverage instruments a repository to count the lines of its templates that execute. It has to be set when the repository is built, because the templates that run must be the ones holding the counters. Clone carries the setting over, so a plain repository clones into an instrumented one:

counting, err := repo.Clone(repository, repo.WithCoverage("example.com/gen/templates"))

The prefix is prepended to the path of every asset in the profile. go tool cover resolves the file a profile names by asking go list, so the paths have to read as an import path of a package that exists. That is why the prefix is required.

Repository.Coverage returns a profile in the format go test writes, which go tool cover renders as html. A line that never ran appears at zero rather than being absent. A line holding nothing but a define, an end or an else is left out, so it greys out the way a Go declaration does.

Concurrency

A repository is immutable and safe for concurrent use, as are the Template values it returns. Clone only reads the repository it derives from.

The coverage counters are the one part that changes after a build. They are atomic, so a generator rendering templates in parallel needs no lock.

Example (Vocabulary)

A template is known by three strings, and only the last one executes it.

package main

import (
	"fmt"

	repo "github.com/go-openapi/codegen/templates-repo"
)

// oneAsset declares two templates: the asset itself, and the "define" it holds.
func oneAsset() repo.Option {
	return repo.FromTemplate("server/parameter.gotmpl", []byte(
		`{{ define "bind" }}bound{{ end }}param[{{ template "bind" }}]`,
	))
}

func main() {
	repository, err := repo.New(oneAsset())
	if err != nil {
		panic(err)
	}

	for address, name := range repository.Addresses() {
		asset, _ := repository.AssetOf(name)
		fmt.Printf("asset %-24s address %-24s name %s\n", asset, address, name)
	}

}
Output:
asset server/parameter.gotmpl  address server/parameter         name serverParameter
asset server/parameter.gotmpl  address server/parameter/bind    name serverParameterBind

Index

Examples

Constants

View Source
const DefaultExtension = ".gotmpl"

DefaultExtension is the file extension a repository recognizes as a template when the caller declares no other with WithExtensions.

View Source
const ErrTemplateRepo repoError = "template repository"

ErrTemplateRepo is matched by every error this package reports.

Errors wrap the cause as well, so a caller may match on a parse error or on an io/fs error with errors.Is and errors.As all the same.

Variables

This section is empty.

Functions

func TemplateName

func TemplateName(assetPath string, extensions ...string) string

TemplateName returns the name a repository gives an asset at a path.

It computes the name before a repository exists, for a caller that has to choose its sources by name: the name decides what to build, so it cannot wait for the build. Repository.NameOf answers the same question for a repository already built.

The extensions are those the repository recognizes, DefaultExtension when none is given.

Example:

repo.TemplateName("validation/primitive.gotmpl")  ->  validationPrimitive

Types

type Option

type Option func(options) options

Option configures a Repository built with New or derived with Clone.

An option that cannot be honoured reports an error from New or Clone, rather than at the point where it is constructed: a repository built from settings the caller did not ask for is worse than one that fails to build.

Usage

Options come in two kinds. Sources declare where templates are read from, and are applied in order: FromFS, FromDir and FromTemplate. Settings shape how they are read, whatever the order: WithFuncMap, WithExtensions, WithRoots, WithExtraRoots and WithCoverage. How one source is read is settled where it is declared, with a SourceOption.

func FromDir

func FromDir(dir, mountPoint string, opts ...SourceOption) Option

FromDir reads every supported asset of a local directory, and mounts them at mountPoint.

dir is a path in the os file system, and the assets are named relative to it. It is the shorthand for FromFS over an os.DirFS, and it reports an error when dir is not a readable directory.

The directory is read once, when the repository is built. Editing a template on disk afterwards has no effect until a repository is built again.

func FromFS

func FromFS(fsys fs.FS, mountPoint string, opts ...SourceOption) Option

FromFS reads every supported asset of fsys, and mounts them at mountPoint.

fsys is read from its root, so a caller serving a subtree re-roots it beforehand with io/fs.Sub. An empty mountPoint, or ".", mounts the assets at the top of the template tree, which is the usual case.

Overriding is not this option's business: a caller that wants one set of files to take precedence over another stacks them into a single io/fs.FS first, and passes the result.

Example:

// the assets of an embed.FS, patched by an alternate set living elsewhere in the same tree
repo.New(repo.FromFS(fileutils.NewOverlayFS(
	fileutils.MustSub(assets, "templates"),
	fileutils.MustSub(assets, "templates/contrib/mine"),
), ""))

func FromRepository

func FromRepository(source *Repository, mountPoint string, opts ...SourceOption) Option

FromRepository reads the templates another repository holds, and mounts them at mountPoint.

This is how a set assembled out of parts is built in one pass. A repository is only built when everything it refers to is there, so a scaffolding that calls into the parts it is assembled with cannot stand on its own. Declare the parts as sources of the same build, and each may be written apart and resolved together.

The templates of the repository are read as they were declared, and mounting them somewhere moves their addresses the way Rebase does. What they refer to moves with them, so a set that resolved on its own resolves the same mounted.

Nothing of the repository is read again: it retained the content of its own sources, and that is what is carried over. It is left untouched.

Example:

// a scaffolding of one's own, with the sets it calls into
repo.New(
	repo.FromDir("./scaffolding", ""),
	repo.FromRepository(modelTemplates, "models"),
	repo.FromRepository(serverTemplates, "server"),
)

func FromTemplate

func FromTemplate(name string, content []byte, opts ...SourceOption) Option

FromTemplate registers a single template held in memory, at the address given.

The address locates the template, exactly as written, so it may hold directories and it is never mangled: overriding a template declared elsewhere means naming the address it was declared at. The key it answers to is derived from the address like any other.

Unlike an asset read from a file system, it is registered whatever its extension.

This is the way to declare a template that no file holds, such as one a configuration provides. A caller holding several of them is better served by an in-memory io/fs.FS passed to FromFS, which keeps every override going through the same mechanism.

The content is retained, not copied.

func Sources

func Sources(opts ...Option) Option

Sources bundles several options into one, so that a package exports everything its templates need as a single option.

A set of templates is rarely one source: the templates themselves, the ones that place each generated section, and whatever else a package ships. The caller assembling them does not have to know how many there are, nor in what order they go.

Example:

// the package publishes this, and nothing of what is inside it
func Sources(opts ...repo.SourceOption) repo.Option {
	return repo.Sources(
		repo.FromFS(templates, "", opts...),
		repo.FromFS(filepaths, "paths", opts...),
	)
}

func WithCoverage

func WithCoverage(prefix string) Option

WithCoverage counts the lines of the templates that run.

Counting is decided here rather than later: the templates that execute have to be the ones holding the counters, so a repository either is instrumented or is not. Clone carries the setting over, and a clone of a plain repository asking for it yields an instrumented twin.

prefix is prepended to the path of every asset in the profile Repository.Coverage writes. go tool cover resolves the file a profile names by asking go list, so the paths have to read as an import path of a package that exists, so prefix is required.

Example:

repo.WithCoverage("github.com/go-swagger/go-swagger/generator/templates")

func WithExtensions

func WithExtensions(extensions ...string) Option

WithExtensions sets the file extensions recognized as templates when reading a file system.

The default is ".gotmpl" alone. An asset whose name ends with none of them is ignored by FromFS and FromDir, while FromTemplate registers its content whatever its name.

The extension is trimmed from the asset path before its name is derived, so "validation/primitive.gotmpl" is named validationPrimitive.

func WithExtraRoots

func WithExtraRoots(names ...string) Option

WithExtraRoots widens the scope of a repository with roots of its own.

It adds to whatever WithRoots settled, and changes nothing on a repository that keeps every template it reads, since that scope already holds them. It takes names, as WithRoots does.

Use it to add a template to a repository the caller did not build. WithRoots would be wrong either way: on an unscoped repository it prunes everything else away, and on a scoped one it discards the scope already set.

Example:

// one more template, reachable whether or not the repository is scoped
mine, err := repo.Clone(repository, repo.FromTemplate("mine", body), repo.WithExtraRoots("mine"))

func WithFuncMap

func WithFuncMap(funcs template.FuncMap) Option

WithFuncMap adds functions that templates may call.

By default a repository binds no function, so templates only have the builtins of text/template. The map is copied, and repeated calls merge, the last definition of a name winning.

Functions are bound when templates are parsed, which is why they cannot be changed afterwards. Adding a function to an existing repository is Clone with this option: the clone re-parses its templates, so the new function reaches all of them.

func WithRoots

func WithRoots(names ...string) Option

WithRoots keeps the templates named, and the templates they reach, and prunes the rest.

A generator ships every template it may ever need, and a single run needs a part of them: the templates a client needs are not those a server needs. Naming the roots of a run keeps the repository to what that run executes, and lets a template set that is incomplete for the other runs build all the same.

Roots are names, the identity Repository.Get takes, never the address a template was declared at. Repository.NameOf converts one to the other for a caller holding addresses.

A root no source declares is an error: a filter naming a template that does not exist builds a repository that generates nothing, which is worse than a build that fails.

Everything is still read and parsed, since a template only names itself once parsed, so a source that does not parse is an error whether it is pruned away or not. Pruning decides only which templates the repository holds, and therefore what Repository.Names lists, what its documentation covers, and what its coverage counts.

This sets the scope rather than adding to it: a Clone naming roots of its own is scoped to those alone, whatever the repository it derives from was scoped to. WithExtraRoots is the one that widens a scope. A repository with no root at all keeps every template it reads, which is the default.

Example:

// the templates a client generation executes, and nothing else
client, err := repo.Clone(repository, repo.WithRoots("clientClient", "clientParameter", "model"))
Example

Roots are names, the identity Get takes, and never addresses.

package main

import (
	"fmt"

	repo "github.com/go-openapi/codegen/templates-repo"
)

// oneAsset declares two templates: the asset itself, and the "define" it holds.
func oneAsset() repo.Option {
	return repo.FromTemplate("server/parameter.gotmpl", []byte(
		`{{ define "bind" }}bound{{ end }}param[{{ template "bind" }}]`,
	))
}

func main() {
	repository, err := repo.New(oneAsset(), repo.WithRoots("serverParameter"))
	if err != nil {
		panic(err)
	}

	fmt.Println(repository.Roots())

	// an address is not a name, and saying so is an error rather than an empty repository
	_, err = repo.New(oneAsset(), repo.WithRoots("server/parameter"))
	fmt.Println(err != nil)

	// a caller holding addresses converts them first
	naming, err := repo.New(oneAsset())
	if err != nil {
		panic(err)
	}

	scoped, err := repo.Clone(naming, repo.WithRoots(naming.NameOf("server/parameter")))
	if err != nil {
		panic(err)
	}

	fmt.Println(scoped.Roots())

}
Output:
[serverParameter]
true
[serverParameter]

type Repository

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

Repository is a set of compiled templates, resolved against one another and sealed.

A repository is built by New from the sources given as options, and derived by Clone. It has no other constructor, and nothing alters it once built.

Usage

A repository reads its sources once, when it is built, and retains their content. Everything else follows from that:

  • templates share a single namespace, so any of them may refer to any other by name
  • Clone re-reads nothing and re-parses everything, so a template added by a clone is seen by the templates that were already there
  • the file system a repository was built from is not retained, and is never read again

A clone therefore costs a full rebuild. This is meant for the settings of a program, decided once, and not for a per-operation derivation.

Concurrency

A repository is immutable, holds no lock, and is safe for concurrent use. Clone only reads its source, so cloning a repository other goroutines are using is safe as well.

func Clone

func Clone(source *Repository, opts ...Option) (*Repository, error)

Clone builds a repository from the assets and settings of another one, with opts applied on top.

The sources of source are not read again: their content, retained when source was built, is carried over and the sources declared by opts are appended to it. Everything is parsed and resolved afresh, so a template that opts overrides is picked up by every template referring to it, and the two repositories share nothing.

source is left untouched, and may be used by other goroutines while Clone runs. A nil source reports an error.

Example:

// the same templates, with one of them replaced
patched, err := repo.Clone(repository, repo.FromTemplate("model", myModel))

func Coalesce

func Coalesce(source *Repository, coalesced ...*Repository) (*Repository, error)

Coalesce derives a repository holding the templates of several, the first to declare an address winning.

It is Merge the other way round: what a later repository declares at an address another one already holds is dropped rather than taking its place. Func maps are coalesced the same way, by github.com/go-openapi/codegen/funcmaps.Coalesce, which also leaves the builtins alone.

This is for assembling a set out of parts where the first one named is the one in charge, and a later one only fills what is missing.

func Merge

func Merge(source *Repository, merged ...*Repository) (*Repository, error)

Merge derives a repository holding the templates of several, the last to declare an address winning.

A merge exists in order to override, so a template declared twice is not an error: Repository.Audit reports which definition stands and which it replaced. Func maps are merged the same way, by github.com/go-openapi/codegen/funcmaps.Merge.

Assembling sets that were written apart usually means Rebase first, which is what keeps their addresses from meeting at all.

Example:

templates, err := repo.Merge(scaffolding,
	must(repo.Rebase(modelTemplates, "models")),
	must(repo.Rebase(serverTemplates, "server")),
)

func New

func New(opts ...Option) (*Repository, error)

New builds a repository from the sources and settings given as options.

Sources are read in the order they are declared, and a template declared by several of them comes from the last one. Reporting an error rather than a repository covers an unreadable source, a template that fails to parse, a template that refers to one no source declares, and an override that would silently not replace what it overrides.

Building a repository with no source at all yields an empty one, which is not an error.

Example:

repository, err := repo.New(
	repo.FromFS(assets, ""),
	repo.FromDir("./mytemplates", ""),
	repo.WithFuncMap(funcs),
)

func Rebase

func Rebase(source *Repository, base string) (*Repository, error)

Rebase derives a repository holding the templates of another one, addressed under a base.

Every address moves under base, so a repository built from server/parameter.gotmpl rebased under "v2" holds v2/server/parameter, answering to v2ServerParameter.

What the templates refer to moves with them. A reference is resolved outward from where it was written, and everything it could reach is still there, one level further in, so a set that resolved on its own resolves the same rebased, so a repository may be assembled rather than only built.

The repository it derives from is untouched.

Example:

models, err := repo.Rebase(modelTemplates, "models")

func (*Repository) AddressOf

func (r *Repository) AddressOf(name string) (string, bool)

AddressOf returns the address a name is declared at, and whether it is declared at all.

It reverses Repository.NameOf.

Example

AddressOf goes back, and reports whether the name is declared at all.

package main

import (
	"fmt"

	repo "github.com/go-openapi/codegen/templates-repo"
)

// oneAsset declares two templates: the asset itself, and the "define" it holds.
func oneAsset() repo.Option {
	return repo.FromTemplate("server/parameter.gotmpl", []byte(
		`{{ define "bind" }}bound{{ end }}param[{{ template "bind" }}]`,
	))
}

func main() {
	repository, err := repo.New(oneAsset())
	if err != nil {
		panic(err)
	}

	fmt.Println(repository.AddressOf("serverParameterBind"))
	fmt.Println(repository.AddressOf("nowhereAtAll"))

}
Output:
server/parameter/bind true
 false

func (*Repository) Addresses

func (r *Repository) Addresses() iter.Seq2[string, string]

Addresses iterates over what this repository declares, address first, name second, ordered by name.

func (*Repository) AssetOf

func (r *Repository) AssetOf(name string) (string, bool)

AssetOf returns the path of the asset that declares a name, and whether it is declared at all.

The path is the asset's, once mounted, and the name was derived from it.

Example

AssetOf names the file a template was read from, extension and all.

package main

import (
	"fmt"

	repo "github.com/go-openapi/codegen/templates-repo"
)

// oneAsset declares two templates: the asset itself, and the "define" it holds.
func oneAsset() repo.Option {
	return repo.FromTemplate("server/parameter.gotmpl", []byte(
		`{{ define "bind" }}bound{{ end }}param[{{ template "bind" }}]`,
	))
}

func main() {
	repository, err := repo.New(oneAsset())
	if err != nil {
		panic(err)
	}

	fmt.Println(repository.AssetOf("serverParameterBind"))

}
Output:
server/parameter.gotmpl true

func (*Repository) Audit

func (r *Repository) Audit() (reports.Audit, error)

Audit reports what a repository holds that is worth a second look.

It reads the assets again, as Repository.Documentation does, so a caller pays for it only by asking. Run it where a build can fail: it reveals a contrib set that replaced a template by accident, or a macro that has outlived its callers.

Example:

report, err := repository.Audit()
if err != nil {
	return err
}

for _, override := range report.Overridden {
	log.Printf("%s comes from %s, replacing %v", override.Name, override.Standing, override.Replaced)
}

func (*Repository) Coverage

func (r *Repository) Coverage() *cover.Profile

Coverage returns the counters of the templates, or nil when the repository was not built with WithCoverage.

The templates of a repository are frozen, their counters are not. The counters record what a run reaches, and are the one part of a repository that changes.

func (*Repository) Documentation

func (r *Repository) Documentation() (reports.Documentation, error)

Documentation returns the structure of the repository and the documentation of its templates.

The analysis runs here rather than while the repository is built: comments are dropped from the trees a template executes, and the data a template reads is of no use to executing it. The repository holds its sources, so both are recovered by reading them again, which only a caller asking for documentation pays for.

The result is built on demand and shared with nobody, so a caller may hold it, walk it, or render it in whatever format.

func (*Repository) Dump

func (r *Repository) Dump(w io.Writer, opts ...reports.DumpOption) error

Dump writes the documentation of the repository, as markdown by default.

It is reports.Dump over what Repository.Documentation returns, which is the common way to ask. Use reports.Dump directly to lay out a document built once and rendered several ways.

func (*Repository) Get

func (r *Repository) Get(name string) (Template, error)

Get returns the template registered under a name.

A template is known by three strings, and this takes the third:

asset path   server/parameter.gotmpl   the file it was read from
address      server/parameter          where it was declared, never recased
name         serverParameter           what it answers to, and what Get takes

It reports an error when no source declares that name. The returned Template resolves the templates it refers to in this repository.

Example

Get takes a name.

package main

import (
	"os"

	repo "github.com/go-openapi/codegen/templates-repo"
)

// oneAsset declares two templates: the asset itself, and the "define" it holds.
func oneAsset() repo.Option {
	return repo.FromTemplate("server/parameter.gotmpl", []byte(
		`{{ define "bind" }}bound{{ end }}param[{{ template "bind" }}]`,
	))
}

func main() {
	repository, err := repo.New(oneAsset())
	if err != nil {
		panic(err)
	}

	tpl, err := repository.Get("serverParameter")
	if err != nil {
		panic(err)
	}

	_ = tpl.Execute(os.Stdout, nil)

}
Output:
param[bound]

func (*Repository) Has

func (r *Repository) Has(name string) bool

Has reports whether a name is declared in this repository.

func (*Repository) Lookup

func (r *Repository) Lookup(address string) (Template, error)

Lookup returns the template declared at an address.

An address is the path a template was declared at, slash-separated and never recased. This method takes one, Repository.Get takes a name. Use whichever a caller already holds.

The extension may be left on, so the asset path a template was read from addresses it too.

Example:

tpl, err := repository.Lookup("server/parameter")
Example

Lookup takes an address, and reaches the same template as Get does by name.

package main

import (
	"os"

	repo "github.com/go-openapi/codegen/templates-repo"
)

// oneAsset declares two templates: the asset itself, and the "define" it holds.
func oneAsset() repo.Option {
	return repo.FromTemplate("server/parameter.gotmpl", []byte(
		`{{ define "bind" }}bound{{ end }}param[{{ template "bind" }}]`,
	))
}

func main() {
	repository, err := repo.New(oneAsset())
	if err != nil {
		panic(err)
	}

	tpl, err := repository.Lookup("server/parameter/bind")
	if err != nil {
		panic(err)
	}

	_ = tpl.Execute(os.Stdout, nil)

}
Output:
bound

func (*Repository) MustGet

func (r *Repository) MustGet(name string) Template

MustGet returns the template registered under a name, and panics when there is none.

Use it for a name hardcoded in the program, and Repository.Get for one coming from the outside.

func (*Repository) MustLookup

func (r *Repository) MustLookup(address string) Template

MustLookup returns the template declared at an address, and panics when there is none.

Use it for an address hardcoded in the program, and Repository.Lookup for one coming from the outside.

func (*Repository) NameOf

func (r *Repository) NameOf(address string) string

NameOf returns the name a template declared at an address answers to.

It recases rather than looks up, so an address nothing declares still yields the name it would have. Ask Repository.Has whether that name is declared. The asset path addresses a template too, the extension being trimmed either way: NameOf("server/parameter.gotmpl") and NameOf("server/parameter") are both serverParameter.

Which extensions are trimmed is a setting of the repository, which is why this is a method. TemplateName answers the same question before a repository exists.

It reverses Repository.AddressOf, and it is idempotent on the names it produces, so a name may be handed back to it: NameOf("serverParameter") is serverParameter. A name an address never produced is not covered by that, an inner "define" being addressed under the asset that holds it.

Example

NameOf recases an address into the name it answers to. It computes, and does not look up: an address nothing declares still yields the name it would have.

package main

import (
	"fmt"

	repo "github.com/go-openapi/codegen/templates-repo"
)

// oneAsset declares two templates: the asset itself, and the "define" it holds.
func oneAsset() repo.Option {
	return repo.FromTemplate("server/parameter.gotmpl", []byte(
		`{{ define "bind" }}bound{{ end }}param[{{ template "bind" }}]`,
	))
}

func main() {
	repository, err := repo.New(oneAsset())
	if err != nil {
		panic(err)
	}

	fmt.Println(repository.NameOf("server/parameter/bind"))
	fmt.Println(repository.NameOf("server/parameter.gotmpl"))
	fmt.Println(repository.NameOf("nowhere/at/all"), repository.Has("nowhereAtAll"))

}
Output:
serverParameterBind
serverParameter
nowhereAtAll false
Example (Scoping)

Scoping takes names alone, where Lookup takes either identity.

package main

import (
	"fmt"

	repo "github.com/go-openapi/codegen/templates-repo"
)

// oneAsset declares two templates: the asset itself, and the "define" it holds.
func oneAsset() repo.Option {
	return repo.FromTemplate("server/parameter.gotmpl", []byte(
		`{{ define "bind" }}bound{{ end }}param[{{ template "bind" }}]`,
	))
}

func main() {
	repository, err := repo.New(oneAsset())
	if err != nil {
		panic(err)
	}

	// a caller holding an address converts it, then scopes
	scoped, err := repo.Clone(repository, repo.WithRoots(repository.NameOf("server/parameter")))
	if err != nil {
		panic(err)
	}

	for name := range scoped.Names() {
		fmt.Println(name)
	}

}
Output:
serverParameter
serverParameterBind

func (*Repository) Names

func (r *Repository) Names() iter.Seq[string]

Names iterates over the names declared in this repository, in lexical order.

Example

Names identify the templates a repository holds, and Get takes one.

package main

import (
	"fmt"

	repo "github.com/go-openapi/codegen/templates-repo"
)

// oneAsset declares two templates: the asset itself, and the "define" it holds.
func oneAsset() repo.Option {
	return repo.FromTemplate("server/parameter.gotmpl", []byte(
		`{{ define "bind" }}bound{{ end }}param[{{ template "bind" }}]`,
	))
}

func main() {
	repository, err := repo.New(oneAsset())
	if err != nil {
		panic(err)
	}

	for name := range repository.Names() {
		fmt.Println(name)
	}

}
Output:
serverParameter
serverParameterBind

func (*Repository) Roots

func (r *Repository) Roots() []string

Roots returns the names this repository is scoped to, in the order they were given to WithRoots.

It is empty when the repository holds every template it read, which is a repository built without WithRoots. This reports a scope rather than deciding anything with it: a caller adding a template to a repository it did not build wants WithExtraRoots, which does the right thing whether there is a scope to widen or not.

type SourceOption

type SourceOption func(sourceOptions) sourceOptions

SourceOption configures how one source is read.

Which directories to skip describes the file system being walked, not the repository. Skipping a directory of the assets one source ships leaves a directory of the same name fully readable in a template set someone else brings.

func Rebased

func Rebased(base string) SourceOption

Rebased mounts a source under a base, on top of wherever it already mounts.

Use it to publish templates without knowing where they land: the package exports sources, and the caller assembling them chooses the mount point of each.

Example:

// the package publishes this
func Sources(opts ...repo.SourceOption) repo.Option {
	return repo.FromFS(templates, "", opts...)
}

// and whoever assembles decides where it lands
repo.New(
	genmodels.Sources(repo.Rebased("models")),
	genclient.Sources(repo.Rebased("client")),
)

func SkipDirectories

func SkipDirectories(names ...string) SourceOption

SkipDirectories walks past the directories named, wherever they are in the tree read.

Directories are matched on their name, at any depth. Nothing is skipped by default. Use this to stack a set of alternate templates without reading all of it, on the source that holds them.

Example:

// the assets shipped, leaving the alternate sets to be stacked explicitly
repo.FromFS(assets, "", repo.SkipDirectories("contrib"))
Example

SkipDirectories matches a directory's own name, at any depth, and never a path or a template name.

package main

import (
	"fmt"
	"slices"
	"testing/fstest"

	repo "github.com/go-openapi/codegen/templates-repo"
)

func main() {
	assets := fstest.MapFS{
		"model.gotmpl":                   {Data: []byte("model")},
		"contrib/mine/model.gotmpl":      {Data: []byte("mine")},
		"server/legacy/contrib/x.gotmpl": {Data: []byte("legacy")},
	}

	skipped, err := repo.New(repo.FromFS(assets, "", repo.SkipDirectories("contrib")))
	if err != nil {
		panic(err)
	}

	fmt.Println(slices.Collect(skipped.Names()))

	// a path matches no directory name, so nothing is skipped
	byPath, err := repo.New(repo.FromFS(assets, "", repo.SkipDirectories("server/legacy")))
	if err != nil {
		panic(err)
	}

	fmt.Println(slices.Collect(byPath.Names()))

}
Output:
[model]
[contribMineModel model serverLegacyContribX]

type Template

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

Template is a compiled template, resolved against every other template of its Repository.

It is obtained from Repository.Get and cannot be built otherwise. The zero value reports an empty name and fails to execute.

A Template exposes execution and nothing else, on purpose: the methods of a text/template.Template that alter a template would alter what the repository serves, for every holder of it. There is no ExecuteTemplate either, since resolving a name is the job of Repository.Get.

Concurrency

A Template is immutable and may be executed concurrently. Concurrent executions sharing a single io.Writer interleave their output, as they do with text/template.Template.

func (Template) Execute

func (t Template) Execute(w io.Writer, data any) error

Execute applies the template to data and writes the result to w.

A template that refers to another one resolves it in the repository the template comes from. The zero Template reports an error.

func (Template) Name

func (t Template) Name() string

Name returns the name the template is registered under, or an empty string for the zero value.

Directories

Path Synopsis
internal
cover
Package cover measures which lines of a template a program reaches when it runs.
Package cover measures which lines of a template a program reaches when it runs.
document
Package document analyses template sources, rather than compiling them.
Package document analyses template sources, rather than compiling them.
Package reports holds what a templates repository says about itself.
Package reports holds what a templates repository says about itself.

Jump to

Keyboard shortcuts

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