Skip to content

Single-Source definition.go, One Generator

Choose a tag to compare

@bw19 bw19 released this 19 Jun 17:19
· 2 commits to main since this release

v1.41.0 collapses each microservice's API down to a single, typed, hand-written source of truth: *api/definition.go. It declares the service's Hostname, metadata consts, and one define.* var per feature alongside its input/output structs — and everything else is generated from it. The new cmd/genservice tool reads definition.go and emits client.go, intermediate.go, mock.go, mock_test.go, and manifest.yaml in one pass, replacing the separate genmanifest and genmock tools (both deleted). The old hand-maintained endpoints.go is gone. The win is the end of drift: the five files that previously had to be kept in sync — by hand, by two generators, across every edit — now derive deterministically from one declarative spec, and each generated file carries a DO NOT EDIT header. Because the spec is typed Go, the compiler checks it: a feature's In/Out types must exist, and a cross-service InboundEvent.Source reference is verified across package boundaries, so a removed or renamed event becomes a compile error in every consumer. There is no change to the runtime or to the generated client API — service-to-service callers (NewClient, URL(), …) are untouched. This is an authoring-model and code-generation change, and the upgrade skill performs the migration mechanically.

Highlights

  • *api/definition.go is the single source of truth (new layout). One hand-written, typed file per microservice declares the Hostname, Name, Version, and Description consts plus one define.* var per feature with its In/Out structs. It replaces endpoints.go.
  • cmd/genservice is the one generator. From definition.go it emits client.go, intermediate.go, mock.go, mock_test.go, and manifest.yaml. A -check flag lets CI fail on stale generated files.
  • genmanifest and genmock are deleted. Their jobs fold into genservice. Housekeeping now runs one tool instead of two.
  • The define package gives the spec a typed vocabulary. define.Function, define.Web, define.Task, define.Workflow, define.OutboundEvent, define.InboundEvent, define.Config, define.Metric, and define.Ticker — bare nouns so the spec reads as the contract.
  • manifest.yaml is now a derived projection. It is generated from definition.go and never hand-authored. You still read manifests to map the system; you no longer write them.
  • Compile-time cross-service safety. InboundEvent.Source is a typed reference to the source service's event var, so a removed or renamed event is caught by the compiler in every sink, not at runtime.
  • Markers in generated files. A uniform // MARKER: Feature anchor now appears in definition.go, intermediate.go, client.go, mock.go, and mock_test.go, so "show me everything for feature X" is one grep within the microservice.

New Features

The definition.go Spec

A microservice's API is now authored as a small declarative file. Each feature is a define.* var whose godoc is the feature's description; its input and output structs sit beside it, with per-field documentation in jsonschema_description:"..." tags:

package calculatorapi

import "github.com/microbus-io/fabric/define"

// Hostname is the default hostname of the microservice.
const Hostname = "calculator.example"

// Name is the decorative PascalCase name of the microservice.
const Name = "Calculator"

// Description is the human-readable summary of the microservice.
const Description = `The Calculator microservice performs simple mathematical operations.`

// Arithmetic performs an arithmetic operation between two integers x and y given an operator op.
var Arithmetic = define.Function{ // MARKER: Arithmetic
	Host: Hostname, Method: "GET", Route: ":443/arithmetic",
	In: ArithmeticIn{}, Out: ArithmeticOut{},
}

// ArithmeticIn are the input arguments of Arithmetic.
type ArithmeticIn struct { // MARKER: Arithmetic
	X  int    `json:"x,omitzero" jsonschema_description:"X is the left operand"`
	Op string `json:"op,omitzero" jsonschema_description:"Op is the operator: + - * /"`
	Y  int    `json:"y,omitzero" jsonschema_description:"Y is the right operand"`
}

Field values are literals only — constant values, struct composite literals as type carriers (ArithmeticIn{}), and references to other define.* vars — so the whole spec is statically resolvable and the compiler verifies it. The routing fields (Host, Method, Route, RequiredClaims, TimeBudget, LoadBalancing) live directly on each routable kind, and Arithmetic.URL() still resolves the canonical endpoint URL for LLM tools and graph/task references, exactly as before.

One Generator: genservice

Run genservice on a microservice directory after editing its definition.go:

go run github.com/microbus-io/fabric/cmd/genservice ./calculator

It regenerates all five derived artifacts together — the client.go stubs (Client, MulticastClient, MulticastTrigger, Hook, Executor, Subflow), the intermediate.go wiring, mock.go, mock_test.go, and manifest.yaml — so they can never fall out of sync. The housekeeping skill invokes it automatically; you rarely call it by hand. genservice -check regenerates in memory and exits non-zero if anything on disk is stale, a drop-in CI guard against a forgotten regeneration.

Typed Configs, Metrics, and Cross-Service Events

The typed spec captures detail that previously lived only in hand-written recorder methods or scattered files:

  • Struct/JSON-valued configs are now fully generated. A define.Config whose Value carrier is a struct produces a typed getter that JSON-unmarshals the stored value and a setter that marshals it — no hand-written accessor.
  • Metrics declare their value type and labels (define.Metric{Kind: define.Counter, Value: int(0), Labels: []string{"op"}}), so the Increment/Record recorder methods are generated rather than hand-rolled.
  • Manual-subscription knobs (Manual, Tags) are declared on the feature's define.* var and flow through to the generated subscription wiring and Hook.

Breaking Changes

The upgrade skill handles each of these mechanically. Manual migration is not recommended.

  • endpoints.go is replaced by definition.go (loud, layout change). Every microservice's hand-maintained endpoints.go is removed and a typed definition.go synthesized in its place. The In/Out and domain-type declarations are lifted verbatim, preserving tags and comments.
  • genmanifest and genmock are deleted (loud if invoked). Replace any direct invocation, script, or CI step with genservice. Housekeeping already runs the new tool.
  • The authoring target moves to definition.go (workflow change). Add, modify, and remove features by editing definition.go and regenerating. The generated client.go, intermediate.go, mock.go, mock_test.go, and manifest.yaml carry a DO NOT EDIT header and must not be hand-edited; manifest.yaml is now a read-only derived view.
  • The Input: / Output: godoc convention is retired; descriptions standardize on the jsonschema_description tag (loud for OpenAPI/LLM-tool descriptions). Per-argument and per-field descriptions now come solely from jsonschema_description:"..." tags on the feature's In/Out struct fields in definition.go; the endpoint's overall description is the define.* var's godoc. The old structured Input: / Output: godoc sections on the handler are no longer parsed, so any per-argument text that lived there must move to the struct-field tags to keep appearing in the OpenAPI document and LLM tool schemas. Authoring also switches from the jsonschema:"description=..." subtag to the dedicated jsonschema_description:"..." tag, which is read whole — so a description may now contain commas (the jsonschema tag is comma-split into directives and silently truncated a description at its first comma). The jsonschema:"..." tag is reserved for other directives such as jsonschema:"example=6". A scalar query or path argument is now describable by the same tag as a body field, closing a gap where Input: / Output: had been the only way to document one. The upgrade migration moves existing descriptions onto jsonschema_description verbatim.
  • No runtime or client-API change. Service-to-service call sites, NewClient/NewMulticastClient/NewHook, and SomeFeature.URL() are unchanged. Nothing downstream of a migrated service needs adapting.

Migration

From inside a Microbus project, ask Claude Code to upgrade Microbus:

Get the latest version of Microbus.

The upgrade skill drives the new versioned migration tool, cmd/genupgrade -v 1.41.0, per microservice:

  1. Bump go.mod to v1.41.0 and go mod tidy.
  2. Refresh .claude/rules/ and .claude/skills/ and other framework-managed files.
  3. For each microservice, run genupgrade -v 1.41.0 to synthesize *api/definition.go from its manifest.yaml, endpoints.go (In/Out and domain types), and intermediate.go, then delete endpoints.go. Sources are migrated before their event sinks so InboundEvent.Source references resolve.
  4. Run genservice on each microservice to regenerate client.go, intermediate.go, mock.go, mock_test.go, and manifest.yaml.
  5. Run go vet ./... and the integration test suite, and review the diff for substantive correctness.

service.go handler bodies and OnStartup/OnShutdown are never touched by the migration. genupgrade is a permanent, versioned tool — downstream projects bumping to v1.41.0 later run the same routine.

Documentation