Skip to main content

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}