package main

Import Path
	go.pact.im/x/plumb (on go.dev)

Dependency Relation
	imports 2 packages, and imported by 0 packages

Involved Source Files Command plumb generates dependency-wiring functions from //plumb:<name> providers. Tag the declarations that produce values with a //plumb:<name> directive (functions, methods, variables, constants, conversions, struct fields, and struct types), and plumb generates a function named <name> that calls them in dependency order. Unlike [wire], you do not write the wiring function or declare its signature. plumb infers it: the parameters are what the providers need but nobody supplies (inputs), and the results are what they produce but nobody consumes (outputs). # Running plumb Invoke plumb as: plumb [-package-name=<name>] [-import-path=<path>] [-output=<file>] [-v] [<packagePattern>...] The flags are: - -package-name: the name in the generated "package <name>" clause. Defaults to the name of the package the file is generated into (the scanned package, when -import-path is inferred or names one); required when that package is not scanned, and rejected when it contradicts a scanned destination's real name. - -import-path: the import path of the package the generated file belongs to. Its own providers are referenced unqualified; everything else is imported and qualified. Defaults to the sole scanned package, so a single-package go:generate needs neither flag. Scanning more than one is ambiguous and must name the destination. A path matching no scanned package makes a separate package. It must be a valid Go import path. See “Where the code goes”, below. - -output: file to write; defaults to stdout. Its parent directory must already exist, since plumb does not create intermediate directories. - -v: print a discovery and inference report to stderr: packages scanned, providers found and where, and each generated signature. - <packagePattern>...: zero or more go/packages patterns (".", "./service", "./...", "work", or a full import path). Each may match several packages, and you may pass several patterns. With none, plumb scans the current directory ("."). plumb finds every set across all the matched packages and writes one file containing one function per set. Commit the generated file, and re-run plumb when providers change. The usual way to wire it up is a go:generate directive. Placed in the providers package, it generates into that package with nothing but an output file, inferring both the package name and the import path from where it sits: //go:generate plumb -output=plumb_gen.go # Directives A provider is tagged with a line comment: //plumb:build “build” is the set name and becomes the generated function’s name. It must be a valid unexported identifier, starting with a lowercase letter. This is not a style choice: //plumb:Build is not a real Go directive. gofmt rewrites it to "// plumb:Build" (a plain comment), silently disabling it, so keep the name lowercase. To get an exported wiring function, call the generated one from a small hand-written wrapper. The directive names a set and nothing else; trailing text (//plumb:build extra) is rejected. A provider may carry several directives to join several sets: //plumb:build //plumb:test func NewClock() Clock { return realClock{} } # Function providers The common case: a constructor. Its parameters are what it consumes; its results are what it produces. //plumb:build func NewDB(cfg *Config) (*sql.DB, error) { ... } //plumb:build func NewStore(db *sql.DB) *Store { ... } Variadic parameters are supported; a variadic parameter is treated as a slice ([]T). A function or method with no results is a side-effect provider: it consumes its inputs and is emitted as a bare call (Register(mux, h)) that produces nothing. # Variable providers A package-level variable is a value provider, equivalent to a function that returns it. The generated code references the variable directly. If a consumer needs a pointer to its type, plumb takes the address of a fresh copy, so the pointer points at that copy rather than the variable’s own storage (see “Value and pointer”, below). //plumb:build var stdin = os.Stdin is equivalent to //plumb:build func stdin() *os.File { return os.Stdin } # Constant providers A typed package-level constant is a value provider too, just like a variable. The generated code references the constant directly. //plumb:build const DefaultPort Port = 8080 provides Port. The constant must have an explicit type. An untyped constant (const DefaultPort = 8080) has no determinate provided type, so plumb rejects it. # Conversions A blank variable with an explicit target type is a conversion provider: it consumes the source type and produces the target type, emitting the Go conversion T(src). Binding a concrete type to an interface (the analog of wire.Bind) is the common case, but it is not special. Any conversion the blank-variable assignment type-checks is allowed. //plumb:build var _ io.Reader = (*os.File)(nil) is roughly equivalent to //plumb:build func bind(f *os.File) io.Reader { return f } except the generated code is a direct conversion, "reader := io.Reader(file)", rather than a function call. The value on the right-hand side is only a type hint: (*os.File)(nil) is the idiom for “the source type is *os.File”. Now anything that needs an io.Reader is satisfied by whatever produces a *os.File. The value must name a source type, so the untyped nil (var _ io.Reader = nil) is rejected because it names none; write (*os.File)(nil). # Struct field providers A struct field can be a provider. It consumes the struct and produces the field’s type. type Config struct { //plumb:build Port int } is equivalent to //plumb:build func port(c *Config) int { return c.Port } plumb consumes the struct as a value or a pointer, whichever your set already has. It prefers *Config if some other provider produces or consumes *Config; otherwise it takes Config by value. Field access works for both. # Struct type providers The inverse: a //plumb: directive on a struct type makes the type build itself from the set. Its exported fields become inputs, as if they were constructor arguments. Unexported fields are left as zero values. //plumb:build type Server struct { Addr string // exported → input DB *DB // exported → input log *log.Logger // unexported → left zero } is equivalent to //plumb:build func newServer(addr string, db *DB) Server { return Server{Addr: addr, DB: db} } It provides the value Server. If another provider in the set needs *Server, plumb also makes the pointer form available (it builds the value once and takes its address), so you can consume either Server or *Server without writing two providers. A generic struct works too, instantiated like any other generic provider (see “Generic providers”, below). A directive on a non-struct type (including a type alias whose target is not a struct) is rejected. # Value and pointer plumb wires by copy, and bridges between a value T and a pointer *T automatically, in both directions, where T is a named type, a type parameter, or a predeclared basic type (int⇄*int bridges just like MyInt⇄*MyInt; an inline composite such as []byte does not). When a provider yields a value T and a consumer needs *T, plumb holds the value in a local and takes its address (&v). Because the local is a fresh copy, *T never aliases a variable’s or field’s own storage, so mutating through the pointer never changes the original. When a provider yields *T and a consumer needs T, plumb dereferences it (*ptr); that assumes the pointer is non-nil, and a nil panics at run time, which plumb cannot rule out statically. The bridge is one level only (T⇄*T, never T⇄**T) and is not interface satisfaction. # Method providers A method is a provider whose receiver is its first input: //plumb:build func (db *DB) Store() *Store { return ... } is equivalent to //plumb:build func store(db *DB) *Store { return db.Store() } An interface method works the same way, with the interface as the receiver: type Reporter interface { //plumb:build Report() *Summary } requires a Reporter and provides a *Summary. The generated code calls the method on the receiver value (db.Store(), reporter.Report()), so the receiver is never package-qualified, but a method must be exported when the generated file lives in a different package. Methods on generic receivers are supported (see “Generic providers”, below). # Generic providers A generic provider is a template: plumb instantiates it as the wiring demands. A type parameter pinned to a concrete type is instantiated there, and the same provider can be instantiated at several types in one function. The generated call writes the type arguments explicitly, which is always valid Go: //plumb:build func NewCache[T any]() *Cache[T] { return ... } //plumb:build func NewUserService(c *Cache[User]) *UserService { return ... } //plumb:build func NewSessionService(c *Cache[Session]) *SessionService { return ... } NewUserService needs *Cache[User] and NewSessionService needs *Cache[Session], so NewCache is called at both NewCache[User]() and NewCache[Session](); the two results are distinct types and never collide. A type parameter that nothing pins is carried to the generated function, which becomes generic over it. plumb keeps the template’s own parameter name where it can: //plumb:build func NewPool[T any]() *Pool[T] { return ... } //plumb:build func Summarize[T any](p *Pool[T]) *Report { return ... } func build[T any]() (report *Report) { pool := NewPool[T]() report2 := Summarize[T](pool) report = report2 return } Methods on generic receivers work too. The receiver value carries its type arguments, so the call needs none: func (c *Cache[T]) Snapshot() *Snapshot[T] emits cache.Snapshot(). A field on a generic struct is likewise a template, equivalent to func(Foo[T]) Baz[T], with the struct as its input, taken by value or pointer just like a field on a non-generic struct: type Foo[T any] struct { //plumb:build Bar Baz[T] } A method on a generic basic interface (its type set is exactly its methods) is also a template, equivalent to func(Source[T]) Result[T], with the interface as the input: type Source[T any] interface { //plumb:build Fetch() Result[T] } A method on a generic constraint interface is rejected: a general, non-basic interface, say one embedding comparable. A non-basic interface can only be used as a type constraint, never as the type of a value, so it cannot be a receiver. A template with several results over different type parameters is wired by joint pinning: each result is pinned independently, by its own consumer. Given //plumb:build func NewKVPair[T, U any]() (Key[T], Value[U]) { ... } plumb instantiates it once one consumer pins Key[T] and another pins Value[U]: NewKVPair[int, string]() when the set needs Key[int] and Value[string]. A demand can match a template’s result by shape yet pin a type its constraint rejects, so the template cannot build it. plumb treats that the way it treats any type nobody produces: the demand becomes an input to the generated function (see “Rules and gotchas”), while the template still serves the demands it can. It only becomes an error when that leaves the template with no consumer at all. Then the diagnostic names the failed constraint. plumb rejects, with a clear message, a result-generic template that nothing instantiates (unused), one with a bare type-parameter result (func Default[T]() T), two templates that match a demand ambiguously, and an instantiation that does not terminate (a provider manufacturing unboundedly larger types). # The generated function plumb treats the wiring as a bag of typed values, one value per type. From the providers in a set it computes: - inputs: types required by some provider but produced by none. They become the generated function’s parameters. - outputs: types produced by some provider but consumed by none. They become its results. So adding a provider that consumes an existing output, or removing the last consumer of a type, changes the inferred signature. Run with -v to see it. # Errors If any provider returns a predeclared error, the generated function returns a trailing error. Each fallible call is checked and the error returned immediately. Every output is held in a local and copied to its named result only on the success path, so an error never returns a partially built value; the other results stay zero: func build(cfg *Config) (store *Store, err error) { db, e := NewDB(cfg) if e != nil { err = e return } store2 := NewStore(db) store = store2 return } # Cleanup functions A provider can return a func() to release whatever it just acquired, the same (T, func(), error) convention as wire. plumb recognizes the bare func() (no parameters, no results) as a teardown hook rather than a value, so it never enters the bag. Instead the generated function returns a single aggregated func(), placed after the outputs and before any error. A resource worth tearing down can usually also fail to open, so cleanup providers typically return (T, func(), error). Each error check unwinds whatever was acquired before it: //plumb:build func OpenConn() (*Conn, func(), error) { ... } //plumb:build func OpenPool(c *Conn) (*Pool, func(), error) { ... } //plumb:build func OpenServer(p *Pool) (*Server, func(), error) { ... } generates: func build() (server *Server, cleanup func(), err error) { conn, openConnCleanup, e := OpenConn() if e != nil { err = e return } pool, openPoolCleanup, e := OpenPool(conn) if e != nil { openConnCleanup() err = e return } server2, openServerCleanup, e := OpenServer(pool) if e != nil { openPoolCleanup() openConnCleanup() err = e return } server = server2 cleanup = func() { openServerCleanup() openPoolCleanup() openConnCleanup() } return } Call it, check the error, then defer the cleanup: server, cleanup, err := build() if err != nil { return err } defer cleanup() Two things to note. First, cleanups run last-acquired-first, both in the aggregated cleanup and on a mid-build error: above, OpenPool failing runs openConnCleanup before returning, and OpenServer failing runs openPoolCleanup then openConnCleanup. A provider that itself fails is assumed to have cleaned up after itself, so its own returned cleanup is not run. Second, the reservation is only of a bare func() result: a function or method returning func() yields a teardown hook, not a value, so to return a func() as an ordinary value give the type a name (type Handler func()). A func()-typed variable, field, or conversion is already an ordinary value. A cleanup may also report its own failure by returning an error: func() error, like (*sql.DB).Close. plumb recognizes that too. When any cleanup in a set is failable, the aggregated cleanup becomes func() error. A single failable cleanup with nothing to combine returns its error directly: //plumb:setup func OpenDB(dsn string) (*sql.DB, func() error, error) { db, err := sql.Open("pgx", dsn) if err != nil { return nil, nil, err } return db, db.Close, nil } generates: func setup(dsn string) (db *sql.DB, cleanup func() error, err error) { db2, openDBCleanup, e := OpenDB(dsn) if e != nil { err = e return } db = db2 cleanup = func() error { cleanupErr := openDBCleanup() return cleanupErr } return } When two or more errors can combine (two failable cleanups, or a failable cleanup plus a mid-build error), they are joined newest-first with errors.Join, which drops nil errors, so a clean teardown still returns nil. # Where the code goes The generated file lives in some package, and that determines what gets imported: - A provider in the same package as the generated file is called unqualified and not imported. - A provider in any other package is imported and qualified (pkg.NewX), so it must be exported, as must any type that appears in the generated signature. plumb keys on the generated file’s import path, not its output location: two packages can share a name at different paths, and -output may even be stdout. It takes that path from -import-path, or infers the sole scanned package when -import-path is omitted, so a go:generate in that package generates “in package” (like wire’s wire_gen.go), its providers unqualified, with no flags. A -import-path matching no scanned package makes a separate package that imports and qualifies every provider. Re-running over existing output is safe. In same-package mode plumb re-scans the file it is about to replace, so a stale generated file that no longer compiles (a provider whose signature changed, or one since removed) does not stop plumb from regenerating the corrected wiring. It tolerates those type errors and rewrites the file. In same-package mode the generated imports and function names share the package block with the destination package’s own declarations, so plumb reads that package’s top-level identifiers and avoids them. An import qualifier that would collide is aliased. A generated function name cannot be aliased (the set names it), so a set whose name is already declared in the destination package (or used as an import qualifier in one of its hand-written files) is rejected, asking you to rename it; the file plumb is about to overwrite (-output) is excluded, so regenerating over plumb’s own prior function still works. With output to stdout, where plumb cannot tell which file it would replace, that name check is skipped. These safeguards need the destination to be one of the scanned packages. A -import-path matching none of them (a separate package, or one the patterns did not load) leaves plumb blind to its declarations, so a generated qualifier or function name may collide with one; include the destination package in the patterns for collision-safe output. # Multiple packages A pattern such as "./..." scans many packages at once, and a set’s providers may live in different packages: // package store //plumb:build func NewDB(cfg *Config) (*DB, error) { ... } // package web (imports store) //plumb:build func NewServer(db *store.DB) *Server { ... } plumb -package-name=app -import-path=example.com/app -output=app/plumb_gen.go ./... generates a separate app package. Its import path matches neither store nor web, so both are imported. When scanning more than one package, you must name the destination: -import-path is required, and -package-name too, since app is not a scanned package. To generate into web instead, name it: "-import-path=example.com/web" makes web’s providers unqualified and imports only store, with -package-name inferred as web. # Rules and gotchas - One value per type. Two parameters of the same type get the same value; you cannot wire two distinct values of one type. Use a slice, a fixed-size array, or a distinct defined type per value (see “Not supported”, below). - One producer per type. If two providers produce the same type, plumb reports an ambiguity. This includes a function returning two values of the same type and a multi-name var/const declaring two names of one type. (Repeating the same directive on one declaration is instead a distinct duplicate-directive error.) - Unprovided means input. There is no “missing dependency” error: a typo in a type, or a forgotten provider, silently widens the signature rather than failing. -v prints the inferred inputs so you can catch this. - Cycles are errors. If the providers cannot be ordered (A needs something B produces and vice versa), plumb reports the cycle. - Matching is by exact type, not Go’s implements relation. A consumer of io.Reader is not matched by a producer of *os.File; add a conversion. - The value/pointer bridge is producer-driven. plumb bridges T and *T when a producer of one covers a demand for the other, but if neither is produced and both are demanded, they become two separate injector inputs (a T and a *T); -v shows both. Give one a producer or a conversion to collapse them. - Directives attach to package-level functions and methods, package-level variables and constants, conversions, struct fields, struct types, and interface methods. Locals are never providers, and embedded fields/interfaces cannot carry one. - Test files are never scanned. A //plumb: directive in a _test.go file is ignored with no diagnostic. This is the one place plumb drops a directive silently, a deliberate limitation. Put a provider a test needs in a regular file, or build it by hand in the test. - Build tags follow the build context: GOFLAGS=-tags=integration plumb … scans tag-gated files, and the output then compiles under that same context. - The destination must be able to import every provider’s package. plumb checks that cross-package names are exported, but not importability, so a provider in a package main or in an internal package outside the destination’s subtree generates an import the compiler, not plumb, rejects. - An -output written outside the destination package’s directory must not share a base name with one of that package’s source files: the file plumb skips as its own prior output is identified by base name, so a matching source file would be silently ignored. # Not supported Multiple distinct values of the same type in one set will never be supported. This is fundamental to plumb’s type-keyed model: each type maps to exactly one value, so two parameters of the same type always receive the same value, and two producers of one type are an ambiguity error. To wire several values of one underlying type, use a slice ([]T) or fixed-size array ([N]T) for a homogeneous collection, or a distinct defined type per use case (type ReadTimeout time.Duration and type WriteTimeout time.Duration rather than two time.Duration values). The same applies to decorators: func(T) T next to another producer of T is ambiguous, so give the decorated value a distinct type. Method providers on generic constraint interfaces are not supported either, since a constraint type cannot be a value. Functions, generic-receiver methods, generic struct fields, and generic basic interfaces are all supported (see “Generic providers”, above). # Example Providers in ./providers: package providers import ( "io" "os" ) type Config struct { //plumb:build Addr string } type Server struct{} //plumb:build var Stdin = os.Stdin //plumb:build var _ io.Reader = (*os.File)(nil) //plumb:build func NewServer(in io.Reader, addr string, cfg *Config) (*Server, error) { return &Server{}, nil } Run: plumb -package-name=app -import-path=example.com/app -output=app/plumb_gen.go ./providers Generated app/plumb_gen.go: // Code generated by plumb. DO NOT EDIT. package app import ( "example.com/providers" "io" ) func build(config *providers.Config) (server *providers.Server, err error) { string2 := config.Addr file := providers.Stdin reader := io.Reader(file) server2, e := providers.NewServer(reader, string2, config) if e != nil { err = e return } server = server2 return } *providers.Config is the only input, since nothing produces it; *providers.Server and error are the outputs. Call build(cfg) from your app package. [wire]: https://go.dev/blog/wire
Package-Level Functions (only one, which is unexported)