facade

package
v0.2.1 Latest Latest
Warning

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

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

Documentation

Overview

Package facade generates the curated public API of an extracted module.

A generated module holds relocated upstream packages under an internal prefix, which makes every one of them unimportable from outside the module. That is deliberate: the upstream package surface is enormous, it changes whenever upstream changes, and almost none of it is what a consumer came for. The facade is the small, deliberately chosen surface that is published in its place, and it is the only thing a consumer may depend on.

The facade is generated rather than written by hand because a hand written forwarding layer drifts. Upstream renames a field, changes a signature, or deletes a constructor, and a hand written wrapper either stops compiling in a place that does not explain itself or, worse, keeps compiling while meaning something new. Generating the surface from the loaded type information of the module that will actually be published makes the drift a build failure of the engine, before anything is tagged.

Four rules decide what the generated file may contain, and each exists to preserve an identity a consumer can rely on:

  • Types and interfaces become aliases. An alias is the same type, so a consumer's implementation of a copied interface satisfies the copied contract, and a value produced by the internal code is the value the facade names. A redeclared struct would be a different type that merely looks similar, and no amount of conversion helpers would make the two interchangeable.
  • Functions become real forwarding declarations with the resolved signature. A function value assigned to a package level variable would let any consumer reassign the published API at run time, so a var is never generated.
  • Constants are forwarded directly, because a constant has no identity to lose.
  • Exported variables are refused. A published mutable variable is shared global state between every consumer of the module, and no facade can make that safe.

The generator then proves two things that a compiler alone would not. It walks the exported surface recursively and refuses any reachable named type that lives inside the generated module but has no facade alias, because such a type is spelled in the public API while being unnameable by a consumer. External module types are left exactly as they are: they keep their upstream package identity, so a value the facade returns is the same type the rest of the ecosystem passes around. It also proves, at generation time, the interface assertions it emits, so a failure names the interface and the method that is missing rather than surfacing as a compile error in a generated file nobody wrote.

Everything the package produces is deterministic. Imports are aliased and ordered by rule, the API manifest is sorted and spelled with full package paths, and the output goes through go/format, so two runs over one commit produce identical bytes and a manifest comparison across a change is a real signal rather than noise.

Index

Constants

View Source
const GeneratedHeader = "// Code generated by soapbox. DO NOT EDIT."

GeneratedHeader is the marker every file this package writes carries.

It matches the convention at https://go.dev/s/generatedcode, so every Go tool that skips generated files skips these, and a reviewer reading the published module can tell the curated surface from code a human maintains.

Variables

View Source
var (
	// ErrSpec reports an export specification that does not describe a
	// generatable facade.
	ErrSpec = errors.New("facade specification is not usable")
	// ErrMissingSymbol reports a requested symbol the loaded module does not
	// export. It is the shape an upstream removal takes.
	ErrMissingSymbol = errors.New("requested symbol is not exported by the loaded package")
	// ErrKindMismatch reports a symbol whose kind is not the kind the profile
	// declared, which is how an upstream change from an interface to a struct,
	// or from a constant to a variable, is caught before it is published.
	ErrKindMismatch = errors.New("symbol is not of the declared kind")
	// ErrMutableVar reports an exported package level variable. A published
	// variable is global mutable state shared by every consumer of the module.
	ErrMutableVar = errors.New("exported variables may not be part of the public API")
	// ErrGeneric reports a generic declaration the generator will not forward.
	ErrGeneric = errors.New("generic declaration cannot be forwarded")
	// ErrCollision reports two exports that would occupy one name in the
	// generated package.
	ErrCollision = errors.New("two exports collide on one facade name")
	// ErrLeak reports an internal named type that is reachable from the public
	// API but has no facade alias, so a consumer could receive a value of a type
	// it cannot name.
	ErrLeak = errors.New("internal type is reachable from the public API without a facade alias")
	// ErrUnrepresentable reports a type the generated package cannot spell
	// identically, such as an unnamed struct or interface carrying an
	// unexported member of another package. Writing the same characters in the
	// generated package would declare a different type, so there is no
	// spelling to publish.
	ErrUnrepresentable = errors.New("type cannot be spelled in the generated package")
	// ErrManifestChanged reports a published API that differs from the baseline
	// it was compared against.
	ErrManifestChanged = errors.New("published API differs from the baseline")
	// ErrAssertion reports a generated interface assertion that does not hold.
	ErrAssertion = errors.New("interface assertion does not hold")
	// ErrLoad reports a module the Go type checker could not load.
	ErrLoad = errors.New("generated module did not load")
)

Facade sentinels. Callers use errors.Is to distinguish the failure.

Functions

This section is empty.

Types

type Assertion

type Assertion struct {
	// Type is the facade name of the asserted type. It must be an exported
	// entry of the same specification.
	Type string
	// Pointer asserts on the pointer to the type rather than the value, which
	// is what a type with pointer receiver methods requires.
	Pointer bool
	// Interface is the qualified interface, such as
	// k8s.io/apiserver/pkg/authorization/authorizer.Authorizer. It is an
	// external module path used verbatim: an assertion against a copy of the
	// interface would prove nothing about the real one in external mode.
	Interface string
	// Local permits a compatibility-mode assertion against an engine-generated
	// module-local interface. User-authored assertions never set it.
	Local bool
}

Assertion is a compile time proof that a facade type implements an interface.

The interface is named by its real upstream path, and the assertion is checked here as well as emitted, because the failure the engine can explain is far more useful than the one the Go compiler would report from a generated file: this package can say which interface, which method, and whether the method set was missing it entirely or had it with a different signature.

type Difference

type Difference struct {
	// Name is the facade name the difference concerns, or a header field name
	// when the two manifests describe different modules or packages.
	Name string
	// Before is the rendered entry in the earlier manifest, empty when the
	// entry was added.
	Before string
	// After is the rendered entry in the later manifest, empty when the entry
	// was removed.
	After string
}

Difference is one entry that is not the same in two manifests.

func Diff

func Diff(before, after Manifest) []Difference

Diff reports how two manifests differ, in a deterministic order.

It is what proves the pre-prune and post-prune public API are the same. A prune removes files from the relocated packages, and the whole claim that a prune is safe rests on the published surface being unchanged by it; comparing the two manifests is how that claim is checked rather than asserted.

func (Difference) String

func (d Difference) String() string

String renders one difference as a short, greppable line pair.

type Entry

type Entry struct {
	// Name is the identifier the generated package declares.
	Name string
	// Kind is the declaration that was generated.
	Kind Kind
	// Target is the relocated qualified symbol the entry forwards to, spelled
	// with the generated module's own path.
	Target string
	// Type is the declared type, spelled with full package paths and without
	// parameter names.
	Type string
	// Underlying is the underlying type of a published defined type, spelled
	// the same way.
	//
	// It is recorded because the qualified name alone hides a real API change:
	// a defined type whose underlying type moves from int to int64, or whose
	// function type gains a parameter, keeps its name and its members while
	// becoming something a consumer's existing code no longer converts to or
	// calls. It is left empty for a struct or an interface, where Members is
	// the more precise record and the underlying spelling would only repeat it
	// while also dragging in unexported fields that are not API.
	Underlying string
	// Value is a constant's exact value. It is empty for every other kind,
	// because a constant whose value changes is an API change that no type
	// string would show.
	Value string
	// Members are the exported members of a published type, sorted. They are
	// recorded because the manifest exists to detect a change in the published
	// surface, and a struct that loses a field or an interface whose method
	// changes signature is exactly such a change while its type string stays
	// the same.
	Members []Member
}

Entry is one published declaration.

type Export

type Export struct {
	// Name is the identifier the generated package declares. It is always
	// explicit, because two upstream packages may export the same name and the
	// facade has one namespace: the four RBAC lister adapters collide with the
	// validation interfaces they adapt, and only an explicit name says which of
	// the two a consumer is being handed.
	Name string
	// Kind is the declaration to generate.
	Kind Kind
	// Source is the upstream qualified symbol, such as
	// k8s.io/kubernetes/pkg/registry/rbac/validation.RoleGetter. It is spelled
	// against the upstream import path rather than the relocated one because
	// that is what a profile author reads in the upstream repository, and
	// because the relocated path is derived from it by a rule this package
	// applies rather than by a second thing to keep in sync.
	Source string
	// Direct marks a generated module-local declaration. It is never accepted
	// from a user profile; an engine compatibility phase derives it.
	Direct bool
	// Doc is an optional documentation sentence for the generated declaration.
	// It is rendered above the declaration, after the provenance line the
	// generator always writes.
	Doc string
}

Export is one entry of the published surface.

type Kind

type Kind uint8

Kind is the declaration a facade entry produces.

The profile states the kind and the generator checks it against what the loaded module actually holds. Restating something the type checker already knows is the point: a symbol that changed kind upstream is a breaking change to the published API, and it has to stop the run rather than quietly generate a different declaration than the one that was reviewed.

const (
	// KindType is a named type exposed through an alias.
	KindType Kind = iota
	// KindInterface is a named interface type exposed through an alias. It is
	// distinct from KindType only so that an upstream interface that becomes a
	// struct is reported rather than aliased.
	KindInterface
	// KindFunc is a function exposed through a forwarding declaration.
	KindFunc
	// KindConst is a constant forwarded directly.
	KindConst
	// KindVar is an exported variable. It is named so a profile that asks for
	// one is refused with an explanation rather than an unknown kind error.
	KindVar
)

func ParseKind

func ParseKind(name string) (Kind, error)

ParseKind maps the profile spelling onto a Kind.

It exists so the configuration layer can convert without this package importing configuration, which would tie the generator to one profile schema.

func (Kind) String

func (k Kind) String() string

String renders the kind as the profile spells it.

type Manifest

type Manifest struct {
	// Module is the generated module path.
	Module string
	// Package is the generated root package name.
	Package string
	// Entries are the published declarations, sorted by name.
	Entries []Entry
}

Manifest is the deterministic description of the published API.

It is the artefact that makes a change to the public surface reviewable. Two manifests taken across a prune, an upstream bump, or a profile edit compare exactly, so a removal, a signature change, or a new name is a diff rather than something a reader has to notice. Everything in it is spelled with full package paths, so a type that moved between packages while keeping its name does not compare equal to the type it replaced.

func (Manifest) CheckAgainst

func (m Manifest) CheckAgainst(baseline Manifest) error

CheckAgainst refuses a published API that differs from a baseline.

It is the blocking seam a caller puts between generating a facade and publishing it. Diff answers what changed; this answers whether the run may continue, and it renders every difference into the error so the decision does not depend on the caller also printing a report. The two uses are the same comparison from opposite directions: a pre-prune manifest checked against the post-prune one proves a prune changed no published API, and a released manifest checked against a regenerated one proves an upstream bump did not break consumers.

func (Manifest) Equal

func (m Manifest) Equal(other Manifest) bool

Equal reports whether two manifests describe the same published API.

func (Manifest) Render

func (m Manifest) Render() string

Render writes the manifest as deterministic text.

The text form is what a report carries and what a reviewer reads, so it is stable rather than pretty: one field per line, a fixed field order, and members indented under the entry they belong to.

type Member

type Member struct {
	// Name is the member's identifier.
	Name string
	// Kind is what the member is.
	Kind MemberKind
	// Type is the member's type, spelled with full package paths and without
	// parameter names. For a method it is the signature without the receiver,
	// which is what a consumer calls.
	Type string
}

Member is one exported member of a published type.

type MemberKind

type MemberKind uint8

MemberKind distinguishes the exported members a published type carries.

const (
	// MemberField is an exported struct field.
	MemberField MemberKind = iota
	// MemberMethod is an exported method, including one promoted from an
	// embedded field and one contributed by an embedded interface.
	MemberMethod
)

func (MemberKind) String

func (k MemberKind) String() string

String renders the member kind.

type Options

type Options struct {
	// Dir is the absolute root of the generated module. It must already hold
	// the relocated packages and a go.mod the toolchain can load.
	Dir string
	// Env is the complete environment the Go toolchain runs under. It is never
	// inherited: see load for why an ambient environment would make the
	// published API depend on the shell that produced it.
	Env []string
	// Spec is the surface to publish.
	Spec Spec
}

Options configures one facade generation.

type Result

type Result struct {
	// Files are the generated root files, sorted by path. They are relocated
	// files with no upstream source, so a caller composes them into the file
	// set the module materializes from without a second write path.
	Files []relocate.File
	// Manifest is the published surface, for comparison against another run.
	Manifest Manifest
}

Result is everything one generation produced.

func Generate

func Generate(ctx context.Context, opts Options) (Result, error)

Generate produces the curated public API of a generated module.

The order of the steps is the order in which a failure is cheapest to explain. The specification is checked on its own first, because a profile mistake should not cost a module load. The module is then loaded once and every symbol resolved against it, so an upstream removal is reported as a missing symbol rather than as a compile error in generated code. Only then is anything rendered, and the interface assertions are proved here rather than left for the Go compiler to discover in a file no human wrote.

type Spec

type Spec struct {
	// ModulePath is the generated module's path, such as
	// monis.app/kk/rbac_authorizer.
	ModulePath string
	// SourcePrefix is the upstream module path every Source is spelled against,
	// such as k8s.io/kubernetes.
	SourcePrefix string
	// InternalPrefix is the module relative directory the upstream paths were
	// relocated below, such as internal/kk.
	InternalPrefix string
	// Package is the generated root package name, such as rbacauthorizer.
	Package string
	// File is the module relative path of the generated facade, such as
	// authorizer.go.
	File string
	// AssertionsFile is the module relative path of the generated assertions,
	// such as zz_generated_assertions.go. It is written even when no assertion
	// is specified, so the published tree never depends on whether a profile
	// happened to declare one.
	AssertionsFile string
	// Exports are the published entries. Order does not matter: the output is
	// sorted by facade name.
	Exports []Export
	// Assertions are the interface implementations to prove and emit.
	Assertions []Assertion
}

Spec is the config derived description of the facade to generate.

The type is owned by this package rather than imported from configuration so the generator depends on the shape of the problem instead of on the shape of one profile schema, and so a test can state a surface in a few lines without building a whole profile.

Jump to

Keyboard shortcuts

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