codegen

package
v1.1.0 Latest Latest
Warning

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

Go to latest
Published: May 31, 2026 License: MIT Imports: 21 Imported by: 0

Documentation

Overview

OpenAPI codegen uses `github.com/getkin/kin-openapi` to build a strongly- typed openapi3.T document and `sigs.k8s.io/yaml` to render it as YAML. The library handles spec compliance, default ordering, and the small quirks of OpenAPI 3.1; we only translate craftgo's AST into its types.

Field-level decorator -> schema metadata mapping.

Multi-package merge + clone/rewrite helpers for the OpenAPI document builder.

Operation assembly: buildOperation, parameters, errors, tags, security.

Path + per-operation request/response schemas + response headers.

Top-level component schemas: types, enums, scalars, errors.

OpenAPI security scheme components emission + manifest cross-check.

TypeRef -> SchemaRef conversion + generic instantiation.

Transport: path/query/header/cookie/form/response binding collection + string-binding helpers.

Transport: @default literal rendering + Go pre-fill code generation.

Package codegen turns a validated semantic.Package into Go source files.

Each generator function in this package is responsible for one artefact (currently only types.go; handler/routes/logic scaffolds and OpenAPI/client generators land in subsequent slices). Output files are formatted with `go/format` before being written so that diffs in version control are minimal and the generated code passes `gofmt`.

Array / file validators: @minItems/@maxItems/@uniqueItems/@maxSize/@mimeTypes.

Format-validator catalogue: per-@format(name) check builders + RFC references.

Nested / enum / type-param validator dispatch.

Numeric validators: @gt/@gte/@lt/@lte/@range/@positive/@negative/@multipleOf.

Required-presence validators emitted onto the generated Validate() chain.

String validators: @length, @minLength, @maxLength, @pattern, @format dispatcher.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func GenerateEnums

func GenerateEnums(pkg *semantic.Package, outDir string) error

GenerateEnums writes enums.go under outDir/<pkg.Name>/ with a Go type alias and const block per enum. No-op when pkg has no enums.

The Go base type is `int` for int-valued enums, `string` otherwise. Constants are named `<EnumName><PascalCase(ValueName)>`; bare values use the value name as the string payload.

func GenerateErrors

func GenerateErrors(pkg *semantic.Package, outDir string) error

GenerateErrors emits a single `errors.go` file under outDir/<pkg.Name>/ declaring one struct + constructor + Error()/HTTPStatus() methods + SCREAMING_SNAKE error-code constant for every ast.ErrorDecl in pkg. When pkg has no errors the function is a no-op.

Equivalent to GenerateErrorsPackage with a nil resolver; kept for single-package callers that don't reach across packages.

func GenerateErrorsPackage

func GenerateErrorsPackage(pkg *semantic.Package, outDir string, r *ProjectResolver) error

GenerateErrorsPackage is the multi-package variant of GenerateErrors. The ProjectResolver supplies the cross-package import paths for body fields (e.g. an error in `tasks` whose body carries a `users.UserRef`) AND the cross-package scalar / enum resolution needed to format a non-string `@header` / `@cookie` error field (`cost shared.Cents`). A nil resolver falls back to local-only resolution.

func GenerateOpenAPI

func GenerateOpenAPI(pkg *semantic.Package, cfg *config.Config, projectRoot string) error

GenerateOpenAPI builds an OpenAPI 3.1 document for pkg and writes it as YAML to the path configured by `output.openapi`. Each service contributes one set of operations under its `@prefix`; every concrete TypeDecl becomes a schema in `components.schemas`.

func GeneratePerServiceRoutes

func GeneratePerServiceRoutes(pkg *semantic.Package, cfg *config.Config, projectRoot string) error

GeneratePerServiceRoutes emits only the per-service `routes.go` files; the umbrella is left to a project-level pass. Used by the multi-package CLI flow so each package's services contribute to a single shared umbrella rather than overwriting each other.

func GenerateProjectMain

func GenerateProjectMain(proj *semantic.Project, cfg *config.Config, projectRoot string) error

GenerateProjectMain scaffolds the project's main.go (`output.main`) using the union of services and middlewares from every package. The single shared umbrella `routes.RegisterAll` (emitted by GenerateProjectRoutesUmbrella) is the one-call wire-up so the template doesn't have to import per-package routes packages.

The file is gen-once: written when missing, skipped on subsequent gen runs so user-written boot code (extra middlewares, config loading, OTel SDK setup, etc.) survives regeneration.

Setting `output.main: "-"` in the manifest opts the project out of scaffolding entirely - useful for test fixtures that ship their own httptest server and would collide with a generated `package main`.

func GenerateProjectMiddlewares

func GenerateProjectMiddlewares(proj *semantic.Project, cfg *config.Config, projectRoot string) error

GenerateProjectMiddlewares emits the unified `svccontext/middlewares.go` + per-middleware scaffolds for every package in the project. Middleware names are global (the project resolver enforces uniqueness), so a single Middlewares struct embeds every declaration regardless of which package it lives in. Run ONCE per `craftgo gen` instead of per-package.

Two artefacts per `middleware Name` block:

  1. The IMPLEMENTATION at `<output.middleware>/<kebab-name>-middleware.go`. Scaffold-only - gen-once; existing files are left alone.
  2. The TYPE declaration list at `<svccontext-dir>/middlewares.go`. Always overwritten - derived purely from the DSL.

Users embed the generated `Middlewares` struct into their own ServiceContext, then assign each field to a concrete impl in main.go. Routes consume the middleware via the embedded fields directly, so no runtime name registry lookup is needed.

func GenerateProjectOpenAPI

func GenerateProjectOpenAPI(proj *semantic.Project, cfg *config.Config, projectRoot string) error

GenerateProjectOpenAPI is the multi-package counterpart of GenerateOpenAPI: it merges every package's types/enums/errors/ scalars/services into a single OpenAPI 3.1 document. When two packages declare a same-named entity, the second-and-subsequent occurrences get renamed to `<PascalPkg><Name>` (e.g. two packages each declaring `User` produce `User` for the first-seen and `AuthUser` / `SharedUser` for collisions). Non-conflicting names stay bare so simple projects keep readable schema names.

func GenerateProjectRoutesUmbrella

func GenerateProjectRoutesUmbrella(proj *semantic.Project, cfg *config.Config, projectRoot string) error

GenerateProjectRoutesUmbrella emits the top-level `<output.routes>/routes.go` that exposes `RegisterAll(srv, svcCtx)`, aggregating every service from every DSL package in the project. When no package declares a service the file is skipped - calling `RegisterAll` from main.go would also be a no-op.

func GenerateRoutes

func GenerateRoutes(pkg *semantic.Package, cfg *config.Config, projectRoot string) error

GenerateRoutes emits one `routes.go` per service under `<output.routes>/<servicePackage>/` PLUS a top-level `<output.routes>/routes.go` that exposes `RegisterAll(srv, svcCtx)` - the one-call wire-up consumed by main.go. Both layers are regenerated on every gen because they're derived purely from the DSL service set.

Single-package callers should keep using this entry point. Multi- package projects call GeneratePerServiceRoutes per package and GenerateProjectRoutesUmbrella once for the project so the umbrella aggregates services from every package.

func GenerateRuntimeConfig

func GenerateRuntimeConfig(cfg *config.Config, projectRoot string) error

GenerateRuntimeConfig scaffolds the project's `config/` package (`config.go` + `config.yaml` + `example.config.yaml`) under `cfg.Output.Config`. Every file is gen-once: written when missing, left untouched when present. main.go reads `<Config>/config.yaml` at boot and hands the loaded `*config.Config` to `svccontext.NewServiceContext`.

Skipped when `cfg.Output.Main == "-"` - projects opting out of the generated main.go (test fixtures, library-style modules) don't need the runtime config package; emitting it would only add a stray import and force the module to track yaml.v3.

The template body lives in `internal/codegen/templates/`. Edit those files to change the shape of the scaffolded artefact - per-project overrides are out of scope here (the runtime config is meant to be edited freely after the first gen).

func GenerateService

func GenerateService(pkg *semantic.Package, cfg *config.Config, projectRoot string) error

GenerateService scaffolds one `<method>.go` per method per service under `<output.service>/<servicePackage>/`. Unlike the other generators this one runs in **scaffold** mode: existing files are left untouched so user-written business logic is never overwritten.

Equivalent to GenerateServicePackage with a nil CrossPkg context.

func GenerateServicePackage

func GenerateServicePackage(pkg *semantic.Package, cfg *config.Config, projectRoot string, crossPkg CrossPkg) error

GenerateServicePackage is the multi-package variant of GenerateService. crossPkg lets the scaffold render `*foo.Cred` for a cross-package request/response type rather than the legacy `*types.Cred`.

func GenerateSvccontext

func GenerateSvccontext(cfg *config.Config, projectRoot string) error

GenerateSvccontext scaffolds `svccontext.go` at the location pointed to by `cfg.Output.Svccontext`. The file accepts a `*config.Config` in its constructor and embeds the codegen-managed `Middlewares` struct (which is regenerated next to it on every gen run).

Gen-once: existing svccontext.go is left untouched so user-added fields (database handles, caches, ...) survive regeneration. The adjacent `middlewares.go` IS regenerated; splitting the two keeps the auto-managed struct from colliding with hand-edited code.

Skipped when `cfg.Output.Main == "-"` - same rationale as GenerateRuntimeConfig: opting out of main.go means the project doesn't want the framework's runtime scaffolding in its module.

func GenerateTransport

func GenerateTransport(pkg *semantic.Package, cfg *config.Config, projectRoot string) error

GenerateTransport emits one `<method>_handler.go` per method per service under `<output.handler>/<servicePackage>/`. Each file contains a single exported `<Method>Handler(svcCtx) http.HandlerFunc` constructor that decodes the request, calls the user's logic, and writes the response.

projectRoot is prepended to `cfg.Output.Transport` so the function can be called with paths relative to the manifest's directory.

Equivalent to GenerateTransportWith with nil CrossPkg and nil ScalarTable - the convenience entry single-package tests reach for. Production CLI flows go straight through GenerateTransportWith because they always have a project-wide cross-package table to feed in.

func GenerateTransportHelpers

func GenerateTransportHelpers(pkg *semantic.Package, cfg *config.Config, projectRoot string) error

func GenerateTransportResolved

func GenerateTransportResolved(pkg *semantic.Package, cfg *config.Config, projectRoot string, r *ProjectResolver) error

GenerateTransportResolved is the canonical entry point. The ProjectResolver supplies every project-wide lookup the handler emit chain may consult — scalar inheritance, cross-package enum/type resolution for binding casts, and the Go import paths the generated handler file needs when it emits qualified identifiers. nil resolver yields the legacy single-package behaviour: only `pkg`'s local symbols resolve.

func GenerateTransportWith

func GenerateTransportWith(pkg *semantic.Package, cfg *config.Config, projectRoot string, crossPkg CrossPkg, scalars ScalarTable) error

GenerateTransportWith is the explicit-tables entry for single-package tests that build CrossPkg / ScalarTable directly. GenerateTransportResolved accepts a ProjectResolver bundling every cross-package table.

func GenerateTypes

func GenerateTypes(pkg *semantic.Package, outDir string) error

GenerateTypes emits a `types.go` file under outDir/<pkg.Name>/ containing Go struct definitions for every concrete (non-generic) ast.TypeDecl in pkg. Generic declarations (those with ast.TypeDecl.TypeParams) are skipped - their concrete instances are emitted at the call site once generic instantiation lands.

outDir is the configured `output.types` directory; the package name segment is appended so that types live alongside the rest of the service-scoped artefacts.

Equivalent to GenerateTypesPackage with a nil CrossPkg context; the legacy single-package signature stays so existing callers / tests keep working unchanged.

func GenerateTypesPackage

func GenerateTypesPackage(pkg *semantic.Package, outDir string, crossPkg CrossPkg) error

GenerateTypesPackage is the multi-package variant of GenerateTypes. crossPkg adds Go imports for every cross-package DSL alias used in pkg's field types or mixin refs - when nil/empty the output is identical to single-package codegen.

func GenerateValidators

func GenerateValidators(pkg *semantic.Package, outDir string) error

GenerateValidators writes `validate.go` next to `types.go`. The file adds a `Validate() error` method to every concrete TypeDecl. Types without any constraints get an empty stub so handlers can call `req.Validate()` uniformly.

Equivalent to GenerateValidatorsPackage with a nil CrossPkg context, for single-package callers and tests.

func GenerateValidatorsAll

func GenerateValidatorsAll(pkg *semantic.Package, outDir string, crossPkg CrossPkg, scalars ScalarTable, types TypeTable, enums EnumTable) error

GenerateValidatorsAll is the explicit-tables entry point for tests that build tables directly; GenerateValidatorsResolved accepts a single ProjectResolver instead of four ad-hoc tables. This wrapper assembles a resolver from the parameters and delegates.

func GenerateValidatorsPackage

func GenerateValidatorsPackage(pkg *semantic.Package, outDir string, crossPkg CrossPkg) error

GenerateValidatorsPackage is the multi-package variant of GenerateValidators. crossPkg adds Go imports for every cross- package alias used in pkg's field types so `req.User.Validate()` can dispatch to the sibling package's validator.

Equivalent to GenerateValidatorsWith with a nil scalar table: scalar inheritance is disabled in this entry point.

func GenerateValidatorsResolved

func GenerateValidatorsResolved(pkg *semantic.Package, outDir string, r *ProjectResolver) error

GenerateValidatorsResolved is the canonical entry point. It takes a single ProjectResolver carrying every cross-package lookup the validator emit chain needs — scalar inheritance, generic Validate dispatch, cross-pkg enum value-set checks, and the matching Go import registrations. nil resolver is tolerated and degrades to local-only behaviour, matching the legacy single-package shape.

func GenerateValidatorsWith

func GenerateValidatorsWith(pkg *semantic.Package, outDir string, crossPkg CrossPkg, scalars ScalarTable, types TypeTable) error

GenerateValidatorsWith is the project-aware entry point: it accepts the ScalarTable built by BuildScalarTable so a field typed `Email` (local scalar) or `shared.NonEmptyID` (cross-pkg scalar) inherits the scalar's own decorator chain into its generated Validate() body. The TypeTable resolves qualified type refs (`shared.Page<T>`), which the local-only `pkg.Types` lookup cannot reach, so they emit recursive `.Validate()` calls.

Used by the multi-package CLI flow; single-package fixtures and tests continue calling GenerateValidators / GenerateValidatorsPackage which pass nil for the tables.

func GoFieldName

func GoFieldName(name string) string

GoFieldName converts a DSL field name (which is allowed to be lowercase, snake_case, or camelCase) into an exported Go identifier applying the common-initialism rule. The DSL field name is preserved verbatim as the JSON tag - see [jsonTag]. Implementation lives in internal/idents so the semantic analyser can detect collisions using the same conversion rule that codegen emits.

func GoTypeRef

func GoTypeRef(t *ast.TypeRef) string

GoTypeRef converts an ast.TypeRef into the corresponding Go type expression. The optional suffix (`?`) prepends `*` only when the underlying Go type isn't already nilable: slices, maps, channels, interfaces, and pointer-shaped builtins (`file` → `*multipart.FileHeader`) already use `nil` as their zero value, so wrapping them in another pointer would just produce `**T` / `*[]T` for no semantic gain. Value types (`string`, `int`, user structs) still receive `*` so the generated field can distinguish "absent" from the zero value.

func ServiceDir

func ServiceDir(svcName string) string

ServiceDir returns the kebab-case directory name for a service. Used for filesystem paths and import segments - `UserService` becomes `user-service`. The Go package declaration inside the directory still uses ServicePackage (no hyphens) so the source remains compilable.

func ServicePackage

func ServicePackage(svcName string) string

ServicePackage returns the Go-identifier package name for a service. Service names use PascalCase in the DSL ("UserService"); the matching Go package declaration is the lowercase concatenation ("userservice") because Go identifiers cannot contain hyphens.

func ValidateProjectOpenAPI added in v1.1.0

func ValidateProjectOpenAPI(proj *semantic.Project, cfg *config.Config) error

ValidateProjectOpenAPI runs every OpenAPI-level uniqueness check (operationId + component-schema names) against the merged project WITHOUT writing anything. The CLI calls it as a pre-flight before any codegen step touches disk, so a name collision fails the whole run up front instead of after types/transport are already written.

func ValidateSecurityRefs

func ValidateSecurityRefs(pkg *semantic.Package, cfg *config.Config) []string

ValidateSecurityRefs cross-checks every `@security(scheme)` reference in pkg against the manifest's declared `openapi.securitySchemes` map. The check is permissive when the manifest declares no schemes: in that case we keep the legacy auto-generated bearer behaviour (so projects that haven't migrated continue to work). When the manifest HAS declared at least one scheme, every reference must resolve to a key in that map; unknown references produce a sorted list of error strings the caller can format. To express "this endpoint is public" use `@ignoreSecurity` at the method level rather than a sentinel scheme name.

Types

type CrossPkg

type CrossPkg map[string]string

CrossPkg maps a DSL package name (the target's `package X` declaration) to the Go import path under `<modulePath>/<typesOutputDir>/<X>`. Generators look up multi-part DSL refs (`shared.User`) by their first segment to decide which Go import statement to add.

A nil or empty CrossPkg indicates no cross-package context - the generators emit only the standard-library imports they have always emitted.

func BuildCrossPkg

func BuildCrossPkg(proj *semantic.Project, cfg *config.Config, currentPkgName string) CrossPkg

BuildCrossPkg returns a fully-populated lookup table for every non-current package in the project. The current package is excluded so a self-reference (`design.Foo` inside `package design`) renders as bare `Foo` without dragging in a redundant Go import.

Caller passes `currentPkgName` = the package being generated; passing "" returns the entire project mapping (useful for tools that don't know the destination yet).

type EnumTable

type EnumTable map[string]*ast.EnumDecl

EnumTable is the per-target-package lookup of EnumDecls reachable from the package being generated. Local enums are keyed bare (`Color`); cross-package enums by qualified DSL form (`shared.Color`). The validator codegen consults this so a field typed `color shared.Color` emits the switch-case validity check for a qualified ref the local-only `pkg.Enums` lookup misses.

func BuildEnumTable

func BuildEnumTable(proj *semantic.Project, currentPkgName string) EnumTable

BuildEnumTable returns the lookup table for `currentPkgName`. Every enum declared anywhere in the project is included once.

type ErrorTable

type ErrorTable map[string]*ast.ErrorDecl

ErrorTable is the per-target-package lookup of ErrorDecls reachable from the package being generated. Mirrors TypeTable: local errors keyed bare (`NotFoundErr`), cross-package errors by qualified DSL form (`shared.NotFoundErr`). The OpenAPI emitter needs this to resolve `@errors(shared.NotFoundErr)` references against the right per-error response schema and to register the error body component.

func BuildErrorTable

func BuildErrorTable(proj *semantic.Project, currentPkgName string) ErrorTable

BuildErrorTable returns the lookup table for `currentPkgName`. Mirrors BuildTypeTable / BuildEnumTable.

type MiddlewareTable

type MiddlewareTable map[string]*ast.MiddlewareDecl

MiddlewareTable is the per-target-package lookup of MiddlewareDecls reachable from the package being generated. Same keying contract as the other tables, included for symmetry with the codegen-side lookups so a cross-pkg middleware rule has the same plumbed lookup as everything else.

func BuildMiddlewareTable

func BuildMiddlewareTable(proj *semantic.Project, currentPkgName string) MiddlewareTable

BuildMiddlewareTable returns the lookup table for `currentPkgName`.

type ProjectResolver

type ProjectResolver struct {
	Types       TypeTable
	Enums       EnumTable
	Scalars     ScalarTable
	Errors      ErrorTable
	Middlewares MiddlewareTable
	CrossPkg    CrossPkg
}

ProjectResolver bundles every per-package-target lookup table the codegen layer needs to resolve qualified cross-package references. One resolver per generated package; built by BuildProjectResolver from a semantic.Project + config.Config.

Pass it as a single parameter instead of plumbing 4-5 separate tables. Lookup methods are nil-tolerant — `(*ProjectResolver)(nil)` is a usable zero value that always misses, matching the legacy behaviour every callsite already handles for `nil` maps.

func BuildProjectResolver

func BuildProjectResolver(proj *semantic.Project, cfg *config.Config, currentPkgName string) *ProjectResolver

BuildProjectResolver assembles every table the codegen layer needs for `currentPkgName`. Returns a non-nil resolver with empty tables when proj is nil so callers don't have to nil-check before use.

func (*ProjectResolver) ImportPath

func (r *ProjectResolver) ImportPath(pkgAlias string) string

ImportPath returns the Go import path for the DSL package alias, or "" when the alias isn't in the cross-package map. Used by emit sites that need to register an import when they output a qualified Go identifier like `shared.ColorRed`.

func (*ProjectResolver) LookupEnum

func (r *ProjectResolver) LookupEnum(name string) *ast.EnumDecl

LookupEnum is the enum counterpart of [LookupType].

func (*ProjectResolver) LookupError

func (r *ProjectResolver) LookupError(name string) *ast.ErrorDecl

LookupError is the error counterpart of [LookupType].

func (*ProjectResolver) LookupMiddleware

func (r *ProjectResolver) LookupMiddleware(name string) *ast.MiddlewareDecl

LookupMiddleware is the middleware counterpart of [LookupType].

func (*ProjectResolver) LookupScalar

func (r *ProjectResolver) LookupScalar(name string) *ast.ScalarDecl

LookupScalar is the scalar counterpart of [LookupType].

func (*ProjectResolver) LookupType

func (r *ProjectResolver) LookupType(name string) *ast.TypeDecl

LookupType returns the type decl bound to name (bare for local, qualified `pkg.Name` for cross-package), or nil when no match.

func (*ProjectResolver) QualifierFor

func (r *ProjectResolver) QualifierFor(n *ast.NamedTypeRef) (string, string)

QualifierFor inspects a named ref and returns (goPrefix, importPath):

  • goPrefix is the Go package qualifier WITH trailing dot (e.g. `"shared."`) for cross-pkg refs, empty for local
  • importPath is the Go import path to register on the generated file when the prefix is non-empty

Returns ("", "") for nil receiver / nil ref / bare ref so callers can use the result unconditionally:

prefix, path := r.QualifierFor(n)
if path != "" { uses[path] = true }
emit(prefix + ed.Name + ...)

type ScalarTable

type ScalarTable map[string]*ast.ScalarDecl

ScalarTable is the per-target-package lookup of scalar declarations reachable from the package being generated. Local scalars are keyed by bare name (`OrderID`); cross-package scalars use the qualified DSL form (`shared.NonEmptyID`). The codegen consults the table when a field's declared type is a scalar so the scalar's decorators (e.g. `@format(email)` on `scalar Email`) inherit into the field's effective validator chain.

Empty / nil table disables inheritance and the generated validators only honour the field's own decorator list - the legacy single-package behaviour.

func BuildScalarTable

func BuildScalarTable(proj *semantic.Project, currentPkgName string) ScalarTable

BuildScalarTable returns the lookup table for `currentPkgName`. Every scalar declared anywhere in the project is included once; scalars from other packages are keyed by their qualified DSL form so a field typed `shared.NonEmptyID` resolves cleanly.

Returns nil when proj is nil - callers can still pass the result straight into GenerateValidatorsPackage without a guard.

type TypeTable

type TypeTable map[string]*ast.TypeDecl

TypeTable is the per-target-package lookup of TypeDecls reachable from the package being generated. Mirrors ScalarTable but for struct-shaped types: local types are keyed by bare name (`Order`), cross-package types by qualified form (`shared.Page`). The codegen consults the table to decide whether a field type carries its own `Validate()` method, so a qualified ref like `shared.Page<T>` (which the local-only `pkg.Types` lookup misses) still emits its recursive validate call.

func BuildTypeTable

func BuildTypeTable(proj *semantic.Project, currentPkgName string) TypeTable

BuildTypeTable returns the lookup table for `currentPkgName`. Every type declared anywhere in the project is included once, qualified for cross-package entries the same way scalars are.

Jump to

Keyboard shortcuts

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