Documentation
¶
Overview ¶
Package patch provides JSON Patch (RFC 6902) and Merge Patch (RFC 7396) parsing with struct-tag-driven schema validation, SQL SET clause generation, and policy enforcement.
Struct Tags ¶
Patch schema generation is driven by struct tags. The following tags are recognized by GeneratePatchSchema:
parse:"patch" — include in patch schema patch_ops:"replace,add" — allowed operations (add, remove, replace, move, copy, test, array_append) enum:"active,inactive" — restrict values to this set db:"column_name" — SQL column name for SET clause (default: snake_case of field name)
The tag key for field names (default "json") and column names (default "db") can be changed via core.WithFieldNameTag and core.WithColumnTag.
Parsing ¶
Parser validates JSON Patch documents against a PatchSchema:
p := patch.NewParser(schema, nil) parsed, err := p.Parse(body) // validates operations, pointers, nullability, values, enums
ParseMergePatch handles RFC 7396 Merge Patch (plain JSON with null = remove):
parsed, err := patch.ParseMergePatch(body, schema, nil)
SQL Generation ¶
BuildPatchSQL converts a ParsedPatch into a parameterized SQL SET clause, one expression per column; operations SQL cannot express (move, copy, test, nested paths) are rejected, and merge-patch objects merge into the stored document:
result, err := patch.BuildPatchSQL(parsed, filter.PostgresDialect{},
patch.WithAutoTimestamp("updated_at"),
)
// result.SetClause = "name = $1, status = $2, updated_at = $3"
// result.Args = ["new-name", "active", <time.Time>]
Policy Enforcement ¶
ApplyPatchPolicyConstraints validates parsed patches against policy rules: forbidden paths, allowed/denied values, regex patterns, and custom validators.
pc := &patch.PatchPolicyConstraints{
ForbiddenPaths: []string{"/tenant_id", "/internal_*"},
ValueConstraints: map[string]*patch.ValueConstraint{
"/status": {AllowedValues: []any{"active", "inactive"}},
},
}
err := patch.ApplyPatchPolicyConstraints(parsed, pc, subject)
Index ¶
- Constants
- Variables
- func ApplyPatchPolicyConstraints(patch *ParsedPatch, pc *PatchPolicyConstraints, subject *core.PolicySubject) error
- func Codes() []core.Code
- func DefaultPatchOps() map[core.FieldType][]PatchOp
- func ServerFieldName(g *core.SchemaGenerator, field *core.Field) (name string, ok bool)
- func ValidateTransferValue(op *PatchOperation, value any, codec core.Codec, index int) error
- type ParsedPatch
- type Parser
- type PatchOp
- type PatchOperation
- type PatchPolicyConstraints
- type PatchSQLOption
- type PatchSQLResult
- type PatchSchema
- type PatchValidator
- type PathSchema
- type RawPatchOp
- type RelationOwner
- type RelationPatchOp
- type RelationVerb
- type RenderRelationOpOptions
- type RenderedRelationStatement
- type SQLDialect
- type ValueConstraint
Constants ¶
const ( ECodeOpNotAllowed core.Code = "E2064" // ECodePathTooDeep shares E2074 with filter's ECodeDepthExceeded: one // ParserConfig.MaxDepth bounds filter nesting and patch pointer depth. ECodePathTooDeep core.Code = "E2074" ECodeTooManyOps core.Code = "E2066" // ECodeInputTooLarge shares E2063 with filter: one MaxInputSize bounds // filter and patch bodies. ECodeInputTooLarge core.Code = "E2063" ECodeInvalidPatchOp core.Code = "E2075" ECodePatchFieldNotFound core.Code = "E2076" ECodePatchValueInvalid core.Code = "E2077" ECodePatchSchemaNotFound core.Code = "E2078" ECodePatchPolicyForbidden core.Code = "E2084" ECodePatchPolicyValue core.Code = "E2085" ECodeInvalidJSON core.Code = "E2086" // relation-patch error codes. ECodeRelationVerbNotAllowed core.Code = "E2117" ECodeRelationValueInvalid core.Code = "E2118" ECodeRelationPatchTooLarge core.Code = "E2119" // / P-13i — SQL-compilation refusals, in the parse gap // E2068-E2069. ECodeTestOpSQL core.Code = "E2068" ECodeSQLColumnMissing core.Code = "E2069" // an accepted operation BuildPatchSQL cannot express // (move, copy, a nested JSON Pointer path) is rejected, never skipped. ECodeOpNotSQLCompilable core.Code = "E2070" )
Error codes for patch operations.
Variables ¶
var ValidPatchOps = map[PatchOp]bool{ PatchOpAdd: true, PatchOpRemove: true, PatchOpReplace: true, PatchOpMove: true, PatchOpCopy: true, PatchOpTest: true, PatchOpArrayAppend: true, }
ValidPatchOps is the set of all recognized patch operations.
Functions ¶
func ApplyPatchPolicyConstraints ¶
func ApplyPatchPolicyConstraints(patch *ParsedPatch, pc *PatchPolicyConstraints, subject *core.PolicySubject) error
ApplyPatchPolicyConstraints validates a ParsedPatch against policy constraints.
func DefaultPatchOps ¶
DefaultPatchOps returns a defensive copy of the package's default FieldType -> patch-ops map. Callers can mutate the returned map without affecting the package-level defaults or other callers.
func ServerFieldName ¶
ServerFieldName returns the patch path name of a server field: its external name, or its column when the field-name tag hides it. ok is false for a field that is not a server field.
func ValidateTransferValue ¶
ValidateTransferValue validates the runtime source of a copy or move against its destination. The source must be resolved from the document at the current operation, after earlier operations have run.
Types ¶
type ParsedPatch ¶
type ParsedPatch struct {
Operations []*PatchOperation
EstimatedCost int
AffectedPaths []string
PolicyConstraints *PatchPolicyConstraints
}
ParsedPatch is the result of parsing a JSON Patch document.
func ParseMergePatch ¶
func ParseMergePatch(body []byte, schema *PatchSchema, config *core.ParserConfig) (*ParsedPatch, error)
ParseMergePatch parses a JSON Merge Patch (RFC 7396) body into a ParsedPatch.
func ParseMergePatchMap ¶
func ParseMergePatchMap(raw map[string]any, schema *PatchSchema, config *core.ParserConfig) (*ParsedPatch, error)
ParseMergePatchMap is ParseMergePatch for a merge patch already decoded, or built by the server: a nested map merges into a document member, any other value — a Go value of the path's type included (time.Time, uuid.UUID, a struct) — replaces, and null removes or clears. Null is whatever encoding/json writes as null: nil, or a nil pointer, map or slice. It avoids the JSON round trip of building a body only to parse it again.
func (*ParsedPatch) AuditInfo ¶
func (p *ParsedPatch) AuditInfo() map[string]any
AuditInfo returns a map of audit-relevant information.
type Parser ¶
type Parser struct {
// contains filtered or unexported fields
}
Parser parses and validates JSON Patch documents.
func NewParser ¶
func NewParser(schema *PatchSchema, config *core.ParserConfig) *Parser
NewParser creates a new patch Parser.
func (*Parser) Parse ¶
func (p *Parser) Parse(body []byte) (*ParsedPatch, error)
Parse validates and parses a JSON Patch document from JSON bytes.
func (*Parser) ParseOps ¶
func (p *Parser) ParseOps(ops []RawPatchOp) (*ParsedPatch, error)
ParseOps validates pre-decoded patch operations against the schema. Use this when the input has already been decoded from a non-JSON format (protobuf, msgpack, YAML, etc.).
type PatchOp ¶
type PatchOp string
PatchOp represents a JSON Patch operation type.
const ( PatchOpAdd PatchOp = "add" PatchOpRemove PatchOp = "remove" PatchOpReplace PatchOp = "replace" PatchOpMove PatchOp = "move" PatchOpCopy PatchOp = "copy" PatchOpTest PatchOp = "test" // PatchOpArrayAppend is the extension to JSON Patch RFC 6902 // for atomic single-statement array append. BuildPatchSQL renders it // to dialect-specific SQL: // // Postgres: col = array_append(COALESCE(col, '{}'), $1) // SQLite: col = json_insert(COALESCE(col, '[]'), '$[#]', $1) // // Concurrent appenders converge without read-modify-write. PatchOpArrayAppend PatchOp = "array_append" )
type PatchOperation ¶
type PatchOperation struct {
Op PatchOp
Path string
From string
Value any
Schema *PathSchema
// Merge marks an op derived from an RFC 7396 merge patch whose value is
// an object — it requires RECURSIVE merge semantics, not whole-value
// replace. The apply path deep-merges via
// internal/jsonmerge; BuildPatchSQL compiles it to json_patch on SQLite
// and per-level jsonb construction on PostgreSQL, so sibling keys survive.
Merge bool
}
PatchOperation represents a validated patch operation.
type PatchPolicyConstraints ¶
type PatchPolicyConstraints struct {
ForbiddenPaths []string
ValueConstraints map[string]*ValueConstraint
Validators []PatchValidator
}
PatchPolicyConstraints holds policy restrictions for patch operations.
func PatchPolicyFromWritableFields ¶
func PatchPolicyFromWritableFields(allPaths []string, writable map[string]bool) *PatchPolicyConstraints
PatchPolicyFromWritableFields builds PatchPolicyConstraints with ForbiddenPaths for every path NOT in the writable map.
type PatchSQLOption ¶
type PatchSQLOption func(*patchSQLConfig)
PatchSQLOption configures BuildPatchSQL behavior.
func WithAutoTimestamp ¶
func WithAutoTimestamp(col string) PatchSQLOption
WithAutoTimestamp adds an automatic timestamp column to the SET clause.
func WithAutoTimestampSQL ¶
func WithAutoTimestampSQL(col, expr string) PatchSQLOption
WithAutoTimestampSQL is WithAutoTimestamp with the column set to a SQL expression — the database's clock, NOW() — instead of a bound time, for a repository whose other writes stamp the database's clock: two clocks make timestamps incomparable once they skew.
func WithStartParam ¶
func WithStartParam(n int) PatchSQLOption
WithStartParam offsets the SET clause's placeholders by n: the first is n+1, as with filter.WithStartParam, so one zero-based offset composes both builders. Default 0 (first placeholder $1).
func WithTimeFunc ¶
func WithTimeFunc(fn func() time.Time) PatchSQLOption
WithTimeFunc overrides the function used to generate timestamps.
type PatchSQLResult ¶
type PatchSQLResult struct {
// SetClause is the comma-separated `col = $N` fragment ready to drop
// into UPDATE … SET. Empty when the patch only touches relation
// fields (junction-only patches).
SetClause string
// Args are the bound values for SetClause placeholders, in $N order.
Args []any
// ParamCount is the number of placeholders consumed by SetClause.
// Callers append filter params starting at startParam + ParamCount.
ParamCount int
// Empty is true when neither the column SET clause nor any relation
// op produced output — the caller can short-circuit with no DB writes.
Empty bool
// RelationOps holds parsed-but-unrendered junction operations
// extracted from the patch. The repository decorator
// runs these alongside the owner UPDATE in a single transaction:
// the UPDATE runs first (or is skipped when SetClause is empty),
// then for each RelationOp the decorator emits a junction
// DELETE / INSERT bound to the affected owner PKs returned by the
// UPDATE … RETURNING clause.
RelationOps []RelationPatchOp
}
PatchSQLResult holds the generated SQL SET clause and arguments.
func BuildPatchSQL ¶
func BuildPatchSQL(patch *ParsedPatch, dialect SQLDialect, opts ...PatchSQLOption) (*PatchSQLResult, error)
BuildPatchSQL converts a ParsedPatch into a SQL SET clause and a list of relation patch ops.
Scalar-column operations fold, in order, into ONE assignment per column : replace/add set the column to the value, remove sets it to NULL, array_append appends and an RFC 7396 merge merges into whatever the column holds at that point in the sequence. Emitting one assignment per operation lost every write but the last to a repeated column (SQLite keeps the last duplicate assignment). Operations before a column's final replace/add/remove are overwritten and bind no placeholder. Columns appear in first-touched order.
Relation ops (PathSchema.Relation != nil) split off into the result's RelationOps slice; the repository decorator is responsible for emitting the junction SQL bound to the affected owner PKs in the same transaction as the owner UPDATE.
Operations SQL cannot express are rejected, never skipped: test (E2068), move/copy and nested JSON Pointer paths (E2070). Apply those through the typed document path instead.
type PatchSchema ¶
type PatchSchema struct {
Resource string
AllowedPaths map[string]*PathSchema
MaxOperations int
// contains filtered or unexported fields
}
PatchSchema defines allowed patch operations for a resource.
func GeneratePatchSchema ¶
func GeneratePatchSchema[T any](g *core.SchemaGenerator) (*PatchSchema, error)
GeneratePatchSchema generates a PatchSchema from struct tags on type T.
func GenerateServerPatchSchema ¶
func GenerateServerPatchSchema[T any](g *core.SchemaGenerator) (*PatchSchema, error)
GenerateServerPatchSchema generates the patch schema of the server's own writes: every field GeneratePatchSchema admits, and every field tagged `parse:"server"` — a column only the server sets, such as a credential's fingerprint, which a request body must never reach. A server field the field-name tag hides (`json:"-"`) is addressed by ServerFieldName.
type PatchValidator ¶
type PatchValidator func(op *PatchOperation, subject *core.PolicySubject) error
PatchValidator is a function that validates a patch operation against policy.
type PathSchema ¶
type PathSchema struct {
AllowedOps []PatchOp
DataType core.FieldType
// SQLType distinguishes declared JSON from native driver-value storage.
SQLType core.SQLType
// GoType is the Go type the path's value decodes into (the struct
// field's type, pointer included). It drives parse-time validation so an
// accepted operation always applies: numeric range
// against the exact kind, nullability (only pointer, slice, map and
// interface fields accept null or remove), array element types, and the
// shape of struct / map values. nil — hand-built schemas — validates by
// DataType alone over the widest numeric domain and admits null.
GoType reflect.Type
Validators []core.Validator
// EnumValues is the path's `enum` allowlist (also a Validator), for
// schema discovery.
EnumValues []string
SQLColumn string
// Relation is non-nil when the path patches an M2M relation rather
// than a scalar column. When set, BuildPatchSQL emits
// junction INSERT/DELETE statements into PatchSQLResult.RelationOps
// instead of touching SQLColumn (which is "" for relation paths).
// AllowedOps for a relation path is the relation-verb subset
// (replace / add / remove); RFC 6902's move/copy/test are rejected
// at validation with E2117.
Relation *core.RelationSchema
}
PathSchema defines a single patchable path.
func RelationPath ¶
func RelationPath(rel *core.RelationSchema, goType reflect.Type) *PathSchema
RelationPath is the patch path of a relation's membership: replace, add and remove of a slice of target keys (goType), routed to RelationOps.
func (*PathSchema) AllowsOp ¶
func (p *PathSchema) AllowsOp(op PatchOp) bool
AllowsOp checks whether op is in the path's allowed ops list.
func (*PathSchema) ClearedValue ¶
func (p *PathSchema) ClearedValue() any
ClearedValue is what removing the path, or setting it to null, stores: nil (SQL NULL) for pointers, interfaces and untyped paths, and the empty value for a slice or map. Slice and map columns are usually NOT NULL with an empty default, where NULL fails the write, and empty is a valid cleared value for a nullable one too — so clearing empties them on every backend.
func (*PathSchema) Nullable ¶
func (p *PathSchema) Nullable() bool
Nullable reports whether the path's Go type can hold null. Unknown types (GoType nil) are treated as nullable.
type RawPatchOp ¶
type RawPatchOp struct {
Op string `json:"op"`
Path string `json:"path"`
From string `json:"from,omitempty"`
Value any `json:"value,omitempty"`
// HasValue reports that the operation carried a value member, so an
// explicit null is told from no value at all. Parse sets it; a caller of
// ParseOps sets it for a null value — a non-nil Value implies it.
HasValue bool `json:"-"`
}
RawPatchOp is a decoded patch operation. Used directly by Parser.ParseOps for non-JSON inputs, and internally by Parser.Parse after JSON decoding.
type RelationOwner ¶
type RelationOwner struct {
Table, Key, Tenant string
}
RelationOwner is the table a relation's owners are rows of.
type RelationPatchOp ¶
type RelationPatchOp struct {
// Field is the filter-API field name that the patch path resolved
// to (e.g. "tags" for path "/tags"). Used for error messages and
// audit logs.
Field string
// Schema is the junction descriptor (junction table, source/target
// columns, optional tenant column).
Schema *core.RelationSchema
// Verb is the membership change verb derived from the wire op.
Verb RelationVerb
// Values is the list of target IDs the verb applies to. Empty is
// valid for RelationVerbReplace (clears the relation) but not for
// add / remove.
Values []any
}
RelationPatchOp is a parsed junction-membership change extracted from a JSON Patch document. The repository decorator binds these to the owner PKs returned by the owner UPDATE and emits the junction SQL inside the same transaction.
type RelationVerb ¶
type RelationVerb string
RelationVerb identifies how a relation patch op modifies junction membership. The wire-level op name (PatchOpReplace etc.) maps 1:1 to a verb at parse time.
const ( // RelationVerbReplace is full-diff: the relation becomes exactly the // supplied set. Implemented as a junction DELETE for rows whose // target is not in the new set, followed by an INSERT … ON CONFLICT // DO NOTHING for the supplied targets. RelationVerbReplace RelationVerb = "replace" // RelationVerbAdd unions the supplied targets into the existing set // (junction INSERT … ON CONFLICT DO NOTHING). RelationVerbAdd RelationVerb = "add" // RelationVerbRemove subtracts the supplied targets from the set // (junction DELETE WHERE target IN (…)). RelationVerbRemove RelationVerb = "remove" )
type RenderRelationOpOptions ¶
type RenderRelationOpOptions struct {
// TenantValue is bound into every junction statement as the value of
// RelationSchema.TenantColumn. Required when the schema declares a
// TenantColumn — rendering fails without it unless Unscoped is set
//. Ignored for unscoped junctions.
TenantValue any
// Unscoped deliberately renders a tenant-scoped junction without a
// tenant predicate. Never set it on a request path.
Unscoped bool
// Owner names the owner table, key and tenant column. A many-to-many
// with a target (RelationSchema.TargetTable) needs it: its inserts
// select the owner rows and the visible target rows instead of binding
// raw values, so no pair names a target the request cannot see.
Owner RelationOwner
// TargetVisible renders the predicate (starting " AND ", or "") that
// holds the target rows under alias to those the request may relate,
// binding values with bind (filter.VisibleRelated). Required with a
// TargetTable: inserts pair only visible targets, and deletes remove
// only pairs to them.
TargetVisible func(alias string, bind func(any) string) (string, error)
}
RenderRelationOpOptions configures RenderRelationOp.
type RenderedRelationStatement ¶
RenderedRelationStatement is one DELETE or INSERT emitted for a relation patch op. The repository decorator runs each statement on the open transaction in slice order; statements within one rendered op are atomic with the owner UPDATE that produced ownerPKs.
func RenderRelationOp ¶
func RenderRelationOp(op RelationPatchOp, dialect dialect.Dialect, ownerPKs []any, opts RenderRelationOpOptions) ([]RenderedRelationStatement, error)
RenderRelationOp turns a parsed RelationPatchOp into the dialect- specific SQL statements that apply the membership change to the junction table for the supplied owner PK set.
The output is a list of {SQL, Args} pairs the caller executes in order on an open transaction. .
Statement shapes:
replace, len(values) > 0: DELETE FROM j WHERE source = ANY/IN (pks) AND target NOT IN (vals) [AND tenant = t] INSERT INTO j (source, target [, tenant]) VALUES (pk×val) ON CONFLICT DO NOTHING replace, len(values) == 0: DELETE FROM j WHERE source = ANY/IN (pks) [AND tenant = t] -- clear add: INSERT INTO j (source, target [, tenant]) VALUES (pk×val) ON CONFLICT DO NOTHING remove: DELETE FROM j WHERE source = ANY/IN (pks) AND target IN (vals) [AND tenant = t]
Cross-product semantics: a patch supplying values [a,b,c] applied to PK set [p1,p2] inserts six rows (p1,a),(p1,b),(p1,c),(p2,a),…,(p2,c). "Replace" diffs per (pk, target) — each PK in ownerPKs gets exactly the supplied value set. Multi-PK patch is allowed but uncommon; the usual call shape is one PK at a time.
type SQLDialect ¶
SQLDialect is the dialect surface BuildPatchSQL needs: placeholders and a name for dialect-specific SQL, plus value binding for composite types (a PostgreSQL TEXT[] column takes a Go slice, a SQLite array column takes JSON text). Requiring MarshalValue in the signature makes the wrong binding uncompilable instead of a runtime "malformed array literal". The parse/filter dialects implement it.
type ValueConstraint ¶
type ValueConstraint struct {
AllowedValues []any
DeniedValues []any
Pattern string
Min *float64
Max *float64
// contains filtered or unexported fields
}
ValueConstraint restricts values for a specific patch path.
func NewValueConstraint ¶
func NewValueConstraint(pattern string, allowed, denied []any, min, max *float64) (*ValueConstraint, error)
NewValueConstraint creates a ValueConstraint and eagerly compiles the regex pattern (if set), returning an error for invalid patterns. Prefer this over direct struct construction to catch regex errors at startup.