language

package
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Oct 9, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

README

Language Package

User-facing language specification: See docs/language-spec.md. This README covers internal architecture and design decisions.

Overview

The language package parses and validates FGA mapping configurations. It transforms mapping YAML into a canonical MappingConfig, checks structural constraints via MappingConfig.Validate(), and stamps source positions onto the parsed structs so downstream consumers (the mapper package, IDE tooling) can attach diagnostics without reaching into the YAML AST.

It performs no expression compilation or evaluation — that is the mapper package's responsibility. language is the leaf of the two-package split: it has no dependency on mapper.

Versioning

The mapping format version is a single integer string ("1", "2", etc.) — no minor versions.

parseMapping() is a version router: parses AST, extracts version, dispatches to parseV1() / parseV2() etc. Each version parser (parse_v1.go, parse_v2.go) owns: YAML unmarshaling, strict field checking, variable extraction, and normalization to canonical MappingConfig. MappingConfig.Validate() validates canonical types — shared across versions. The mapper package's Compiler has zero version awareness; it only accepts canonical types.

Adding a new version: create parse_vN.go, add a case to the version switch in parseMapping(). Never drop support for a published version.

Canonical Types

  • Variables YAML format: YAML mapping (not list) with order preserved via custom UnmarshalYAML.
  • TupleAction: "write" (default) or "delete" — validated at parse time.
  • TupleFilterAction: "patch" (default) or "delete" — separate type from TupleAction since semantics differ (patch/delete operate on FGA read results, not individual tuples).
  • ParsedTupleFilter: YAML input for a tuple filter. Object is a required interpolated string (at least an object type prefix, since FGA's Read API needs an object type); User and Relation are optional interpolated strings; Action defaults to "patch". All-concrete (non-interpolated, non-type-prefix) three-field filters are rejected.
  • TupleFilter: Rendered tuple filter (mapper output). Carries User, Relation, and Object fields (any may be empty to act as a wildcard) plus Action. Filter-level Action is independent of tuple-level TupleAction.
  • Rule-level action: Rule.Action ("write" or "delete") propagates to all tuples during validateRule(), before validateTupleTemplate() runs. When set, tuple-level action is forbidden. Rule-level action: delete with patch tuple filters is a ValidationError.
  • Tuple condition and context: ParsedTuple.Condition (string, literal FGA condition name) and ParsedTuple.Context (map of interpolated strings). Validation: context requires condition; condition forbidden on action: delete tuples.
  • Tuple.Key(): Returns a length-prefixed string key encoding all tuple fields for use as a comparable map key. Tuple is non-comparable due to Context map[string]any. All dedup, diff, and test comparison logic uses Key().
  • Strict YAML parsing: parseMapping() passes yaml.DisallowUnknownField() to the single NodeToValue call. Rule.Variables is yaml:"-" (extracted from AST for ordering); a sink field RawVariables any tagged yaml:"variables,omitempty" absorbs the YAML key during unmarshal — its value is never read. Unknown field errors are converted to *ValidationError with position via toValidationError().

Validation Errors

*ValidationError is the sole error type produced by this package. It is distinguishable via errors.As() and always carries a Position.

The Category (surfaced via the mapper's Diagnostic) and Field are the stable, programmatically-checked contract; message text may be reworded over time. The user-facing spec links back here for this detail.

ValidationError Scenarios

All structural checks below run during Validate()/parsing (parser.go), before any event is evaluated, and all include a Position. {i}/{j} denote array indices in the field path.

Scenario Field Path Example Message
Missing version version is required
version wrong type version must be a string, got 123
Unsupported version version unsupported version "2" (supported: "1")
Unknown top-level or nested YAML field {field name} (from strict YAML unknown-field decoding, via toValidationError())
No rules rules must contain at least one rule
Too many rules (default >100, override with WithMaxRules) rules exceeds maximum of 100 rules
Rule missing name rules[{i}].name is required
Invalid rule-level action rules[{i}].action invalid action "freeze" (must be "write" or "delete")
Tuple-level action set alongside rule-level action rules[{i}].tuples[{j}].action tuple-level action is not allowed when rule has action "write"; remove the tuple-level action
Iterator present with no iterator.tuples rules[{i}].iterator.tuples must contain at least one tuple
Rule has neither tuples, iterator, nor tuple_filters rules[{i}].tuples must contain at least one tuple
tuples empty with a patch tuple filter present rules[{i}].tuples must contain at least one tuple when tuple_filters includes a patch filter
Tuple missing user rules[{i}].tuples[{j}].user is required
Tuple missing relation rules[{i}].tuples[{j}].relation is required
Tuple missing object rules[{i}].tuples[{j}].object is required
Invalid tuple-level action rules[{i}].tuples[{j}].action invalid action "pause" (must be "write" or "delete")
context set without condition rules[{i}].tuples[{j}].context context requires a condition name
condition set on an action: delete tuple rules[{i}].tuples[{j}].condition condition is not allowed on delete tuples
Empty variable name rules[{i}].variables[{j}].name is required
Empty variable expression rules[{i}].variables[{j}].expression is required
Duplicate variable name rules[{i}].variables[{j}] duplicate variable name "x" (first defined at index 0)
Iterator missing source rules[{i}].iterator.source is required
Iterator missing as rules[{i}].iterator.as is required
Iterator as shadows input/variables rules[{i}].iterator.as iterator "as" name "input" shadows the built-in input scope; choose a different name
More than 3 tuple filters rules[{i}].tuple_filters exceeds maximum of 3 filters
Tuple filter with no object rules[{i}].tuple_filters[{j}].object must be set to at least an object type prefix (e.g. "document:")
Tuple filter with all three fields concrete (no type prefix) rules[{i}].tuple_filters[{j}] filter with all three fields set to concrete values describes a single tuple, not a range; use tuple-level action instead
Invalid tuple filter action rules[{i}].tuple_filters[{j}].action invalid action "ignore" (must be "patch" or "delete")
delete filters combined with rule-level tuple templates rules[{i}].tuple_filters (rejected — see validateTupleFilters())
Test case missing name tests[{i}].name is required

Source Positions

ValidationError (and, downstream, the mapper's compile-time EvalError) includes a Position struct with 1-based line/column range data. A value of 0 means the position is unknown.

type Position struct {
    StartLine   int `json:"startLine"`   // 1-based, 0 = unknown
    StartColumn int `json:"startColumn"` // 1-based, 0 = unknown
    EndLine     int `json:"endLine"`     // 1-based, 0 = unknown
    EndColumn   int `json:"endColumn"`   // 1-based, 0 = unknown
}

Positions are resolved via a dual-parse approach: YAML is parsed once into structs for data and once into a yaml.Node tree for line/column lookup. Positions are stamped onto the parsed structs at parse time (positions.go, yamlpos.go), so consumers read line/column data from plain struct fields and never touch the node tree.

MappingConfig retains the AST (root) after parsing because Validate() needs it to resolve positions for structural errors, and Validate() must stay idempotent (callable repeatedly with positions intact). Once the final Validate() has run, call ClearRoot() to release the tree for garbage collection — mapper.Compile() does this automatically before returning a long-lived Mapping.

Usage Example

cfg, err := language.Parse(yamlBytes)
if err != nil {
    // Parse reports syntax errors only (malformed YAML, unknown version).
}

// Parse does not validate. Run Validate to check the mapping is well-formed;
// it returns (or joins) *language.ValidationError values.
if err := cfg.Validate(); err != nil {
    // Inspect the validation errors.
}
// cfg is now validated and ready to hand to mapper.Compile.

Documentation

Overview

Package language parses and validates FGA mapping configurations.

It transforms mapping YAML into a MappingConfig, checks structural constraints via MappingConfig.Validate, and stamps source positions onto the parsed structs so downstream consumers (the mapper package, IDE tooling) can attach diagnostics without reaching into the YAML AST. It performs no expression compilation or evaluation — that is the mapper package's responsibility.

Index

Constants

View Source
const (
	// DefaultMaxRules is the default cap on the number of rules a single mapping
	// file may declare. Override per-call with WithMaxRules.
	DefaultMaxRules = 100
)

Variables

This section is empty.

Functions

This section is empty.

Types

type Iterator

type Iterator struct {
	Source string        `yaml:"source"`
	As     string        `yaml:"as"`
	Tuples []ParsedTuple `yaml:"tuples"`

	SourcePos Position `yaml:"-"` // source position of the iterator source expression, stamped at parse time
}

Iterator represents a fan-out configuration that iterates over a collection.

type MappingConfig

type MappingConfig struct {
	Version string     `yaml:"version"`
	Rules   []Rule     `yaml:"rules"`
	Tests   []TestCase `yaml:"tests,omitempty"`
	// contains filtered or unexported fields
}

MappingConfig is the top-level structure parsed from a mapping YAML file. It contains the schema version, an ordered list of rules, and optional embedded test cases.

func Parse

func Parse(data []byte) (*MappingConfig, error)

Parse parses a YAML mapping file and returns the MappingConfig without compiling. Useful for inspecting rule structure (when guards, variables, tuple templates) without running full expression compilation.

func (*MappingConfig) ClearRoot

func (m *MappingConfig) ClearRoot()

ClearRoot releases the retained YAML AST node tree so it can be garbage collected. Source positions are stamped onto the parsed structs at parse time, so they survive; only Validate's ability to look up positions for structural errors depends on the tree. Call this once, after the final Validate, when a long-lived MappingConfig no longer needs position lookup — the compiled mapping reads only the flat Position fields, never the tree.

func (*MappingConfig) Validate

func (m *MappingConfig) Validate(opts ...ValidateOption) error

Validate checks structural constraints on a parsed MappingConfig. Source positions are attached to errors using the YAML node tree stored during parsing. Returns a joined error tree containing one or more *ValidationError values, or nil if the configuration is valid.

Validate does not consume the AST node tree; it may be called repeatedly and still attach source positions. Callers done with position data should call ClearRoot to release the tree for garbage collection.

type ParsedTuple

type ParsedTuple struct {
	When      string            `yaml:"when,omitempty"`
	Action    TupleAction       `yaml:"action,omitempty"`
	User      string            `yaml:"user"`
	Relation  string            `yaml:"relation"`
	Object    string            `yaml:"object"`
	Condition string            `yaml:"condition,omitempty"` // FGA condition name
	Context   map[string]string `yaml:"context,omitempty"`   // FGA context (values are interpolation templates)

	// Source positions stamped at parse time so the mapper can attach diagnostics
	// to compiled interpolations without reaching back into the YAML AST.
	WhenPos     Position            `yaml:"-"`
	UserPos     Position            `yaml:"-"`
	RelationPos Position            `yaml:"-"`
	ObjectPos   Position            `yaml:"-"`
	ContextPos  map[string]Position `yaml:"-"`
}

ParsedTuple is a tuple parsed from YAML whose User, Relation, and Object fields are interpolated strings rendered against the evaluation context (input, variables, iterator value).

type ParsedTupleFilter

type ParsedTupleFilter struct {
	User     string            `yaml:"user,omitempty"`
	Relation string            `yaml:"relation,omitempty"`
	Object   string            `yaml:"object,omitempty"`
	Action   TupleFilterAction `yaml:"action,omitempty"`

	UserPos     Position `yaml:"-"`
	RelationPos Position `yaml:"-"`
	ObjectPos   Position `yaml:"-"`
}

ParsedTupleFilter is a tuple filter parsed from YAML. Object is a required interpolated string; User and Relation are optional interpolated strings. Empty rendered fields act as wildcards.

The *Pos fields carry the source position of each interpolated field, stamped at parse time so the mapper can attach diagnostics without reaching back into the YAML AST.

type Position

type Position struct {
	StartLine   int `json:"startLine"`
	StartColumn int `json:"startColumn"`
	EndLine     int `json:"endLine"`
	EndColumn   int `json:"endColumn"`
}

Position represents a source location range in a YAML file. All fields are 1-based. A value of 0 means the position is unknown. Parent structs use json:"omitzero" to omit zero-valued positions from JSON.

type Rule

type Rule struct {
	Name         string              `yaml:"name"`
	When         string              `yaml:"when,omitempty"`
	Action       TupleAction         `yaml:"action,omitempty"`
	RawVariables any                 `yaml:"variables,omitempty"` // sink for strict YAML; unused -- variables extracted from AST
	Variables    Variables           `yaml:"-"`
	TupleFilters []ParsedTupleFilter `yaml:"tuple_filters,omitempty"`
	Iterator     *Iterator           `yaml:"iterator,omitempty"`
	Tuples       []ParsedTuple       `yaml:"tuples"`

	WhenPos Position `yaml:"-"` // source position of the when guard, stamped at parse time
}

Rule defines a mapping from an input event to one or more tuples. Each rule optionally filters via a When guard (Expr), computes Variables (Expr), iterates over a collection, and renders Tuples via interpolated strings ({{ expr }}).

type TestCase

type TestCase struct {
	Name                        string         `yaml:"name"`
	Input                       map[string]any `yaml:"input"`
	ExpectTuples                []Tuple        `yaml:"expect_tuples"`
	ExpectTupleFilters          []TupleFilter  `yaml:"expect_tuple_filters,omitempty"`
	AssertWritesCoveredByFilter bool           `yaml:"assert_writes_covered_by_filter,omitempty"`
}

TestCase represents an embedded test case defined in the mapping file.

type Tuple

type Tuple struct {
	User      string         `yaml:"user"      json:"user"`
	Relation  string         `yaml:"relation"  json:"relation"`
	Object    string         `yaml:"object"    json:"object"`
	Action    TupleAction    `yaml:"action,omitempty"    json:"action,omitempty"`
	Condition string         `yaml:"condition,omitempty" json:"condition,omitempty"` // FGA condition name
	Context   map[string]any `yaml:"context,omitempty"   json:"context,omitempty"`   // rendered FGA context
}

Tuple is a resolved FGA relationship tuple. The mapper produces values of this type; the language owns the type because embedded test cases (TestCase) declare expected tuples, keeping the data model in a single leaf package.

func (Tuple) Key

func (t Tuple) Key() string

Key returns a comparable string key for use as a map key. Includes all fields: user, relation, object, action, condition, and context. Uses length-prefixed encoding to avoid separator collision issues. Context is serialized as JSON (json.Marshal sorts map keys deterministically).

type TupleAction

type TupleAction string

TupleAction represents the action to perform on a tuple.

const (
	ActionWrite  TupleAction = "write"
	ActionDelete TupleAction = "delete"
)

type TupleFilter

type TupleFilter struct {
	User     string            `json:"user,omitempty"`
	Relation string            `json:"relation,omitempty"`
	Object   string            `json:"object,omitempty"`
	Action   TupleFilterAction `json:"action"`
}

TupleFilter is a rendered tuple filter produced by the mapper. Empty fields act as wildcards that match any value in the corresponding FGA Read API field.

type TupleFilterAction

type TupleFilterAction string

TupleFilterAction represents the action for a tuple filter. Semantically distinct from TupleAction: patch/delete operate on FGA read results, not individual tuples.

const (
	FilterActionPatch  TupleFilterAction = "patch"
	FilterActionDelete TupleFilterAction = "delete"
)

type ValidateOption

type ValidateOption func(*validateConfig)

ValidateOption configures a Validate call.

func WithMaxRules

func WithMaxRules(n int) ValidateOption

WithMaxRules overrides the default cap on the number of rules per mapping file. A non-positive value is ignored and the default is retained.

Note: the identically-named mapper.WithMaxRules instead treats a non-positive value as a configuration error surfaced from Compile, rather than a no-op.

type ValidationError

type ValidationError struct {
	Field    string
	Message  string
	Position Position
}

ValidationError represents a validation error in the mapping configuration.

func (*ValidationError) Error

func (e *ValidationError) Error() string

type Variable

type Variable struct {
	Name       string
	Expression string
	Position   Position // source position of the variable entry, stamped at parse time
}

Variable represents a named variable with an Expr expression to evaluate.

type Variables

type Variables []Variable

Variables is an ordered list of Variable, extracted from the YAML AST while preserving definition order for sequential evaluation.

Jump to

Keyboard shortcuts

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