formars_core/schema.rs
1//! The two validation views and their shared introspection types.
2
3use std::borrow::Cow;
4use std::fmt;
5
6use crate::error::{FieldPath, FormaError, FormaIssue, IssueCode, IssueParams};
7use crate::value::Value;
8
9#[diagnostic::on_unimplemented(
10 message = "`{Self}` is not a formars `Schema`",
11 label = "expected a schema built from the `string()`, `number()`, `bool()` or `coerced()` builders"
12)]
13/// The typed validation view.
14///
15/// `Err` always carries at least one issue. v0 primitives set
16/// `Input == Output`; [`crate::coerce::CoercedSchema`] is the first
17/// transform-shaped citizen (`Input = String`, `Output = T`), and
18/// [`crate::types::object::ObjectSchema`] makes the ordered [`crate::value::Object`]
19/// the struct-shaped currency (`Input = Object`, `Output = Object`).
20pub trait Schema {
21 /// What the schema consumes.
22 type Input;
23 /// What the schema produces on success.
24 type Output;
25
26 /// Validates and parses `input`; all violated constraints accumulate into one error.
27 fn parse(&self, input: &Self::Input) -> Result<Self::Output, FormaError>;
28}
29
30/// The object-safe erased view over a [`Value`] tree (DV-2).
31///
32/// Both views execute from the same internal representation, so results always
33/// agree with the typed path (SC-9). Usable as `Box<dyn DynSchema>`.
34///
35/// The `Send + Sync` supertraits make erased schemas thread-safe and safe to
36/// hold in reactive/memo contexts (`Arc<Box<dyn DynSchema>>` captured by
37/// shared closures), so introspection can be cached across threads.
38pub trait DynSchema: Send + Sync {
39 /// Validates an erased value; issues mirror what the typed parse would report.
40 fn validate_value(&self, v: &Value) -> Vec<FormaIssue>;
41 /// Introspection node describing this schema's kind and constraints,
42 /// memoized in a per-instance cache on first call.
43 fn shape(&self) -> &ShapeNode;
44 /// UI-facing metadata slots attached to this schema.
45 fn metadata(&self) -> &FieldMeta;
46}
47
48/// Introspection projection of a schema: primitive kind plus declared constraints.
49#[derive(Clone, Debug, PartialEq)]
50pub struct ShapeNode {
51 /// Which primitive family this schema validates.
52 pub kind: ShapeKind,
53 /// Declared constraints in declaration order.
54 pub constraints: Vec<ConstraintDesc>,
55}
56
57/// Primitive families distinguishable through erasure.
58#[derive(Clone, Debug, PartialEq)]
59pub enum ShapeKind {
60 /// String schema.
61 Str,
62 /// Number schema; `integer` marks i64-family schemas.
63 Number {
64 /// True when the schema is over i64.
65 integer: bool,
66 },
67 /// Boolean schema.
68 Bool,
69 /// Coercing string-to-type schema.
70 Coerced,
71 /// Object schema; declared fields in declaration order.
72 Object {
73 /// Declared fields, derived from the same registry the kernel walks.
74 fields: Vec<ObjectFieldDesc>,
75 },
76}
77
78/// One declared object field as introspection data — key plus the child's
79/// own shape projection, taken from the same `Vec` the kernel walks so
80/// introspection cannot drift (DV-6).
81#[derive(Clone, Debug, PartialEq)]
82pub struct ObjectFieldDesc {
83 /// Declared key (same `Box<str>` as the kernel's field registry).
84 pub key: Box<str>,
85 /// Child projection, produced by the child's own `shape()`.
86 pub child: ShapeNode,
87}
88
89/// Sealing module: keeps the [`ObjectChild`] extension point crate-internal.
90pub mod sealed {
91 /// Prevents downstream implementations of [`super::ObjectChild`].
92 pub trait Sealed {}
93}
94
95/// Contract for anything that can back an [`crate::types::object::ObjectSchema`]
96/// field: path-aware validation with a fail-fast override, plus an
97/// introspection projection (D3).
98///
99/// Public only so the `ObjectSchema::field` signature is nameable; sealed,
100/// so only the builtin families implement it.
101pub trait ObjectChild: fmt::Debug + Send + Sync + sealed::Sealed {
102 /// Validates `v` addressed at `path` (already joined by the caller);
103 /// on full success returns the validated output converted to `Value`.
104 fn validate_at(
105 &self,
106 v: &Value,
107 path: &FieldPath,
108 fail_fast: bool,
109 ) -> Result<Value, Vec<FormaIssue>>;
110 /// Introspection projection (delegates to the child's own `shape()`).
111 fn shape_node(&self) -> ShapeNode;
112 /// UI-facing metadata slots of the child (serves `ObjectSchema::field_meta`).
113 fn meta(&self) -> &FieldMeta;
114 /// Crate-internal deep-clone seam powering `ObjectSchema: Clone`
115 /// (`Field.child` is `Box<dyn ObjectChild>`, which cannot be duplicated
116 /// without dispatch). Sealed: exactly six impls, all below.
117 fn clone_boxed(&self) -> Box<dyn ObjectChild>;
118}
119
120/// Backs an [`crate::types::object::ObjectSchema`] field slot with any schema
121/// whose composed representation is an [`crate::types::object::ObjectSchema`]
122/// (e.g. a derive companion from the separate `formars-derive` crate).
123///
124/// Pure delegation (NE-2): joined paths, inherited fail-fast and introspection
125/// come from the wrapped schema's own [`crate::types::object::ObjectSchema::validate_at`] kernel —
126/// never reimplemented. The only contract is [`AsRef<ObjectSchema>`], so the
127/// adapter stays narrow while [`ObjectChild`] remains sealed.
128#[derive(Debug)]
129pub struct Nested<S> {
130 inner: S,
131}
132
133impl<S> Nested<S> {
134 /// Wraps a schema whose composed representation is an [`crate::types::object::ObjectSchema`].
135 #[must_use]
136 pub fn new(inner: S) -> Self {
137 Self { inner }
138 }
139}
140
141impl<S> ObjectChild for Nested<S>
142where
143 S: fmt::Debug + Send + Sync + AsRef<crate::types::object::ObjectSchema> + Clone + 'static,
144{
145 fn validate_at(
146 &self,
147 v: &Value,
148 path: &FieldPath,
149 fail_fast: bool,
150 ) -> Result<Value, Vec<FormaIssue>> {
151 self.inner.as_ref().validate_at(v, path, fail_fast)
152 }
153
154 fn shape_node(&self) -> ShapeNode {
155 self.inner.as_ref().shape_node()
156 }
157
158 fn meta(&self) -> &FieldMeta {
159 self.inner.as_ref().meta()
160 }
161
162 fn clone_boxed(&self) -> Box<dyn ObjectChild> {
163 Box::new(Self {
164 inner: self.inner.clone(),
165 })
166 }
167}
168
169// Explicit seal registration: `ObjectChild` carries the sealed supertrait, and
170// this is the single new in-crate implementor alongside the builtin families.
171impl<S> sealed::Sealed for Nested<S> {}
172
173/// One declared constraint as data — derived from the same check vector the
174/// kernels run, so introspection cannot drift from behavior.
175#[derive(Clone, Debug, PartialEq)]
176pub struct ConstraintDesc {
177 /// Stable constraint identifier.
178 pub code: IssueCode,
179 /// Constraint parameters (e.g. the declared minimum).
180 pub params: IssueParams,
181}
182
183/// UI-facing metadata slots (DV-3); unknown extras survive round-trips.
184#[derive(Clone, Debug, Default, PartialEq)]
185pub struct FieldMeta {
186 /// Short display label.
187 pub label: Option<Cow<'static, str>>,
188 /// Longer description.
189 pub description: Option<Cow<'static, str>>,
190 /// Input placeholder text.
191 pub placeholder: Option<Cow<'static, str>>,
192 /// Extensible key-value extras for UI layers.
193 pub extra: Vec<(Cow<'static, str>, Value)>,
194}
195
196/// Builds an issue addressed to ROOT with the given code, message and params.
197pub(crate) fn issue_at_root(
198 code: IssueCode,
199 message: Cow<'static, str>,
200 params: IssueParams,
201) -> FormaIssue {
202 FormaIssue {
203 path: crate::error::FieldPath::ROOT,
204 code,
205 message,
206 params,
207 }
208}