preset

package
v0.5.2 Latest Latest
Warning

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

Go to latest
Published: Jul 3, 2026 License: MIT Imports: 18 Imported by: 0

README

preset

This package loads and applies presets — curated scaffolds that sit on top of an existing maestro project. A preset declares one or more services and ships a tree of source files the loader writes into them.

This is a contributor reference. End-user docs live in the top-level README under maestro preset.

What a preset is

A preset is a directory under internal/preset/presets/ containing:

<preset-name>/
├── preset.toml                      # manifest (required)
├── <overlay-dir>/                   # overlay tree for one service
│   └── …
└── <another-overlay-dir>/           # overlay tree for another service
    └── …

Presets ship inside the maestro binary via //go:embed (see embed.go). The loader takes any fs.FS, so tests hand it synthetic trees from testdata/.

The preset root holds the manifest plus one directory per service that ships an overlay. Each directory's name must match the dir field of a [[service]] block in preset.toml. Other root-level files (README, LICENSE, …) are silently ignored; an unknown directory is rejected as an authoring typo.

The manifest

preset.toml is the on-disk shape of preset.Manifest. The fields are:

name = "auth-jwt"                          # stable id used by `maestro preset apply <name>`
description = "JWT auth · …"               # one-liner shown in the picker

[[service]]                                # one or more service blocks
name = "PRESETSERVICE"                     # sentinel — resolved at apply time
dir  = "main"                              # overlay dir under the preset root
shape = "http"                             # http | grpc | worker | bare
db = "postgres"                            # optional; mutually exclusive with db_ref

[[service]]
name = "PRESETSERVICE-cleanup"
dir  = "cleanup"
shape = "worker"
db_ref = "PRESETSERVICE"                   # borrow another service's postgres sidecar

[[parameter]]                              # zero or more interactive prompts
key = "methods"
prompt = "Sign-in methods"
type = "multiselect"                       # v1 ships multiselect only
options  = ["password", "google"]
preselect = ["password"]                   # ticks at wizard open
min_pick = 1                               # floor before "continue" advances
Rules the manifest must satisfy

Manifest.Validate (preset.go) enforces these at load time so apply never trips on a malformed preset halfway through scaffolding:

  • name is required.
  • At least one [[service]] block, each with a non-empty name and a valid shape.
  • db and db_ref are mutually exclusive on a service.
  • A db_ref must point at another service declared in the same manifest, not at itself.
  • dir is optional, but if set must be a single path component (no separators, no ./..), not collide with the manifest filename, and unique across services.
  • Parameter keys are unique; preselect must be a subset of options; min_pick must be between 0 and len(options).

The first service in Services is the main service — the one PRESETSERVICE resolves to and the one the wizard collects DB mode + port for. Overlays are routed per-service via the dir field, not by service order.

Sentinel substitution

Preset files (both manifest strings and overlay file contents) contain sentinels: bare uppercase Go identifiers the loader rewrites at apply time. The convention is gonew-style — every sentinel is a single token so preset source files compile as ordinary Go in the author's editor.

Sentinel Value
PRESETSERVICE User-chosen service name (resolves [[service]].name and shows up inside file contents).
PRESETMODULE Full Go module path of the main service (e.g. github.com/owner/proj/auth).

Sentinels are substituted in both file paths and file contents (see SubstitutePath / Substitute in subst.go). Substitution is longest-token-first so a future PRESETSERVICEROLE wouldn't get half-replaced by PRESETSERVICE.

An empty Vars field leaves its sentinel in the output — resolveServices (apply.go) catches that as a caller bug rather than letting the empty string flow downstream. Add a new sentinel by:

  1. Declaring a constant in subst.go.
  2. Adding a field to Vars.
  3. Adding a pair to Vars.pairs().

Filename-suffix conditional inclusion

A preset that asks "which sign-in methods?" doesn't put the conditional files in separate directories — that breaks gopls because cross-file symbol references span Go packages. Instead, the filename suffix decides inclusion.

The rule (see walker.go):

  • A file whose basename ends in _<option>.<ext> is included only when <option> is among the user's picks for the parameter that declared it.
  • Every other file is included unconditionally.

For auth-jwt:

main/internal/routes/
├── routes.go            # always — Register() entry point, mounts slice
├── auth.go              # always — /auth/refresh, /auth/logout
├── auth_password.go     # only if methods includes "password"
└── auth_google.go       # only if methods includes "google"

All files in routes/ share a Go package, so cross-file references like mounts (defined in routes.go, appended-to from each handler file's init()) work both in author-mode and after apply.

If a filename suffix isn't a declared option (e.g. auth_helper.go when no parameter declares helper), the walker treats it as just a filename — the file is always included. This means authors can use the convention freely without pre-registering every suffix.

The init() + mounts pattern

Because the conditional files are gated by inclusion (not by build tags), routes.go can't reference symbols from auth_password.go directly — that file might not be on disk after apply. The pattern auth-jwt uses:

// routes/routes.go
var mounts []func(r *gin.Engine)

func Register(r *gin.Engine) {
    for _, fn := range mounts {
        fn(r)
    }
}
// routes/auth_password.go
func init() {
    mounts = append(mounts, func(r *gin.Engine) {
        auth := r.Group("/auth")
        auth.POST("/register", passwordRegister)
        // …
    })
}

server.go calls routes.Register(r) once and never has to know which conditional files are present; each conditional file appends a mount in init(). Pattern reusable for any preset where a central wiring point needs to compose with optional extras.

Apply

Apply (apply.go) is the orchestrator. From a preset name + main service name + MainDB (mode + port the dbwizard captured) + Selections (the parameter picks):

  1. Load and validate the manifest.
  2. Resolve every [[service]] block — substitute sentinels, merge MainDB into the first service.
  3. Pre-flight: validate every resolved ServiceConfig before any side effect lands. Catches caller bugs (e.g. main service missing DBMode when the manifest declares postgres) before half the scaffold is on disk.
  4. Scaffold each service in declaration order via the existing service.New(…).WithDB(…).Generate() pipeline. Order matters for db_ref — the target must already be in project.toml when the borrower scaffolds.
  5. Walk the preset root, substitute sentinels in path and contents, route each overlay file to its service's on-disk directory via the manifest's dir field.
  6. Re-run go mod tidy for every overlaid service so any new direct deps introduced by the overlay (the template's tidy already ran in step 4 and didn't see them) land in go.mod.

There's no rollback. Apply is expected to run on a clean working tree so a botched apply can be undone with git checkout.

Authoring a new preset

  1. Create internal/preset/presets/<name>/preset.toml with the manifest fields above.
  2. For each service that ships overlay source, set its dir field to a Go-idiomatic role name (main, cleanup, api, …) and put the overlay tree at <preset>/<dir>/. Use PRESETSERVICE / PRESETMODULE inside paths and file contents wherever the user's choices need to land.
  3. For conditional content, name files <base>_<option>.<ext> where <option> matches a manifest option.
  4. Add the preset to TestFS_List in embed_test.go so the embed inventory stays explicit.
  5. Add an end-to-end test in internal/e2e/preset_test.go — scaffold a project, call Apply, assert the result compiles. Skipping this means the loader can drift without a CI signal.
Go imports in overlay files

Overlay .go files compile inside the maestro module during preset authoring (gopls + go build ./...). That gives us two import shapes that both work:

  • Stdlib and module-level deps work normally. Any dep in maestro's go.mod is importable from an overlay file (e.g. github.com/gin-gonic/gin, go.uber.org/zap, github.com/golang-jwt/jwt/v5). Add a new dep with go get once and every preset can use it.

  • Cross-package imports work via the maestro-internal path. A handler in <preset>/main/internal/routes/auth.go can import <preset>/main/internal/db by writing the full maestro-internal path:

    import "github.com/Zagforge-Org/maestro/internal/preset/presets/auth-jwt/main/internal/db"
    

    That's a real path in author-mode, so gopls resolves it and go build ./... compiles it. At apply time, RewriteSourcePrefix (subst.go) rewrites the prefix to the user's actual service module path — e.g. github.com/owner/proj/auth/internal/db. The rewrite is scoped per overlay dir, so a file in main/ is rewritten against the main service's module and a file in cleanup/ is rewritten against the cleanup service's.

    The prefix is PresetSourceRoot + "/" + presetName + "/" + overlayDir. If maestro itself ever gets renamed, update PresetSourceRoot in subst.go in lockstep.

Run go test ./internal/preset/... to exercise the manifest validator, walker, and substitution. Run go test -tags=e2e ./internal/e2e/... for the compile-the-generated-service check.

Documentation

Overview

Package preset loads and applies curated scaffolds ("presets") on top of an existing maestro project. A preset bundles a TOML manifest, a set of service declarations to scaffold, and a tree of source files that the loader substitutes and writes into those services.

The package is structured around three building blocks:

  • Manifest types (this file): preset.toml shape — services, parameters.
  • Substitution (subst.go): sentinel-replacement engine over file bytes.
  • Loader (loader.go): walks an fs.FS, validates a manifest, produces a Loaded preset the orchestrator can iterate over.

Discovery is via //go:embed so presets ship inside the binary; the loader takes an fs.FS so tests can hand it synthetic trees.

Index

Constants

View Source
const (
	// SentinelService stands in for the user-chosen service name.
	SentinelService = "PRESETSERVICE"

	// SentinelModule stands in for the full Go module path of the
	// generated service, e.g. github.com/owner/proj/auth.
	SentinelModule = "PRESETMODULE"

	// PresetSourceRoot is the maestro-module path prefix every preset
	// overlay file lives under. Overlay code can write its sibling-
	// package imports using real paths under this prefix — gopls and
	// `go build ./...` resolve them in author-mode because they're
	// honest paths inside the maestro module. Apply rewrites the
	// prefix at apply time to the user's actual service module path.
	//
	// If maestro itself ever gets renamed, this constant has to be
	// updated in lockstep — the prefix is the load-bearing string
	// that makes cross-package imports work in both worlds.
	PresetSourceRoot = "github.com/Zagforge-Org/maestro/internal/preset/presets"

	// CommongoSourceRoot is the maestro-module path prefix for the
	// shared common/go starter packages. Overlay code that needs to
	// reach a common/go package (ginx, logger, errs, …) imports it
	// from this path — gopls resolves it in author-mode because the
	// real Go files live there. Apply rewrites the prefix at apply
	// time to the user's common/go module path, which is the project
	// namespace root (github.com/<owner>/<project>). Aliased to the
	// commongo package's own constant so the prefix has one definition.
	CommongoSourceRoot = commongo.ImportRoot
)

Sentinels are the named tokens preset authors embed in source files and manifest strings; the loader replaces each one with the corresponding runtime value before writing the file to disk.

Following the gonew convention, every sentinel is a single uppercase Go identifier so template source files compile as valid Go in the preset author's editor (gopls treats `package PRESETSERVICE` as a normal package named PRESETSERVICE).

Variables

This section is empty.

Functions

func Apply

func Apply(
	ctx *project.Context,
	fsys fs.FS,
	presetName string,
	mainService string,
	mainDB MainDB,
	picks Selections,
) error

Apply scaffolds every [service] block in the named preset's manifest into ctx's project, then overlays the preset's template files onto each service the manifest's files/ tree targets.

mainService is the user-chosen name that PRESETSERVICE resolves to. mainDB is the runtime database choice the dbwizard captured for that service (engine + shape come from the manifest; mode + port from the wizard). picks is the parameter selection map the wizard collected.

The function does no rollback — partial failure leaves whatever was written on disk. Callers are expected to run inside a clean working tree so a `git checkout` reverts a botched apply.

func FS

func FS() fs.FS

FS returns the embedded preset filesystem rooted at the presets/ directory — i.e. fs.Sub of presetsFS so callers see preset directories at the root, matching the shape Walk and LoadManifest already expect.

func List

func List(fsys fs.FS) ([]string, error)

List returns the names of every preset directory in fsys that has a readable, validating manifest. Broken manifests are silently skipped — the picker only ever shows usable presets. Use LoadManifest if you need the error for a specific name.

func RewriteCommongoPrefix

func RewriteCommongoPrefix(content, projectModule string) string

RewriteCommongoPrefix rewrites every occurrence of the maestro-internal common/go source prefix with the user's project module path (which is the common/go module's path — common/go owns the namespace root). An overlay file can write

import "github.com/Zagforge-Org/maestro/internal/scaffold/commongo/ginx"

and Apply turns it into

import "github.com/<owner>/<project>/ginx"

at apply time. The substitution is a literal ReplaceAll for the same reason the per-service one is: the prefix is unique enough that incidental matches outside import lines are vanishingly unlikely.

func RewriteSourcePrefix

func RewriteSourcePrefix(content, presetName, overlayDir, userModule string) string

RewriteSourcePrefix rewrites every occurrence of the preset's maestro-internal source prefix with the user's service module path. This is what lets an overlay file write a real, gopls-resolvable import like

github.com/Zagforge-Org/maestro/internal/preset/presets/<preset>/<dir>/internal/db

and have Apply rewrite it to

github.com/<owner>/<project>/<service>/internal/db

at apply time. The substitution is intentionally a literal-string ReplaceAll: the maestro-internal prefix is unique enough that any match outside an import line (a comment, a doc string) is almost certainly intentional too.

func Substitute

func Substitute(content string, vars Vars) string

Substitute returns content with every sentinel mention replaced by the matching field in vars. Files without any sentinels pass through byte-for-byte. Empty vars fields don't replace — leaving a sentinel in the output is loud at the call site and easier to debug than a silent blank-out.

func SubstitutePath

func SubstitutePath(p string, vars Vars) string

SubstitutePath is the same as Substitute but applied to a file path. Kept as a distinct helper so a future change to path-only escaping rules (e.g. disallowing path separators in a substituted name) has a single place to land.

Types

type File

type File struct {
	Dir        string
	TargetPath string
	Bytes      []byte
}

File is one preset source file the walker has decided should be included for the current parameter selection.

Dir is the overlay directory under the preset's root that this file came from — matches a [service].dir value in the manifest. Apply uses Dir to look up which resolved on-disk service the file should be written into.

TargetPath is the path the file should land at relative to the target service's root. Sentinels in both TargetPath and Bytes are unresolved — the caller substitutes after walk completes.

func Walk

func Walk(fsys fs.FS, presetName string, m Manifest, picks Selections) ([]File, error)

Walk lists every file under presets/<name>/<dir>/ in fsys that should be applied given the parameter picks in picks and the parameter schema in m.Parameters.

Layout: the preset root holds preset.toml plus one directory per service that ships an overlay. Each directory's name must match the dir field of some [service] block. Stray top-level files (e.g. a preset-level README.md) are ignored; stray top-level directories are rejected as authoring typos.

Inclusion rule (filename-suffix convention):

  • A file whose basename ends in `_<option>.<ext>` — where <option> matches one of the declared options for some parameter — is included only when that option is in picks[<that parameter's key>].
  • Every other file is included unconditionally.

Keeping every preset file in the same physical directory it'll occupy in the generated service means cross-file symbol references inside a single Go package work both in the preset (gopls + `go build`) and after apply.

type MainDB

type MainDB struct {
	DBMode projectcfg.DBMode
	Port   int
	// Redis is the docker-vs-live choice for the main service's Redis
	// dependency, applied only when the manifest's main [[service]] sets
	// redis = true. Empty leaves the service without Redis.
	Redis projectcfg.RedisMode
}

MainDB carries the runtime answers the dbwizard captured for the preset's first (main) service. The manifest declares the engine and shape; the wizard supplies the mode, port, and (for live mode) the URL the user already pinged.

type Manifest

type Manifest struct {
	// Name is the preset's stable identifier — used by `maestro preset
	// apply <name>`. Must be non-empty.
	Name string `toml:"name"`

	// Description is the one-liner shown in the picker. Optional.
	Description string `toml:"description"`

	// Services lists the services this preset scaffolds. At least one is
	// required. Each entry hands off to the existing service.Generate()
	// pipeline; the loader resolves sentinels in fields like Name and
	// DBRef before generation runs.
	Services []ServiceSpec `toml:"service"`

	// Parameters lists the interactive prompts the wizard renders for
	// this preset. Keys feed into the file-walker's conditional inclusion
	// rules (files/<key>/<value>/ is included only when the user picks
	// value for key). May be empty for presets that take no parameters.
	Parameters []Parameter `toml:"parameter"`

	// CommonGo lists the opt-in common/go capability libraries this preset
	// depends on (e.g. "jwtx"). Apply delivers them into the project's
	// common/go so the preset's overlay can import them — and so they're
	// then reusable by hand in any other service. Each entry must be a
	// known capability lib. May be empty.
	CommonGo []string `toml:"commongo"`
}

Manifest is the on-disk shape of a preset.toml. The loader parses one of these per preset, validates it, and pairs it with the preset's files/ tree to produce a Loaded preset.

func LoadManifest

func LoadManifest(fsys fs.FS, name string) (Manifest, error)

LoadManifest reads and validates the manifest for the preset at name inside fsys (typically the embedded presets/ FS). The returned Manifest is safe to hand to the wizard and the file walker.

func (Manifest) Validate

func (m Manifest) Validate() error

Validate enforces the rules a manifest must satisfy before the loader hands it to anything else. The intent is to catch every authoring mistake at load time, so the apply path never trips on a malformed preset halfway through scaffolding.

type Parameter

type Parameter struct {
	Key       string        `toml:"key"`
	Prompt    string        `toml:"prompt"`
	Type      ParameterType `toml:"type"`
	Options   []string      `toml:"options"`
	Preselect []string      `toml:"preselect,omitempty"`
	MinPick   int           `toml:"min_pick,omitempty"`
}

Parameter is one [[parameter]] block. It declares an interactive prompt and the constraints the wizard enforces before the user can advance. For multiselect, Preselect is the set of options ticked when the wizard opens (pure UI state) and MinPick is the floor on how many must be ticked before "continue" advances.

func (Parameter) Validate

func (p Parameter) Validate() error

Validate enforces the per-parameter rules. Pulled out of Manifest so the wizard can re-validate a parameter against a candidate selection before letting the user advance.

type ParameterType

type ParameterType string

ParameterType enumerates the prompt shapes the wizard supports. v1 ships multiselect only; the type is open for future single-select, text, and bool prompts.

const (
	ParameterMultiselect ParameterType = "multiselect"
)

type Selections

type Selections map[string][]string

Selections maps parameter key → the option values the user picked. Used by Walk to decide which conditional files to include.

type ServiceSpec

type ServiceSpec struct {
	Name  string              `toml:"name"`
	Dir   string              `toml:"dir,omitempty"`
	Shape config.ServiceShape `toml:"shape"`
	DB    config.DBEngine     `toml:"db,omitempty"`
	DBRef string              `toml:"db_ref,omitempty"`
	// Redis declares that this service uses Redis; the apply flow then
	// captures a docker-vs-live choice for it (the gateway uses this for
	// rate-limit state). Independent of DB.
	Redis bool `toml:"redis,omitempty"`
}

ServiceSpec is one [service] block. The loader substitutes sentinels in Name and DBRef before passing the resulting config.ServiceConfig to the service generator.

Dir is the top-level directory under the preset's root that holds this service's overlay tree. The convention is a Go-idiomatic role name ("main", "cleanup", "api"), decoupled from the sentinel-laden Name so the on-disk tree is pleasant to navigate. Optional — a service with no Dir scaffolds from the maestro template alone, no overlay.

type Vars

type Vars struct {
	Service string
	Module  string
}

Vars carries the runtime values that get spliced into a preset file or manifest string. New sentinels go here as fields, get a constant above, and get listed in pairs(). Keep the set small and named for roles, not for the values they happen to hold today.

Directories

Path Synopsis
presets
api-gateway/main/internal/auth
Package auth verifies the access tokens where the gateway upstream services are behind.
Package auth verifies the access tokens where the gateway upstream services are behind.
api-gateway/main/internal/gateway
Package gateway is the reverse-proxy core of api gateway preset which routes inbound requests to upstream services by longest-matching path prefix.
Package gateway is the reverse-proxy core of api gateway preset which routes inbound requests to upstream services by longest-matching path prefix.
api-gateway/main/internal/server
Package server wires the api-gateway: request logging, local health probes, and the reverse proxy as the catch-all for everything else.
Package server wires the api-gateway: request logging, local health probes, and the reverse proxy as the catch-all for everything else.
auth-jwt/cleanup/internal/worker
Package worker is the cleanup target for the auth-jwt preset.
Package worker is the cleanup target for the auth-jwt preset.
auth-jwt/main/internal/auth
Authenticated symmetric encryption for the auth-jwt preset.
Authenticated symmetric encryption for the auth-jwt preset.
auth-jwt/main/internal/db
Package db owns the database connection for this service.
Package db owns the database connection for this service.
auth-jwt/main/internal/email
Package email builds the typed mailer.Message values the auth service sends.
Package email builds the typed mailer.Message values the auth service sends.
auth-jwt/main/internal/requests
Package requests holds the typed JSON bodies every auth route expects, with go-playground/validator constraints declared inline via Gin's `binding:` tags.
Package requests holds the typed JSON bodies every auth route expects, with go-playground/validator constraints declared inline via Gin's `binding:` tags.
auth-jwt/main/internal/responses
Package responses holds the typed JSON bodies the auth routes return on success.
Package responses holds the typed JSON bodies the auth routes return on success.
auth-jwt/main/internal/routes
Always-on base of the auth-jwt preset's API.
Always-on base of the auth-jwt preset's API.
auth-jwt/main/internal/server
Package server wires the auth-jwt service: opens the database, builds the auth service container (JWT + OTP configs + sqlc queries), attaches middleware + base routes, and registers the feature routes collected in internal/routes.
Package server wires the auth-jwt service: opens the database, builds the auth service container (JWT + OTP configs + sqlc queries), attaches middleware + base routes, and registers the feature routes collected in internal/routes.
auth-jwt/main/internal/services
Always-on auth services for the auth-jwt preset: refresh and logout, used by every sign-in method.
Always-on auth services for the auth-jwt preset: refresh and logout, used by every sign-in method.

Jump to

Keyboard shortcuts

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