restcompat

package
v0.9.0 Latest Latest
Warning

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

Go to latest
Published: Aug 3, 2026 License: MIT Imports: 4 Imported by: 0

Documentation

Overview

Package restcompat diffs the REST contract two schemas generate, and classifies each delta as breaking, additive, or neutral for a deployed client. It is the engine behind `sqlb impact` (ADR-0039: a schema edit is an API edit, and the break is diffed).

It is a sibling of migrate.Diff, not a consumer of it. migrate.Diff reads the columns and types that produce DDL and ignores capabilities, because capabilities emit no SQL. This reads the capabilities — Filterable, the Op set, exposure — precisely because the sharpest API breaks produce no DDL at all: un-exposing a column, dropping an operation, or a rename that is a clean migration and a wire break at the same time. So the two functions run over the same pair of registries and read different projections of them.

Like migrate.Diff it is a pure function over two *schema.Registry values, with no database and no running server, which is what makes it golden-testable.

What is deliberately honest here

A change that is compatible for a reader and breaking for a writer — a column going NOT NULL widens the create body's required set while leaving responses untouched — is reported as two separate breaks, one per side, never folded into one. A classifier that reported the reader side and forgot the writer side would be a guard that fires sometimes, which reads as coverage it does not have (ADR-0016). Where a type change cannot be classified confidently in both directions, it is reported LevelUnknown rather than guessed as neutral.

Index

Constants

View Source
const SnapshotVersion = 1

SnapshotVersion is the format version of a captured contract. It is stamped into every Snapshot so a future format change can be recognised rather than misread — the snapshot is a checked-in artefact, and ADR-0039 flags its format as the expensive thing to change once teams have committed one. This format is still experimental and may change before it is frozen.

Variables

This section is empty.

Functions

This section is empty.

Types

type ActionPropSnap added in v0.5.0

type ActionPropSnap struct {
	Name       string   `json:"name"`
	Type       string   `json:"type"`
	Enum       []string `json:"enum,omitempty"`
	Nullable   bool     `json:"nullable,omitempty"`
	HasDefault bool     `json:"has_default,omitempty"`
}

ActionPropSnap is one property of an action's request body.

type ActionSnap added in v0.5.0

type ActionSnap struct {
	Name string `json:"name"`
	Path string `json:"path"`
	// Body is the request body's properties, in declaration order.
	Body []ActionPropSnap `json:"body,omitempty"`
	// Writes names the columns the envelope persists. No client couples to it,
	// so a change here is neutral — but it widens or narrows what one route
	// can mutate, which is exactly the blast-radius question this tool is for.
	Writes []string `json:"writes,omitempty"`
}

ActionSnap is one declared verb's contract.

type Break

type Break struct {
	Level    Level
	Resource string // the collection path, e.g. "/posts"
	Facet    Facet
	Field    string // column or relation name; empty for resource- and ops-level
	Summary  string // one line, in the allow-list voice: what changed, for whom
}

Break is one classified delta between two generated contracts.

func Breaking

func Breaking(breaks []Break) []Break

Breaking returns only the breaks a strict gate would fail on — the breaking ones and the unknowns, which cannot be shown safe. This is what `--api-compat=error` would count.

func Diff

func Diff(old, new *schema.Registry) []Break

Diff reports how the REST contract changes moving from old to new. The result is deterministic: sorted by resource, then by facet, then by field. An empty result means the contract is byte-for-byte compatible.

This is the convenience form for two registries in hand. `sqlb impact` diffs a registry against a checked-in Snapshot instead — see DiffSnapshots — because "backward compatible relative to what?" needs a committed answer, not the other side of a comparison that only exists at generation time.

func DiffSnapshots

func DiffSnapshots(old, new Snapshot) []Break

DiffSnapshots is Diff over two captured contracts. It is what the CLI runs: the old side is read from a file in the repository, the new side is captured from the current schema.

func (Break) String

func (b Break) String() string

type Facet

type Facet string

Facet names the part of the contract a break sits in. Facets are ordered so that a resource-level break sorts above the field-level breaks under it.

const (
	FacetResource Facet = "resource"    // the endpoint set as a whole
	FacetOps      Facet = "ops"         // which operations exist
	FacetResponse Facet = "response"    // the fields a read returns
	FacetFilter   Facet = "filter"      // ?column=op.value parameters
	FacetSort     Facet = "sort"        // ?sort columns
	FacetExpand   Facet = "expand"      // ?expand relations
	FacetCreate   Facet = "create-body" // the POST body
	FacetPatch    Facet = "patch-body"  // the PATCH body
	FacetAction   Facet = "action"      // a declared domain verb and its body
)

type FieldSnap

type FieldSnap struct {
	Name        string   `json:"name"`
	Rel         string   `json:"rel,omitempty"`
	Type        string   `json:"type"`
	Array       bool     `json:"array,omitempty"`
	Size        int      `json:"size,omitempty"`
	Enum        []string `json:"enum,omitempty"`
	Nullable    bool     `json:"nullable,omitempty"`
	HasDefault  bool     `json:"has_default,omitempty"`
	Hidden      bool     `json:"hidden,omitempty"`
	ReadOnly    bool     `json:"read_only,omitempty"`
	Immutable   bool     `json:"immutable,omitempty"`
	Filterable  bool     `json:"filterable,omitempty"`
	Sortable    bool     `json:"sortable,omitempty"`
	SortNulls   string   `json:"sort_nulls,omitempty"`
	Expandable  bool     `json:"expandable,omitempty"`
	RenamedFrom string   `json:"renamed_from,omitempty"`
}

FieldSnap is one column's contract-relevant shape. Storage-only properties — the primary-key flag, the index list, the constraint names — are deliberately absent: they do not change how a client couples to the field.

type Level

type Level int

Level is how a contract delta lands on a client that already exists.

const (
	// LevelNeutral: no client is affected. A response field going from nullable
	// to always-present is neutral for a reader that already handled the type.
	LevelNeutral Level = iota
	// LevelAdditive: a new capability. Nothing that a client sends or reads
	// today changes meaning; a client that ignores the addition is unaffected.
	LevelAdditive
	// LevelBreaking: an existing client can break — a request that worked now
	// fails, or a response field it relied on is gone or changed shape.
	LevelBreaking
	// LevelUnknown: the delta is real but its effect depends on how a specific
	// client generated its types (a widened integer overflows a narrow one), so
	// it is surfaced for review rather than claimed safe. Treat it as breaking
	// under a strict gate.
	LevelUnknown
)

func (Level) String

func (l Level) String() string

type ResourceSnap

type ResourceSnap struct {
	Path   string      `json:"path"`
	Ops    []string    `json:"ops"` // create, read, update, delete, list
	Fields []FieldSnap `json:"fields"`
	// Actions are the declared domain verbs. A snapshot recorded before they
	// existed has none, which reads correctly: every verb in the new schema is
	// an addition.
	Actions []ActionSnap `json:"actions,omitempty"`
}

ResourceSnap is one exposed table's contract.

type Snapshot

type Snapshot struct {
	Version   int            `json:"version"`
	Resources []ResourceSnap `json:"resources"`
}

Snapshot is the serialisable REST contract of a schema: one entry per exposed resource, holding exactly the capabilities a client couples to and nothing about storage. It is what `sqlb impact -write` records and what a later run diffs against.

func Capture

func Capture(r *schema.Registry) Snapshot

Capture projects a registry into its serialisable REST contract. It is the same projection Diff uses, exposed so the CLI can record and re-read it. Resources are sorted by path so a re-record produces a minimal file diff.

Jump to

Keyboard shortcuts

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