Documentation
¶
Overview ¶
Package resourcepolicy is the foundational slice of the model-first resource generator (ADR-016, issue #352). It defines the `gombit` field-policy tag and resolves, per model field, the API behavior the generator derives DTOs, mappers, and CRUD plumbing from.
Ownership of facts is deliberately split (ADR-016): the GORM schema owns persistence facts (DB-backed / primary key / auto-increment / DB default / nullability / auto timestamp / soft-delete / column name) and policy MUST NOT restate them. This resolver carries those facts losslessly — it never collapses them — because create-source correctness depends on the exact combination (a manual string key is not auto-generated; a NOT NULL column with no default must be supplied on create). The `gombit` tag owns only API behavior:
read the field appears in the response DTO write the field is settable via the create request DTO (create source = request) server the field is set server-side (a hook), not from the request (create source = server) - the field is hidden from the API (no request, no response)
and the list endpoint's declared query surface (all opt-in, all requiring the field to be response-visible):
filterable exact-match ?<field>= query param sortable ?ordering=<field> (- prefix for DESC) searchable matched by ?search= aggregatable ?aggregate=sum:<field>,… (numeric columns)
`-` may combine with `server` (hidden from the API but set by a hook); it may not combine with `read`/`write`, and `write`+`server` conflict. A query capability requires the field to be response-visible: a hidden or write-only column with a query surface leaks through result membership, counts, and ordering, so Resolve rejects it. A field with no `gombit` tag defaults by kind: a regular content column and a manual primary key are read+write from the request; an auto-increment key and auto timestamps are read-only; soft-delete is hidden. Resolve rejects a required column (NOT NULL / primary key, no `default:` clause, not DB-generated) that ends up with no create source — the silent zero-fill #352 exists to eliminate. It does not evaluate whether a default's SQL yields a non-null value; the database enforces that and fails loudly, so a `default:` clause defers to the database.
Persistence facts are supplied by the caller (slice 2 reads them from the model); this slice is pure and side-effect free.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func RequestColumns ¶
RequestColumns returns the DB column names settable via the create request, sorted — the write surface the generated request DTO must expose.
func ResponseColumns ¶
ResponseColumns returns the DB column names surfaced in responses, sorted.
func ServerColumns ¶
ServerColumns returns the DB column names whose create value is set server-side (a hook), sorted — the create-time non-request source.
Types ¶
type CreateSource ¶
type CreateSource int
CreateSource is where a persisted field's create value comes from.
const ( // CreateSourceNone is for fields the create path does not set (auto-generated, // or a nullable/defaulted column left unset, or not DB-backed). CreateSourceNone CreateSource = iota // CreateSourceRequest: the value comes from the create request body. CreateSourceRequest // CreateSourceServer: the value is set server-side (a BeforeCreate hook). CreateSourceServer )
func (CreateSource) String ¶
func (s CreateSource) String() string
type Field ¶
type Field struct {
FieldFacts
Tag string
}
Field pairs a field's persistence facts with its raw `gombit` tag. It keeps the association 1:1: GORM can flatten two embedded fields to the same GoName with different columns and tags, so policy is never keyed by name.
func FromModel ¶
FromModel parses a GORM model (a pointer to a value, e.g. &Book{}) and returns the resolver input for its effective persisted columns, after validating that no relationship field carries a gombit policy (see FromSchema).
func FromSchema ¶
FromSchema maps a parsed GORM schema to resolver input over its EFFECTIVE persisted columns — ordered sch.DBNames → sch.FieldsByDBName — so a duplicate-column or shadowed field is included once, each carrying its own `gombit` tag. A belongs-to FK column is a persisted column and is included; the association object/slice fields (belongs-to/has-one/has-many/many2many) are not columns and are NOT emitted.
Selection and validation are separate. An explicit `gombit` policy on a field that is not an effective persisted column (a relationship, a `gorm:"-"` ignored field, or a column shadowed by another field) would otherwise be silently dropped — it lives outside the emitted set. FromSchema therefore validates EVERY policy-bearing field from GORM's authoritative per-field set (sch.Fields), keyed by field identity, not a name-keyed secondary index that collapses same-named embedded fields. A policy that cannot be honored fails closed. Then it emits the effective persisted columns.
type FieldFacts ¶
type FieldFacts struct {
GoName string // Go field name, e.g. "Title"
// Column is the DB column name (e.g. "title"). It is EMPTY exactly for a
// relationship/association field (has_many / many2many / belongs-to object) —
// there is no separate "DBBacked" flag to disagree with it; db-backedness is
// derived from Column != "" so an included field always has a column.
Column string
PrimaryKey bool
AutoIncrement bool // database-generated key (serial/identity)
AutoTime bool // auto create/update timestamp (autoCreateTime/autoUpdateTime)
SoftDelete bool // gorm.DeletedAt
NotNull bool // NOT NULL constraint
HasDefault bool // a `default:` clause is present. Whether that default's SQL expression actually yields a non-null value is the database's concern (it enforces NOT NULL and rejects a bad default loudly at migrate/insert), NOT something this pure layer evaluates.
// Creatable / Readable mirror GORM's per-field permissions (schema.Field
// Creatable/Readable, driven by the `->`/`<-` permission tags), independent of
// whether the field has a column. GORM OMITS non-creatable fields from INSERT
// and skips non-readable fields on SELECT, so policy must honor them or the API
// promises an operation persistence will not perform. Normal columns are both
// true; a `gorm:"->"` read-only/computed column is Creatable=false, and a
// `gorm:"->:false;<-:create"` column is Readable=false.
Creatable bool
Readable bool
}
FieldFacts are the persistence facts about a model field, owned by the GORM schema. Policy never restates these; it only layers API behavior on top. The facts are kept independent (not collapsed) because create-source correctness depends on their exact combination.
func FactsFromSchema ¶
func FactsFromSchema(f *schema.Field) FieldFacts
FactsFromSchema is the lossless projection of a parsed GORM field into FieldFacts. Soft-delete is read from the clauses schema.Parse stored on the field's own owning schema (f.Schema, which GORM sets during parse) — its authoritative parsed result — so gorm.DeletedAt, *gorm.DeletedAt, and any wrapper whose DeleteClauses rewrite DELETE into an UPDATE are recognized, in either the value or pointer clause form, while a generic clause provider that hard-deletes is not. Create/read capability comes from GORM's own Creatable/Readable (the `->`/`<-` permission tags); nothing is re-derived by hand. The field carries its own owner, so callers pass only the field.
type Resolved ¶
type Resolved struct {
FieldFacts
InRequest bool // settable via the create request DTO
InResponse bool // surfaced in the response DTO
CreateSource CreateSource // where a persisted create value comes from
// The list endpoint's declared query surface for this column. Each is opt-in
// via the gombit tag and requires the column to be response-visible.
Filterable bool
Sortable bool
Searchable bool
Aggregatable bool
}
Resolved is a field's resolved API behavior: its persistence facts plus where it appears in the API and where its create value comes from.
func Resolve ¶
func Resolve(facts FieldFacts, gombitTag string) (Resolved, error)
Resolve combines a field's persistence facts with its `gombit` tag into its API behavior. It errors on a contradictory tag (writing/server-setting a managed or non-creatable column, reading a non-readable one, write+server, tagging a relationship) and on a required column that no create source can satisfy — whether because the policy gives it none or because GORM never writes a valid value for it (a non-creatable, `<-:update` auto-time, or NOT NULL soft-delete column). The two questions "may the API write this?" (managed / capability) and "does a valid value reach the INSERT?" (dbSuppliesCreateValue) are kept apart.
func ResolveAll ¶
ResolveAll resolves every field, in input order, preserving each field's own facts+tag pairing (never keyed by the possibly-ambiguous Go name). It fails on the first contradictory field so the generator never emits from a bad policy.
func ResolvedFromModel ¶
ResolvedFromModel parses a model and resolves the API policy for each of its effective persisted columns. It returns the first policy error (a contradictory tag or an unsatisfiable required column), so a bad model fails closed.