builder

package
v1.7.0 Latest Latest
Warning

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

Go to latest
Published: Oct 20, 2026 License: MIT Imports: 29 Imported by: 0

README

builder

builder generates the doors of an application on top of inject: a handler declares with an annotation which door serves it (a REST route on echo, a gRPC service, an MCP tool, the event function or a job that runs once), and the generator writes one fx module per door, a Modules() per app and the main of every app. @Middleware on a constructor wraps every method of the interface it provides in extra/middleware plugins. One command runs inject and builder in one pass; the application writes only handlers, plain services and annotations:

go generate ./...
go run ./cmd/api

Annotations

@REST: a route on echo
package createorder

// New provides the handler.
// @Provide(index=0)
func New(svc *order.Service) *Handler { return &Handler{svc: svc} }

// Create creates an order.
// @REST(method=POST, path=/orders)
func (h *Handler) Create(c echo.Context) error { ... }
package probe

// Live answers the liveness probe.
// @REST(method=GET, path=/live, server=admin)
// @REST(method=HEAD, path=/live, server=admin)
func Live(c echo.Context) error { ... }
Key Required Meaning
method yes GET, POST, PUT, PATCH, DELETE, HEAD or OPTIONS
path yes echo path (/orders/:id)
server no server name, default rest; each server is its own echo instance
app no application (binary) that hosts the route, default api
  • The handler is func(echo.Context) error, a function or a method.
  • On a method, the receiver (*T or T) must be provided by a default @Provide: the door gets it from the graph.
  • @REST is repeatable: each line is one route of the same handler.
@GRPC: a service on the gRPC server
package healthcheck

import healthpb "google.golang.org/grpc/health/grpc_health_v1"

// NewServer provides the health service.
// @Provide(index=0)
// @GRPC(register=healthpb.RegisterHealthServer)
func NewServer() *health.Server { ... }
Key Required Meaning
register yes the RegisterXServer function, written as the file writes it (pkg.RegisterXServer with pkg imported by the file, or a function of the same package)
server no server name, default grpc
app no application, default api

@GRPC goes on a constructor with exactly one @Provide(index=N) without name or group; the door calls register(srv.ServiceRegistrar(), value).

@MCP: a tool on the MCP server
package stocktool

// New provides the handler.
// @Provide(index=0)
func New(stock stock.Service) *Handler { return &Handler{stock: stock} }

// CheckStock tells whether an item can be ordered.
// @MCP
func (h *Handler) CheckStock(ctx context.Context, req *mcp.CallToolRequest, in Input) (*mcp.CallToolResult, Output, error) { ... }
Key Required Meaning
name no tool name (letters, digits, _, -, ., up to 128), default the handler name in snake case (check_stock)
server no server name, default mcp
app no application, default api
  • The handler is a go-sdk mcp.ToolHandlerFor[In, Out]: func(context.Context, *mcp.CallToolRequest, In) (*mcp.CallToolResult, Out, error). The door calls mcp.AddTool, which infers the input and output schemas from In and Out and fills the result from Out, so the handler may return a nil *mcp.CallToolResult. A method needs a provided receiver, as in @REST.
  • The tool description is the doc comment of the handler, without its leading name: Tells whether an item can be ordered.
  • The handler lives in the app layer and calls the service; the service never imports mcp.
@Function: the event handler
package ordercreated

// Handle records the order of the event.
// @Function(adapter=[cloudevents, nats], app=worker, middlewares=[recovery, logger])
func (h *Handler) Handle(ctx context.Context, in cloudevents.Event) (*cloudevents.Event, error) { ... }
Key Required Meaning
adapter yes azure (Azure Functions custom handler), cloudevents, cenats (cloudevents over NATS), kafka, kafkago (kafka on segmentio/kafka-go), lambda (AWS Lambda), nats or pubsub; or a list [a, b] of them, chosen by config
app no application, default api
middlewares no [a, b], the outermost first: ignore_errors, logger, opentelemetry, prometheus, publisher, recovery
  • The handler is a function.Handler[T]: func(context.Context, cloudevents.Event) (T, error); T is its first result. A method needs a provided receiver, as in @REST.
  • An app runs at most one @Function. The door includes functionfx.Module[T] and the adapter module under fx/modules/bootstrap/function/adapter/contrib (pubsub includes v1 and v2; boost.bootstrap.function.adapter.pubsub.version, v2 by default, turns on one).
  • The nats, cenats and pubsub (v2) adapters read with the app's default client: the *nats.Conn or *pubsub.Client of the app's unnamed @Provide, or else the one inject generates (DefaultConnModule(), DefaultClientModule() under gen/client/...), the same one the publisher and an @Inject without name get. The door wires the adapter's ModuleWithoutClient, so a function that reads from a broker and publishes to it boots with one client.
  • client=orders reads with the named client orders instead: the app's @Provide(name=orders) of that type, or else the one inject generates (OrdersConnModule(), keys boost.factory.nats.clients.orders.*), the same one @Inject(name=orders) and the publisher orders get. The door wires the adapter's ModuleWithNamedClient("orders"). The name follows the client names ([a-z][a-zA-Z0-9]*), and the adapters must include one that reads with a client (nats, cenats, pubsub), else NotValid. The pubsub v1 adapter keeps its own client.
  • With a list, adapter=[pubsub, nats], config chooses the adapter: <app>.function.adapter (env <APP>_FUNCTION_ADAPTER; see App names for a kebab-case app), default the first name. The door includes only the chosen adapter module, so the others and their clients are never built; an unknown name fails the boot with NotValid naming the key, the value and the valid names. The adapters' own keys (.enabled, the pubsub version) stay as they are. The app runs the chosen adapter with no CLI argument and no boost.bootstrap.function.default.
  • The pubsub adapter on v1 (boost.bootstrap.function.adapter.pubsub.version=v1) reads with its own v1 *pubsub.Client, built from the boost.factory.gcp.pubsub keys; the app's default client is v2.
  • Each middleware is the fx module under fx/modules/bootstrap/function/middleware of the bootstrap/function/middleware of that name; the door hands them to functionfx.Module under functionfx.MiddlewaresName. One turned off in config (boost.bootstrap.function.middleware.<name>.enabled) is skipped. publisher needs the app's unnamed publisher.Driver: the door builds the @Provide constructor that returns it, and generation fails with NotFound when there is none (fx would skip the middleware silently).
@Job: an app that runs once and exits
package stockaudit

// Run checks the stock of the configured items.
// @Job(app=stock-audit)
func (a *Auditor) Run(ctx context.Context) error { ... }
Key Required Meaning
app yes application; a job is a binary of its own
  • The handler is a job.Handler: func(context.Context) error, a function or a method. A method needs a provided receiver, as in @REST; @Middleware on the services it receives applies as in any door.
  • The app is the shape of a Kubernetes CronJob: boost.Start(), the graph, the handler once, then the app stops. The door includes jobfx.Module(), which runs the handler on start.
  • Nil exits 0. An error is logged at ERROR (job failed: <error>) and exits 1.
  • SIGTERM cancels the handler's context and the app waits for it to return, up to the fx stop timeout (15s by default). Its error (ctx.Err() when it gave up) fails the stop and the app exits 1; nil exits 0; a handler still running at the timeout exits 1.
  • An app runs one @Job and nothing else: a server door or a @Function in the same app fails the generation.
@Middleware: plugins around every method of a service
package stock

// New provides the stock service.
// @Provide(index=0)
// @Middleware(log, prometheus)
func New() Service { return &memory{} }
  • Plugin names, at least one, no duplicates, the first is the outermost:
Name Plugin
log plugins/local/wrapper/log
hystrix plugins/contrib/afex/hystrix-go/v0
datadog plugins/contrib/datadog/dd-trace-go/v1
prometheus plugins/contrib/prometheus/client_golang/v1
  • It goes on a function with one @Provide (with or without name, not group) whose provided result is an exported, non-generic interface. Every method (embedded ones included) takes context.Context first and returns error, (R, error) or R, and uses only exported types.
  • builder generates a decorator of the interface and applies it with a root fx.Decorate, so every consumer in the app receives it, whatever module it lives in. Each method runs next in the wrapper that fits its results (ErrorWrapper, AnyErrorWrapper[R], AnyWrapper[R]); the service's methods stay plain.
  • The wrapper of a method is named <package>.<interface>.<method>, lowercased (stock.service.available): it is the hystrix command, the span and metric name and the config path of each plugin. Config only turns one off: boost.factory.local.extra.middleware.plugins.<plugin>.<name>.enabled (default true).
  • An app whose doors never build the constructor gets neither the decorator nor its plugins.
  • On an implementation of a set (see inject -- Default implementation), the decorator applies only when config chooses that implementation, so every implementation may carry its own @Middleware of the same interface.
@Server: the plugins of a server
// Package rest holds what the REST server of the app is built with.
// @Server(type=rest, plugins=[recover, requestId, logger, errorHandler, health, prometheus])
package rest
  • It goes in a package doc, above the package clause (a doc.go): everything of a server lives in its own package, internal/pkg/server/<transport>/. On a declaration it fails.
  • type (required) is the server type, one of rest, grpc, mcp and cloudevents (the http adapter of @Function). Each type has its own plugins; a new server type is a new row of this table, not a new annotation.
  • name is the server (default: rest, grpc or mcp, the type's shipped server; any other is a named server, see Servers) and app its app (default api). cloudevents takes no name: its plugins go to the @Function of app when it runs with that adapter.
  • plugins lists the plugins in order, none twice; plugins=[] builds the server without any. Without plugins, and for a server with no @Server at all, it gets the default set (the plugins marked below).
  • The door imports each plugin and provides the []Plugin of the server (the unnamed one of the shipped module, or the one named after the server), so the app writes no plugin code. Config only turns one off: <plugins root>.<name>.enabled (default true), as boost.factory.echo.plugins.prometheus.enabled.
rest Plugin (boost.factory.echo.plugins.<name>) Default
recover native/recover yes
requestId native/requestid yes
logger local/wrapper/log yes
errorHandler extra/error_handler yes
health local/extra/health yes
prometheus contrib/prometheus/client_golang/v1
otel contrib/go.opentelemetry.io/contrib/v0
datadog contrib/datadog/dd-trace-go/v1
pprof contrib/hiko1129/echo-pprof/v1
swagger contrib/swaggo/echo-swagger/v1
cors native/cors
gzip native/gzip
bodyLimit native/bodylimit
bodydump native/bodydump
semaphore extra/semaphore
status local/model/restresponse
multiServer local/extra/multi-server
bytedanceSonic contrib/bytedance/sonic/v1
goccyGoJson contrib/goccy/go-json/v0
grpc Plugin (boost.factory.grpc.server.plugins.<name>) Default
log local/wrapper/log yes
prometheus contrib/prometheus/client_golang/v1
datadog contrib/datadog/dd-trace-go/v1
compressor native/compressor
mcp Plugin (boost.factory.mcp.server.plugins.<name>) Default
log local/wrapper/log yes
otel contrib/go.opentelemetry.io/otel/v1
cloudevents Plugin (boost.bootstrap.function.adapter.cloudevents.http.plugins.<name>) Default
otel contrib/go.opentelemetry.io/contrib/v0
  • A @Provide []Plugin still builds a server (see Servers): that server gets no default set, and a @Server for it too fails.
App names

An app name is kebab case, as deploys are named: it matches ^[a-z][a-z0-9]*(-[a-z0-9]+)*$, is not middleware and, without its hyphens, is not a Go keyword or main. It maps to:

App cmd/ and gen/builder/ directory, binary Go package Config path element Env prefix
api api api api API_
orders-updater orders-updater ordersupdater ordersUpdater ORDERS__UPDATER_

The config element is camelCase because the env loader reads a __ as the hyphen of a camelCase word: ORDERS__UPDATER_FUNCTION_ADAPTER=nats sets ordersUpdater.function.adapter, ORDERS__UPDATER_SERVER_ADMIN_PORT sets ordersUpdater.server.admin.port. The generated main imports the package under the alias app.

A server name matches ^[a-z][a-z0-9]*$ (it becomes a file, a function and a config path element) and is not function.

Generating

//go:generate go run github.com/xgodev/boost/builder/cmd/builder --boost.builder.output=gen
Key Env Default Description
boost.builder.path BOOST_BUILDER_PATH . directory scanned for the builder and inject annotations
boost.builder.output BOOST_BUILDER_OUTPUT gen output directory; must be inside the Go module
boost.builder.cmd BOOST_BUILDER_CMD cmd directory that receives <app>/main.go for every app; must be inside the Go module
cmd/<app>/main.go                    # boost.Start() and fxfact.NewApp(<app>.Modules()...).Run()
gen/
  module.go                          # inject root module
  internal/.../*_module.go           # inject modules, one per @Provide/@Invoke/@Config, at the package's path in the project
  client/<client type import path>/<name>_<type>.go # a factory client asked by @Inject (see inject)
  builder/<app>/modules.go           # func Modules() []fx.Option
  builder/<app>/<server>_module.go   # one REST, gRPC or MCP server and its routes/services/tools
  builder/<app>/function_module.go   # the @Function handler and its adapter
  builder/<app>/job_module.go        # the @Job handler, run once
  builder/middleware/<package path>/<func>_middleware.go # Decorate<Func>(): the @Middleware decorator

gen/ mirrors the project: a package in internal/app/api/rest gets its module in gen/internal/app/api/rest, so apps whose packages live under internal/ (project layout) compile and gen/ reads like the project. Below gen/builder/middleware/ and gen/implementation/ the leading internal is written internal_, which Go needs there (see inject).

Modules() returns the doors sorted by name, then the @Middleware decorators the doors reach and, when the app has servers, multiserverfx.Module(), which starts all of them. Each door includes the inject modules of its handlers, so only what the app serves is built. Generated files start with // Code generated by builder; DO NOT EDIT.; every run removes the ones no longer produced, including the main of a removed app.

Settings are generated too: a package declares a struct marked @Config whose fields carry default and desc tags, and inject registers one config key per field and provides *Struct to the constructors that receive it (see inject). A door that reaches such a constructor includes the config module, so the package never imports wrapper/config. Factory clients too: @Inject(index=0, name=quote, plugins=[otel]) on a *resty.Client (redis, mongo, NATS, Pub/Sub, gocql, AWS, *sql.DB through godror or pgx) parameter makes inject build that client with those plugins and its own config under boost.factory.resty.clients.quote (see inject), so the app writes no factory module.

builder owns the main of every app: a cmd/<app>/main.go without that header fails the generation instead of being overwritten. Other files under cmd/ (tests next to the main, other commands) are kept.

Servers
  • rest, grpc and mcp use the shipped echo, gRPC and MCP fx modules, configured under boost.factory.echo, boost.factory.grpc.server and boost.factory.mcp.server.
  • Any other name is a separate server configured under <app>.server.<name> (api.server.admin.port): give it its own port.
  • A server name serves one kind of door: REST, gRPC or MCP.
  • Plugins come from the server's @Server or, without one, from its default set. A plugin of your own comes from @Provide instead: a []boostecho.Plugin (REST) or []server.Plugin (gRPC, grpc/v1/server; MCP, go-sdk/v1/server) provided with name=<server> reaches that server, rest, grpc and mcp included; one provided without name reaches rest, grpc or mcp too. The door includes the provider's inject module, so the generated main needs nothing else:
// Plugins tags the admin responses.
// @Provide(name=admin, index=0)
func Plugins() []boostecho.Plugin { ... }

Validation

The command fails, without touching the output directory, when:

  • method is missing or unknown, or path is missing;
  • the same method+path is declared twice in one server, a gRPC register twice, or an MCP tool name twice;
  • an MCP tool name has a character other than letters, digits, _, - and ., or more than 128;
  • the handler signature does not match the door;
  • a method handler has no @Provide of its receiver;
  • the receiver of a handler method, or a @GRPC constructor, is one of the implementations of a set (@Default): a door is built from one constructor, so it takes the chosen value through a type of its own;
  • @GRPC has no single default @Provide, or register does not resolve to an exported function;
  • an app has two @Function, or adapter is missing or unknown, or its list is empty or repeats a name;
  • @Job has no app, an app has two @Job, or a job app also has a server door or a @Function;
  • middlewares is not written as [a, b], or lists an empty, unknown or repeated name;
  • a server is used by two kinds of door;
  • rest, grpc or mcp gets both a []Plugin with name=<server> and one without name: provide one of them;
  • @Server is not in a package doc, has no type, an unknown one, an unknown or repeated plugin, name on cloudevents, names a server of another type or one no app has (or an app whose @Function does not run cloudevents), is repeated for a server, or meets a @Provide []Plugin of the same server;
  • an app or server name is invalid, or an annotation has an unknown key;
  • the handler is unexported or in package main;
  • @Middleware lists no plugin, an unknown one or one twice, is repeated, is on a struct, is not on a single @Provide, decorates a group, a type that is not an exported non-generic interface with methods, or a method that breaks the rules above, or two constructors decorate the same interface (unless they are implementations of the same set);
  • cmd/<app>/main.go exists and was not generated by builder;
  • any inject validation fails (see inject).

Library

// what cmd/builder does: gen/ under the module of src, mains under cmd
err := builder.Generate(ctx, src, "gen", "cmd") // empty cmd: no main

// the files Generate would write, change or remove, relative to src
stale, err := builder.Stale(ctx, src, "gen", "cmd")

Stale writes nothing, which lets a test of the app check that gen/ and the mains are up to date:

func TestGeneratedCodeIsUpToDate(t *testing.T) {
	stale, err := builder.Stale(context.Background(), ".", "gen", "cmd")
	require.NoError(t, err)
	assert.Empty(t, stale, "run go generate")
}

The steps underneath, for a tool of its own:

modulePath, moduleDir, _ := inject.FindModule(".")
entries, _ := builder.CollectEntries(".")
g, _ := inject.NewGraphFromEntries(ctx, entries)
spec, _ := builder.NewSpec(entries, g, builder.LoadPackages(".")) // loads the interfaces @Middleware decorates
opts := builder.Options{
	GeneratorOptions: inject.GeneratorOptions{ModulePath: modulePath, ModuleDir: moduleDir, OutputDir: outputDir},
	CmdDir:           cmdDir, // empty: no main
}
doors := builder.NewGenerator(spec, opts)
files, _ := doors.Render(ctx)    // builder/<app>/... and builder/middleware/... relative to OutputDir
mains, _ := doors.RenderCmd(ctx) // <app>/main.go relative to CmdDir
// or doors.Generate(ctx) to write both

Example

examples/simple has three apps:

  • api: REST POST /orders and GET /orders/:id on rest, GET/HEAD /live on admin (built with the X-Served-By: admin plugin of probe.Plugins), the gRPC health service on grpc and the check_stock MCP tool of stocktool on mcp; POST /orders and check_stock check stock.Service, implemented by stock/memory (@Default, with @Middleware(log, prometheus)) and stock/unlimited, chosen by builder.examples.simple.stock.implementation (BUILDER_EXAMPLES_SIMPLE_STOCK_IMPLEMENTATION); stock/memory reads its sold-out items from its @Config Options (builder.examples.simple.stock.memory.soldOut, env BUILDER_EXAMPLES_SIMPLE_STOCK_MEMORY_SOLD__OUT, default sold-out);
  • order-worker: the ordercreated function on the cloudevents, the nats or the pubsub adapter, chosen by orderWorker.function.adapter (ORDER__WORKER_FUNCTION_ADAPTER, default cloudevents), behind the recovery and logger middlewares; its handler injects the publisher with drivers=[nats, pubsub], so the worker links no Kafka client. Its package is gen/builder/order-worker, package orderworker;
  • stock-audit: the @Job of stockaudit, which asks stock.Service (the decorated memory one by default) for the items of its @Config Options (builder.examples.simple.stockaudit.items, default book, pen, sold-out), logs the sold-out ones and exits.

cmd/api/main.go, cmd/order-worker/main.go and cmd/stock-audit/main.go are generated. Its tests call the routes, the gRPC service and the MCP tool of the generated app, check that the handler receives the stock decorator and that prometheus counted its call, boot the api with unlimited (wired without the decorator), with an unknown name (the boot fails) and with SOLD__OUT=book,pen (the env replaces the default list), run the function handler, boot the worker with each adapter (nats on an embedded server) and with an unknown one (the boot fails), run the generated main with only ORDER__WORKER_FUNCTION_ADAPTER=nats until it records an order published on NATS and then send it SIGTERM (it exits 0 within a second), run it with only ORDER__WORKER_FUNCTION_ADAPTER=pubsub, v2 and v1, against a pstest fake until it records a CloudEvent and a raw message, stays up and exits 0 on SIGTERM, and with no subscription (the boot fails naming it), build the worker and check with go version -m that it has no confluent-kafka-go, boot it with the kafka driver (the boot fails listing nats, noop, pubsub), run the generated main of stock-audit on the memory stock (it exits 0 by itself), on the remote stock against a warehouse that answers 500 (it logs job failed and exits 1) and against one that never answers, sending SIGTERM while the job waits (the job context is cancelled and it exits 1 within seconds), and fail when gen/ or a main is stale.

Non-goals

  • A YAML spec: annotations are the only source, and server settings stay in boost config.
  • Generating OpenAPI or proto from annotations: the spec in api/ is the source, and code may come from it, never the reverse.
  • Transport-agnostic handlers: a handler is native to its door.
  • Per-route middleware and GraphQL.
  • cache and fallback in @Middleware (they need a cache manager or a fallback per method), and different middlewares per method.

Documentation

Overview

Package builder generates the doors of an application (REST routes on echo, gRPC services, MCP tools, event functions and jobs) from @REST, @GRPC, @MCP, @Function and @Job annotations, on top of the fx modules generated by inject.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CollectEntries

func CollectEntries(path string) ([]annotation.Entry, error)

CollectEntries returns the declarations under path, and in the packages they import, annotated with a door (@REST, @GRPC, @MCP, @Function, @Job) or with the inject annotations (@Provide, @Inject, @Invoke, @Default, @Config) that build their receivers, the @Middleware of constructors and the package docs with the @Server of each server.

func Generate added in v1.3.0

func Generate(ctx context.Context, src, out, cmd string) error

Generate writes the inject modules and the doors of the annotations under src to out, and the main of every app to cmd (none when cmd is empty): what cmd/builder does. It writes nothing when an annotation is not valid or the main of an app was written by hand.

func Stale added in v1.3.0

func Stale(ctx context.Context, src, out, cmd string) ([]string, error)

Stale returns the files Generate would write, change or remove under out and cmd, as slash-separated paths relative to src, sorted; none when the generated code is up to date. It writes nothing, so a test of the application can fail when gen/ or a main is stale:

stale, err := builder.Stale(ctx, ".", "gen", "cmd")

Types

type App

type App struct {
	Name     string
	Servers  []Server  // sorted by name
	Function *Function // nil when the app has no @Function
	Job      *Job      // nil when the app has no @Job; an app with one has nothing else
	// Middlewares are the @Middleware of the constructors the doors of the
	// app build, sorted by constructor.
	Middlewares []Middleware
}

App is one binary: its servers and, optionally, its event function, or its job alone.

type Function

type Function struct {
	// Adapter is the adapter of adapter=x, or the first of adapter=[x, y]:
	// the one wired when <app>.function.adapter is unset.
	Adapter string
	// Alternatives are the other adapters of adapter=[x, y], in order, that
	// <app>.function.adapter may choose instead. Empty for a single adapter.
	Alternatives []string
	Handler      Handler
	Middlewares  []string // middleware names, the outermost first
	// Providers are the inject constructors of the values the middlewares
	// are built with, like the publisher.Driver of publisher. Sorted by key.
	Providers []inject.Component
	// ClientProviders are the inject constructors of the default clients the
	// adapters read with, by client type ("<import path>.<Type>"). inject
	// builds the default client of a type no constructor provides.
	ClientProviders map[string]inject.Component
	// Client is the name of the client the adapters read with, from
	// client=x; "" for the default client.
	Client string
	// Builtin are the plugins of each adapter that takes them, by adapter,
	// from the @Server of the adapter's type or its default set.
	Builtin map[string][]string
}

Function is a @Function annotation: the event handler of a function app, the adapter that feeds it and the middlewares it runs around the handler.

func (Function) Adapters added in v1.6.1

func (f Function) Adapters() []string

Adapters returns the adapter and its alternatives, in order.

type Generator

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

Generator writes, under OutputDir/builder/<app>, one fx module per door of the app (a REST, gRPC or MCP server, the function or the job) and a modules.go whose Modules() returns all of them and the decorators of the app's @Middleware, written under OutputDir/builder/middleware/<import path>, and under CmdDir/<app> the main that runs them.

func NewGenerator

func NewGenerator(spec *Spec, opts Options) *Generator

NewGenerator returns a generator for the spec.

func (*Generator) Generate

func (g *Generator) Generate(ctx context.Context) error

Generate writes the doors under OutputDir/builder and the mains under CmdDir, and removes the builder files of previous runs that are no longer produced. Nothing is written when one of them cannot be rendered.

func (*Generator) Render

func (g *Generator) Render(context.Context) (map[string][]byte, error)

Render returns the generated files keyed by their slash-separated path relative to OutputDir (builder/<app>/... and builder/middleware/...), without touching the disk.

func (*Generator) RenderCmd

func (g *Generator) RenderCmd(context.Context) (map[string][]byte, error)

RenderCmd returns the main of every app keyed by its slash-separated path relative to CmdDir (<app>/main.go). It fails when the main of an app exists and was not generated by builder: builder owns the main of every app.

type Handler

type Handler struct {
	Entry annotation.Entry
	// Receiver is how a method gets its receiver; nil for a package function.
	Receiver *Receiver
}

Handler is the annotated function or method serving a door.

type Job added in v1.7.0

type Job struct {
	Handler Handler
}

Job is a @Job annotation: the function a job app runs once before it exits.

type Kind

type Kind string

Kind is the protocol a server speaks.

const (
	// KindREST is an echo server serving @REST routes.
	KindREST Kind = "rest"
	// KindGRPC is a gRPC server serving @GRPC services.
	KindGRPC Kind = "grpc"
	// KindMCP is an MCP server serving @MCP tools.
	KindMCP Kind = "mcp"
)

type Middleware

type Middleware struct {
	Provider  inject.Component // the annotated constructor
	Index     int              // result index of the interface value
	Plugins   []string         // plugin names, the outermost first
	Interface *types.Named     // the provided interface
	// Implementation is the implementation set the constructor is a member
	// of (@Default): the decorator runs only when config chooses it. Nil
	// for a constructor that is the only provider of its value.
	Implementation *inject.Implementation
}

Middleware is a @Middleware annotation: the plugins run around every method of the interface its constructor provides.

type Options

type Options struct {
	inject.GeneratorOptions

	// CmdDir receives <app>/main.go for every app; it must be inside the
	// module. Empty, no main is generated.
	CmdDir string
}

Options are the options of the inject generator of the same application, whose modules the doors include, and the directory of the mains.

type PackageLoader

type PackageLoader func(paths ...string) (map[string]*types.Package, error)

PackageLoader returns the type-checked packages at the import paths, keyed by path. NewSpec uses it to read the interfaces @Middleware decorates.

func LoadPackages

func LoadPackages(dir string) PackageLoader

LoadPackages returns a PackageLoader that loads the packages with go/packages from dir, which must be inside the module that imports them.

type Receiver

type Receiver struct {
	Type     string // provided type as written in the handler's package: "*Handler" or "Handler"
	Provider inject.Component
}

Receiver is the inject component providing the receiver of a handler method.

type Route

type Route struct {
	Method  string // upper case
	Path    string
	Handler Handler
}

Route is a @REST annotation: an HTTP method and path served by a handler.

type Server

type Server struct {
	App      string
	Name     string
	Kind     Kind
	Routes   []Route   // KindREST, sorted by path then method
	Services []Service // KindGRPC, sorted by register function
	Tools    []Tool    // KindMCP, sorted by name
	// Plugins are the inject constructors of the []Plugin the server is built
	// with: the value named after the server, or the default (unnamed) value
	// for rest, grpc and mcp. Sorted by key.
	Plugins []inject.Component
	// PluginsName is the name Plugins are provided under, empty for the
	// default value and when there are no Plugins.
	PluginsName string
	// Builtin are the plugins the generated door builds the server with, by
	// their @Server name, in order: the ones its @Server lists or, without
	// one, the default set of its kind. Empty when Plugins give them.
	Builtin []string
}

Server is one echo, gRPC or MCP server of an app, with its own config path.

type Service

type Service struct {
	Register string           // register function as written in the constructor's file
	Provider inject.Component // the annotated constructor
	Index    int              // result index of the service value
}

Service is a @GRPC annotation: the value provided by an inject constructor, registered with a generated Register<Service>Server function.

type Spec

type Spec struct {
	Apps []App // sorted by name
}

Spec is every door declared by the annotations, grouped by application.

func NewSpec

func NewSpec(entries []annotation.Entry, g *graph.Graph[inject.Component], load PackageLoader) (*Spec, error)

NewSpec builds the doors declared by the @REST, @GRPC, @MCP, @Function and @Job annotations of entries, the @Middleware of the constructors each app builds and the plugins each server is built with: the ones the @Server of a package doc lists or, without one, the default set of its type. g is the inject graph of the same entries: it provides the receivers of handler methods and the values of @GRPC constructors. load reads the interfaces @Middleware decorates; it is only called, once, when there is a @Middleware.

It returns NotValid for malformed annotations, for handlers that are unexported, in package main or whose signature does not match their door and for an app that runs a @Job and anything else, NotFound when the receiver of a handler method or a value a @Function middleware needs has no provider, and AlreadyExists for a duplicate route, service or tool, for a second @Function or @Job in an app, for two @Middleware that decorate the same interface, for a rest, grpc or mcp server given both a named and an unnamed []Plugin and for a server given two @Server or both a @Server and a @Provide []Plugin. A @Server of a server or adapter no app has is NotFound.

func (Spec) DefaultClients added in v1.6.1

func (s Spec) DefaultClients() []string

DefaultClients returns the client types ("<import path>.<Type>") the adapters of the functions without client=x read with that no inject constructor provides, sorted: inject generates their default client for the function doors.

func (Spec) NamedClients added in v1.6.2

func (s Spec) NamedClients() []inject.ClientRef

NamedClients returns the clients the adapters of the functions with client=x read with that no inject constructor provides, sorted by type and name: inject generates them for the function doors.

type Tool

type Tool struct {
	Name        string
	Description string // the doc comment of the handler
	Handler     Handler
}

Tool is a @MCP annotation: an MCP tool served by a handler.

Directories

Path Synopsis
cmd
builder command
Command builder generates the inject modules, the door modules and the main of every app from the annotations of an application, in one pass.
Command builder generates the inject modules, the door modules and the main of every app from the annotations of an application, in one pass.
examples
simple
Package simple is an application whose doors come from builder annotations: app api serves REST routes on two echo servers (rest and admin), a gRPC health service and the check_stock MCP tool; app order-worker records the orders of order.created events; app stock-audit is a job that checks the stock of a list of items once and exits.
Package simple is an application whose doors come from builder annotations: app api serves REST routes on two echo servers (rest and admin), a gRPC health service and the check_stock MCP tool; app order-worker records the orders of order.created events; app stock-audit is a job that checks the stock of a list of items once and exits.
simple/cmd/api command
Command api runs app api: the doors builder generates in github.com/xgodev/boost/builder/examples/simple/gen/builder/api.
Command api runs app api: the doors builder generates in github.com/xgodev/boost/builder/examples/simple/gen/builder/api.
simple/cmd/order-worker command
Command order-worker runs app order-worker: the doors builder generates in github.com/xgodev/boost/builder/examples/simple/gen/builder/order-worker.
Command order-worker runs app order-worker: the doors builder generates in github.com/xgodev/boost/builder/examples/simple/gen/builder/order-worker.
simple/cmd/stock-audit command
Command stock-audit runs app stock-audit: the doors builder generates in github.com/xgodev/boost/builder/examples/simple/gen/builder/stock-audit.
Command stock-audit runs app stock-audit: the doors builder generates in github.com/xgodev/boost/builder/examples/simple/gen/builder/stock-audit.
simple/createorder
Package createorder serves the order routes of app api.
Package createorder serves the order routes of app api.
simple/healthcheck
Package healthcheck serves grpc.health.v1.Health on the grpc server of app api.
Package healthcheck serves grpc.health.v1.Health on the grpc server of app api.
simple/order
Package order keeps the orders of the example in memory.
Package order keeps the orders of the example in memory.
simple/ordercreated
Package ordercreated records the orders of order.created events in app order-worker.
Package ordercreated records the orders of order.created events in app order-worker.
simple/probe
Package probe answers the liveness probe on the admin server of app api, apart from the public routes.
Package probe answers the liveness probe on the admin server of app api, apart from the public routes.
simple/stock
Package stock tells whether an item can be ordered.
Package stock tells whether an item can be ordered.
simple/stock/memory
Package memory keeps the stock in memory.
Package memory keeps the stock in memory.
simple/stock/remote
Package remote asks a warehouse over HTTP for the stock.
Package remote asks a warehouse over HTTP for the stock.
simple/stock/unlimited
Package unlimited is a stock without limits.
Package unlimited is a stock without limits.
simple/stockaudit
Package stockaudit checks the stock of a list of items in app stock-audit, a job: the app runs the audit once and exits, the shape of a CronJob.
Package stockaudit checks the stock of a list of items in app stock-audit, a job: the app runs the audit once and exits, the shape of a CronJob.
simple/stocktool
Package stocktool serves the stock tools of app api over MCP.
Package stocktool serves the stock tools of app api over MCP.

Jump to

Keyboard shortcuts

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