Skip to content

feat(schemadsl): add generic @decorator syntax to the schema DSL - #3265

Draft
josephschorr wants to merge 1 commit into
authzed:mainfrom
josephschorr:worktree-schema-decorators
Draft

feat(schemadsl): add generic @decorator syntax to the schema DSL#3265
josephschorr wants to merge 1 commit into
authzed:mainfrom
josephschorr:worktree-schema-decorators

Conversation

@josephschorr

@josephschorr josephschorr commented Aug 6, 2026

Copy link
Copy Markdown
Member

Adds generic @decorator syntax to the schema DSL. Machinery only — no decorator is defined, and the production registry ships empty, so the feature is inert until one is added.

Syntax

use somefeature

@somedecorator
definition user {}

@somedecorator(count: 16, mode: hash)
definition document {
    @otherdecorator
    relation parent: @thirddecorator document | folder

    @otherdecorator(label: "note", on: true)
    permission view = parent->view
}

A decorator attaches to the declaration that follows it. Sites: definition, relation, permission, caveat, and individual subject types inside a relation's type list. A decorator on a partial applies to every definition that includes it — identical duplicates collapse, conflicting ones error.

Parameters are always named, separated by :. Values are integers, quoted strings, bools, or bare-identifier enums; the declared parameter type drives coercion. Parameters are stored in the registry's canonical order so generated schema text is stable.

Design

use flags are feature flags; decorators are how a schema applies the feature. The registry maps each decorator to its required flag, N decorators to 1 flag, so there is no use decorators meta-flag.

The parser validates shape only and never consults the registry — @anything(foo: bar) parses. Names, sites, parameters, and flag gating are all checked by the compiler, which is what lets errors carry source positions.

Decorators are stored as a generic repeated Decorator field on NamespaceDefinition, Relation, CaveatDefinition, and AllowedRelation, so a new decorator costs a registry entry and nothing else — no proto change, no generator change.

Included fixes

Two pre-existing issues this depends on:

  • use flags are now collected before partials are translated. Previously collectPartials ran first, so anything inside a partial body validated against an empty flag set.
  • translateUseFlag now enforces the deployment's allowed-flag set, closing an existing TODO.

Behavior changes

⚠️ A schema declaring a bare use import — with no actual import statement — now fails to compile, because WriteSchema applies DisallowImportFlag() unconditionally. Same shape for use expiration where that flag is disabled. This is the intended effect of enforcing the flag list.

⚠️ Breaking (Go API): generator.GenerateCaveatSource now returns (string, []string, bool, error). It previously discarded the use flags a caveat requires, which would have produced generated schemas that do not recompile.

⚠️ Downstream note: lexer.Flags and lexer.AllUseFlags are exported, and a test-only testdecorators flag is registered in test binaries via a testing.Testing() guard. Downstream consumers (zed, playground) will therefore see it in their test binaries — any golden test asserting the Unknown use flag: … Options are: list will need updating. It cannot reach a production binary.

Notes on string values

The DSL has no escape syntax — the lexer scans raw for the closing quote and unquoting is a bare strings.Trim. Decorator string values are therefore emitted with whichever quote style the value does not contain, and values that cannot be represented (containing both quote styles, or a newline) are rejected at compile time rather than silently corrupted on regeneration. This matters because ComputeSchemaHash hashes generated source and that hash rides in ZedTokens; round-trip must be a fixed point.

Testing

Registry validation, parser fixtures at every site, compiler validation per error case, generator round-trip (including hostile string values), diff deltas, and partial merge semantics. A fixture registry exercises parameter and site combinations no real decorator is expected to have.

Follow-ups

  • Surface a real decorator diff message in the reflection APIs — the delta types are currently handled as explicit no-ops.
  • pkg/schema/v2 does not carry decorators through ToDefinitions(); dormant today (no callers outside the package) but needed before a core→v2→core round trip can preserve them.

@github-actions github-actions Bot added area/schema Affects the Schema Language area/api v1 Affects the v1 API area/tooling Affects the dev or user toolchain (e.g. tests, ci, build tools) labels Aug 6, 2026
Adds `@name` and `@name(param: value)` decorators to the schema DSL. A decorator
attaches to the declaration that follows it, and is validated against a closed
registry that declares, per decorator, its legal sites, its parameters, and the
`use` feature flag that enables it. Several decorators may share one flag.

Sites: definition, relation, permission, caveat, and individual subject types
within a relation's type list. A decorator on a `partial` applies to every
definition that includes that partial, collapsing identical duplicates and
rejecting conflicting ones.

Parameters are always named and separated by `:`. Values are integers, quoted
strings, bools, or bare-identifier enum values; the declared parameter type
drives coercion, and parameters are stored in the registry's canonical order so
generated schema text is stable.

This ships the machinery only. No decorator is defined and the production
registry is empty, so the feature is inert until one is added; a fixture
registry drives the tests. Decorators are stored on the compiled protos, emitted
back out by the generator along with the `use` lines they require, and surfaced
as deltas by schema diffing.

Two pre-existing issues the feature depends on are also fixed: `use` flags are
now collected before partials are translated, and translateUseFlag enforces the
deployment's allowed-flag set, closing an existing TODO. As a result, a schema
declaring a bare `use import` (or `use expiration` where that flag is disabled)
now fails to compile where it previously succeeded, since WriteSchema applies
DisallowImportFlag unconditionally.

Note that the schema DSL has no string escape syntax, so decorator string values
are emitted with whichever quote style the value does not contain, and values
that cannot be represented are rejected at compile time rather than silently
corrupted on regeneration.

BREAKING CHANGE: generator.GenerateCaveatSource now returns
(string, []string, bool, error). It previously discarded the `use` flags a
caveat requires, which would have produced generated schemas that do not
recompile.
@josephschorr
josephschorr force-pushed the worktree-schema-decorators branch from 6584012 to e046a32 Compare August 9, 2026 00:11
@github-actions github-actions Bot added the area/datastore Affects the storage system label Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/api v1 Affects the v1 API area/datastore Affects the storage system area/schema Affects the Schema Language area/tooling Affects the dev or user toolchain (e.g. tests, ci, build tools)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant