inference

package
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Aug 13, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package inference derives a baseline OpenAPI Operation from what can be deduced statically from a route and its handler, with no help from the programmer: path parameters, a default response, and a few conventional defaults (operationId, summary). It never overrides anything the programmer wrote in a "gota:" comment — see internal/merger for that.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AmbiguousSchemaNames added in v0.2.0

func AmbiguousSchemaNames(pkgs []*packages.Package) map[string]bool

AmbiguousSchemaNames returns the set of top-level type names declared in more than one of pkgs. componentName qualifies exactly these with their package name (e.g. "author.UpdateRequest") so two distinct Go types sharing a name don't collide on one OpenAPI component. It's computed once and threaded to every schema-name producer (body inference and this package's registry) so the emitted $ref string and the stored component key always agree.

func DetectBody

func DetectBody(op *model.Operation, decl *ast.FuncDecl, info *types.Info, cmap ast.CommentMap, funcIndex map[types.Object]astutil.FuncDeclInfo, d Dialect, ambiguous map[string]bool)

DetectBody is a best-effort heuristic that walks decl's body looking for the request/response idioms d recognizes (see Dialect; the call shapes themselves — e.g. netHTTPDialect's Decode/Encode/WriteHeader/ http.Error set — live with each dialect, not here) and, when found, augments op with a requestBody and/or response schemas. A chain of helper functions wrapping a recognized call — same-package or not — is followed as if inlined, see "Following indirection" below.

A nil d disables detection entirely (the operation is still documented from routes and "gota:" comments, just without inferred bodies) — the honest degrade for a router plugin with no paired dialect.

The detected schema is a bare "$ref: '#/components/schemas/<Name>'" (or an array of one) using the same convention a hand-written "gota:" comment would use, so the ResolveSchemaRefs pass expands it into a real component the same way regardless of which source produced it.

Response detection walks the body respecting if/else branch boundaries: the status code in effect for a given Encode/Marshal/http.Error call is whatever the *nearest enclosing branch* last set via WriteHeader (or 200, Go's implicit default, if none did) — a WriteHeader in one branch never leaks into a sibling branch. Distinct status codes coexist as separate responses (e.g. a 200 success path and a 404 error path both get documented); if the *same* code is produced more than once, the last occurrence in source order wins.

This is not a general dataflow analysis, and a WriteHeader call whose argument isn't a compile-time constant is ignored (the code in effect is left unchanged) rather than guessed. Detecting nothing is silent, not an error: a "gota:" comment remains the reliable, explicit path, and always wins over whatever this infers.

Following indirection

funcIndex (typically internal/astutil.IndexFuncDecls over every loaded package) lets detection follow a call to a helper function — in the same package or a different one — that itself makes a call the dialect recognizes, or delegates further to another helper, up to maxFollowDepth calls deep. A parameter reference inside a followed helper's body resolves back to whatever expression was actually passed at its own call site, transitively through as many levels as it takes — e.g.:

// package httputil
func Success(w http.ResponseWriter, status int, data any) {
	JSON(w, status, map[string]any{"status": "success", "data": data})
}
func JSON(w http.ResponseWriter, status int, payload any) {
	w.WriteHeader(status)
	json.NewEncoder(w).Encode(payload)
}

// package products
httputil.Success(w, http.StatusCreated, product)

resolves all the way through: JSON's "payload" is Success's map literal, and "data" inside that literal is Success's own parameter, which resolves again to products' own "product" — two packages, two levels of helper, one detected 201 response with a real schema.

Followable helpers include functions and methods alike, as long as their declaration is in the analyzed module (that's what funcIndex spans) — a server struct's own s.respond(...) is followed the same as a free function, while a call outside the module (a stdlib or framework call) or through a function-typed variable declines. Declining to follow (silent, not an error) also happens for: a helper with a variadic parameter; an argument count that doesn't match the helper's parameter count; a chain already maxFollowDepth calls deep; and a helper already present earlier in the current chain (a direct or mutual cycle — declined immediately, not just eventually stopped by the depth cap, since nothing here does real dataflow analysis and an unbounded static call graph walk has no other way to terminate on a recursive helper). Pass a nil funcIndex to disable following entirely.

cmap, if non-nil, lets a specific statement opt out of response detection entirely via a "gota:" comment declaring "x-gota-skip: true" placed directly above it — e.g. an immature error path the developer doesn't want documented yet, without affecting the rest of the handler's detected responses. This only applies to response detection, not request body detection, and only to statements in decl's own body — a "gota:" comment inside a followed helper's body has no effect (cmap is built from decl's own file, and a shared helper has no single caller to scope a skip to anyway).

func DetectBodyWithRoots added in v0.3.3

func DetectBodyWithRoots(op *model.Operation, decl *ast.FuncDecl, info *types.Info, cmap ast.CommentMap, funcIndex map[types.Object]astutil.FuncDeclInfo, d Dialect, ambiguous, roots map[string]bool)

DetectBodyWithRoots is DetectBody plus roots — the set of analyzed root package import paths (see RootPaths). A detected response/request type declared OUTSIDE those roots (a dependency, or another go.work module) is package-qualified in its component name (componentName), so it resolves uniquely by its own package instead of colliding on a bare name with an unrelated same-named type elsewhere in the reachable graph. generate.Run passes roots; DetectBody passes nil, meaning "treat every type as a root" (bare names) — the correct standalone default when the caller doesn't distinguish roots from dependencies.

func Operation

func Operation(route router.Route) *model.Operation

Operation builds the inferred baseline Operation for route.

func ResolveSchemaRefs

func ResolveSchemaRefs(doc *model.Document, pkgs []*packages.Package, ambiguous map[string]bool) error

ResolveSchemaRefs finds every "$ref: '#/components/schemas/<Name>'" declared anywhere in doc (whether written by a "gota:" comment or produced by body inference), and generates the referenced component by looking up the matching Go type in pkgs and converting its structure into an OpenAPI Schema (fields, primitive types, omitempty -> required/optional, time.Time, slices, maps, embedding). Nested named struct types discovered along the way are registered as their own linked components rather than inlined, so the type graph in Go becomes a $ref graph in the spec.

ambiguous is the set of type names declared in more than one analyzed package (see AmbiguousSchemaNames); it must be the SAME set the inference-time producers used, so that a package-qualified $ref they emitted (e.g. "author.UpdateRequest") is looked up and stored under the identical component key.

A type is looked up in the analyzed root packages first, then — if not found there — in the full reachable import graph, so a $ref to a type declared in a dependency or another go.work module (a handler returning []*catalog.Product) resolves rather than aborting.

It returns an error only if a $ref names a schema with no matching Go type anywhere in that graph (typically a typo in a hand-written comment — an inferred $ref is always to a type already in the graph); if a HAND-WRITTEN comment references a bare name that's declared in more than one package (gota can't tell which was meant — an inferred $ref for such a name is pre-qualified and doesn't hit this); or if two distinct types would collide on one component key even after qualification (see register). It does not attempt any inference for operations that never declare a $ref.

func RootPaths added in v0.3.3

func RootPaths(pkgs []*packages.Package) map[string]bool

RootPaths returns the set of import paths of the analyzed root packages (pkgs). componentName qualifies a type whose package is NOT in this set (a dependency or other-module type), so it resolves uniquely by package instead of by a bare name that can collide across the reachable graph. generate.Run computes it once and passes it to DetectBodyWithRoots; ResolveSchemaRefs derives it the same way from the same pkgs, so emission and resolution agree on which names are qualified.

Types

type Dialect added in v0.2.0

type Dialect interface {
	// contains filtered or unexported methods
}

Dialect is the set of framework-specific call-shape recognizers DetectBody consults while walking a handler body. The shared walk machinery — branch-aware status tracking, helper-chain following, schema building — is framework-agnostic; a Dialect tells it which calls mean "decode the request body into X", "write a response", or "set the status code in effect".

The interface is deliberately sealed (unexported methods): dialects must live in this package, because the walk hands them unexported state (*evalCtx). Callers select one via the exported constructors (NetHTTP today; future frameworks add their own, paired with their router plugin at the generate.Options level).

A future framework dialect should embed netHTTPDialect and try its own recognizers first, falling back to the embedded ones: the encode/decode recognizers netHTTPDialect carries are encoding/json idioms, not net/http ones, and stay perfectly ordinary inside e.g. a Gin handler (json.NewDecoder(c.Request.Body).Decode(&x) — Gin's c.Request is a plain *http.Request).

func Gin added in v0.2.0

func Gin() Dialect

Gin returns the dialect recognizing gin-gonic/gin idioms. Pair it with the gin router plugin (internal/router/gin) at the generate.Options level. See ginDialect for the exact shapes.

func NetHTTP added in v0.2.0

func NetHTTP() Dialect

NetHTTP returns the dialect recognizing stdlib net/http + encoding/json idioms — the one every plain-net/http (and future Chi) router plugin pairs with. See netHTTPDialect for the exact shapes.

Jump to

Keyboard shortcuts

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