Repository F# setup
open Reified
open Reified.Refinements
open Reified.Result
open Reified.Schema.JsonContext and fallback
Context and attribute
A renderer holds two scoping roles, and they behave differently on purpose.
let signup = renderer |> Renderer.context "signup" // appends a segment
let name = signup |> Renderer.attribute "name" // replaces the whole attribute
let email = name |> Renderer.attribute "email" // "name" is gone, not nested
let bare = email |> Renderer.unscoped // both roles clearedcontext appends because a document, model, form, and component nest. attribute replaces because a form-scoped
renderer gets reused across sibling fields, and an appending attribute would quietly produce
signup.name.email on the second field.
The context is never used as a noun. With no attribute, fullMessage uses the contextual
constraint.attribute.default — "value" in English — so a whole-model failure reads "value must be present"
rather than "Signup must be present".
Schema supplies its typed path as the attribute for you; see Redisplay and field errors.
Message fallback
For identity constraint.presence.present, context signup, and attribute path address.postcode, lookup removes
rightmost specificity one segment at a time:
signup.address.postcode.constraint.presence.present
signup.address.constraint.presence.present
signup.constraint.presence.present
constraint.presence.present
The message identity itself is never truncated. books.isbn.invalid never degrades to books.isbn or books:
those are segments of one name, not a namespace to search.
That is what makes a per-field override cheap. Translate constraint.presence.present once, then override only the
handful of fields whose wording genuinely differs.
Attribute nouns
Attribute nouns use their own prefix and their own chain. For context signup and attribute path
address.postcode:
attribute.signup.address.postcode
attribute.address.postcode
attribute.postcode
If no resource resolves, Reified humanizes the final raw attribute segment. Humanization splits camelCase,
snake_case, and kebab-case, preserves acronym runs, keeps _id, and applies invariant sentence casing:
postcode -> Postcode
firstName -> First name
postcodeID -> Postcode ID
billing_address -> Billing address
Humanization applies only to a raw segment Reified invented a noun for. A value that came out of a resource file is returned exactly as authored — no recasing, trimming, or Unicode normalization, ever.
Segments are opaque
Context names, attribute names, and Schema path keys are arbitrary text. Reified rejects an explicitly empty segment and encodes the reserved characters before joining:
% -> %25
. -> %2E
[ -> %5B
] -> %5D
% is encoded first, so a field literally named %2E cannot collide with a field named .. Consequently these
two renderers address different resources:
renderer |> Renderer.context "address.postcode"
renderer |> Renderer.context "address" |> Renderer.context "postcode"Emitted hex digits are uppercase. Lookup never decodes and re-encodes your resource keys.