Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 221 additions & 0 deletions .allium/code-generator.allium
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
-- allium: 3
-- code-generator.allium

-- Scope: OAS-driven code generation pipeline in cmd/generate/
-- Includes: spec table, command registry (CommandMapping), generation
-- pipeline, coverage validation (checkExhaustive)
-- Grounded in: cmd/generate/main.go, cmd/generate/commands.go
-- Excludes:
-- - Output files: internal/api/*.gen.go, internal/cmd/commands.gen.go
-- - Hand-written extension points: bodyHook and configureFunc
-- implementations (specified in hand-authored-commands.allium)
-- - OAS specification source files (api/specs/)

------------------------------------------------------------
-- Enumerations
------------------------------------------------------------

enum ArtifactKind { type_definitions | client_methods | operation_descriptions | cli_commands }

------------------------------------------------------------
-- Entities
------------------------------------------------------------

-- One row of the developer-maintained spec table (main.go:36-39). The OAS
-- JSON documents under api/specs/ are external inputs; this table is owned
-- by the generator.
entity OASSpec {
spec_file: String -- OAS source under api/specs/ ("trading-api.json", "market-data-api.json")
prefix: String -- output file prefix ("trading" or "marketdata")
client_name: String -- name of the generated client struct ("Trading", "MarketData")
client_var: String -- package-level client var wired into commands ("tradingClient", "dataClient")
base_url_field: String -- client.Client field with the API base URL ("BaseURL", "DataURL")
}

entity CommandMapping {
-- Developer-maintained registry entry classifying one OAS operation:
-- cmdRegistry (mapped) or cmdSkip (excluded) in cmd/generate/commands.go.
operation_id: String
disposition: mapped | excluded
parent_command: String? -- key into the cmdParents group table
command_use: String? -- Cobra `Use` string; empty + self=false is invalid
self_command: Boolean -- true: the parent group itself is runnable (attachCmd)
examples: String? -- required when mapped; rendered as CLI help examples
exclusion_reason: String? -- required when excluded; explains why
flag_aliases: Set<String> -- renames body fields that collide with path/query params (bodyAliases)
skip_fields: Set<String> -- body fields handled by a hand-written hook (bodySkipFields)
default_overrides: Set<String> -- per-flag default value overrides (defaults map)
normalized_params: Set<String> -- path params stripped of "/" (normalize list, e.g. BTC/USD → BTCUSD)
body_hook: String? -- hand-written func(cmd, body) run after body construction
configure_func: String? -- hand-written func(cmd) appended to configure closures

invariant MappedMustHaveExamples {
disposition = mapped implies examples != null
}

invariant ExcludedMustHaveReason {
disposition = excluded implies exclusion_reason != null
}
}

entity GeneratedArtifact {
-- A file produced and written by the generator during a run
spec: OASSpec? -- null for combined (cross-spec) artifacts
kind: ArtifactKind
output_path: String
is_gofmt_applied: Boolean
}

------------------------------------------------------------
-- Surfaces
------------------------------------------------------------

-- Invoked as `go run ./cmd/generate` (Makefile `generate` target).
surface CodeGeneratorCLI {
provides:
RunCodeGenerator()
}

-- Developers maintain the registry by hand in cmd/generate/commands.go:
-- every new OAS operation must be added to cmdRegistry (mapped) or cmdSkip
-- (excluded) — checkExhaustive's error message says exactly that
-- (commands.go:876).
surface CommandRegistryMaintenance {
provides:
RegisterOperation(operation_id, disposition, examples?, exclusion_reason?)
}

------------------------------------------------------------
-- Rules
------------------------------------------------------------

rule RegisterOperation {
when: RegisterOperation(operation_id, disposition, examples?, exclusion_reason?)
ensures: CommandMapping.created(
operation_id: operation_id,
disposition: disposition,
examples: examples,
exclusion_reason: exclusion_reason
)
}

-- Phase 1+2a: per-spec types and clients, plus the combined descriptions
-- file, are written UNCONDITIONALLY — before the command registry is
-- validated (main.go:67-83).
rule GenerateApiArtifacts {
when: RunCodeGenerator()

ensures:
for spec in OASSpecs:
GeneratedArtifact.created(
spec: spec,
kind: type_definitions,
output_path: type_output_path(spec.prefix),
is_gofmt_applied: true
)
ensures:
for spec in OASSpecs:
GeneratedArtifact.created(
spec: spec,
kind: client_methods,
output_path: client_output_path(spec.prefix, spec.client_name),
is_gofmt_applied: true
)
ensures: GeneratedArtifact.created(
spec: null,
kind: operation_descriptions,
output_path: "internal/api/descriptions.gen.go",
is_gofmt_applied: true
)

@guidance
-- Type generation: one internal/api/<prefix>_types.gen.go per spec
-- (main.go:71). Schema name collisions WITHIN a spec are resolved by
-- appending a "V3" suffix; the capitalised variant keeps the clean
-- name and the lowercase variant is renamed (main.go:169-184).
--
-- Client generation: one <prefix>_client.gen.go per spec. Each
-- operation becomes a typed method with path injection
-- (url.PathEscape), query-parameter building, and JSON marshaling.
-- Generic unmarshal helpers handle typed and array responses;
-- operations with no response schema return raw JSON
-- (main.go:541-612). Request types with required string fields gain
-- a Validate() method (main.go:489-513, 634-648).
--
-- Descriptions file: one combined descriptions.gen.go across all
-- specs. Contains one Op variable per operation (Name, Summary,
-- Long, Example, ReturnsArray, Flags), lazily-built response
-- schemas, an AllOps slice, and an OpByName lookup used by the CLI
-- at runtime for flag registration, --schema output and CSV headers
-- (main.go:1192-1324).
--
-- Formatting: gofmt is applied to every artifact; if formatting
-- fails, the file is still written UNFORMATTED and a warning goes to
-- stderr — generation does not abort (main.go:1354-1363).
}

-- Phase 2b: the combined Cobra command tree is gated on registry coverage.
-- checkExhaustive runs after the API artifacts are written and aborts the
-- process before commands.gen.go is touched (main.go:90-92,
-- commands.go:868-915).
rule GenerateCommandTree {
when: RunCodeGenerator()

-- no registry entry references an operation missing from the specs
let stale_mappings = CommandMappings where not operation_exists_in_specs(operation_id)

-- every operation_id found in the OAS specs has a registry entry
requires: all_endpoints_registered()
requires: stale_mappings.count = 0

ensures: GeneratedArtifact.created(
spec: null,
kind: cli_commands,
output_path: "internal/cmd/commands.gen.go",
is_gofmt_applied: true
)

@guidance
-- checkExhaustive collects ALL violations, then aborts with the full
-- list (commands.go:912-914). The three violation classes:
-- 1. an operation_id present in an OAS spec but in neither
-- cmdRegistry nor cmdSkip (commands.go:871-878);
-- 2. a mapped entry with empty examples (commands.go:881-884) —
-- this machine-enforces MappedMustHaveExamples;
-- 3. a body field whose kebab-case flag name collides with a
-- path/query parameter and has no flag_aliases entry
-- (commands.go:885-910).
-- A registry entry referencing an operation that no longer exists in
-- any spec is also fatal, at emit time (commands.go:948-951).
-- ExcludedMustHaveReason holds by construction (cmdSkip maps
-- operation → reason string) but is not machine-checked for
-- non-emptiness.
--
-- Commands file contents: parent command group variables
-- (cmdParents), a fetchCmd/attachCmd wiring per mapped operation,
-- POST body constructors, PATCH/PUT body constructors with change
-- tracking (rejecting when no flag was set, commands.go:1323-1326),
-- and a single init() that wires the Cobra tree. Body fields in
-- skip_fields are excluded from generated construction and handled
-- by body_hook; default_overrides bake flag defaults; nullable OAS
-- scalars become pointer fields set only when the flag was
-- explicitly changed (commands.go:1240-1258).
}

------------------------------------------------------------
-- Invariants
------------------------------------------------------------

invariant UniqueCommandMappings {
for a in CommandMappings:
for b in CommandMappings:
a != b implies a.operation_id != b.operation_id
}

------------------------------------------------------------
-- Open Questions
------------------------------------------------------------

open question "Registry validation runs only before commands.gen.go is written; *_types.gen.go, *_client.gen.go and descriptions.gen.go are regenerated even when checkExhaustive subsequently aborts (main.go:67-92), leaving the tree partially regenerated. Intended, or should validation run before any file is written?"

open question "Schema-name deduplication is applied per spec (main.go:55); goName collisions ACROSS the trading and market-data specs are not disambiguated and would silently overwrite entries in the shared schemaByGoName lookup (main.go:60-65). Is cross-spec collision considered impossible, or should dedup run over the combined schema list?"
Loading
Loading