Skip to main content

formars_core/
form.rs

1//! Struct↔schema contracts and value bridging used by the opt-in
2//! `formars-derive` crate — macro-free, dependency-free additions to core.
3//!
4//! Two small traits with disjoint duties:
5//! - [`FormSchema`] connects a Rust struct to its companion schema (nesting).
6//!   It lives in core because proc-macro crates cannot export runtime items.
7//! - [`FormBridge`] connects a Rust value to the erased [`Value`] tree in both
8//!   directions (typed-parse bridging), so plain primitives can participate in
9//!   derived reconstruction without deriving anything.
10//!
11//! [`FormSchema`]: crate::form::FormSchema
12//! [`FormBridge`]: crate::form::FormBridge
13//! [`Value`]: crate::value::Value
14
15use crate::schema::{DynSchema, Schema};
16use crate::value::Value;
17
18/// Connects a Rust struct to its companion schema.
19///
20/// Hand-written impls are supported for types that predate the derive; nested
21/// form fields additionally require [`FormBridge`] on the field's type (the
22/// generated typed parse bridges through it).
23#[diagnostic::on_unimplemented(
24    message = "`{Self}` is not a formars `FormSchema`",
25    label = "nested form fields require `#[derive(FormSchema)]` (crate `formars-derive`) or a manual `FormSchema` impl",
26    note = "add `#[derive(FormSchema)]` on `{Self}`, or use `#[form(schema = <expr>)]` on the field"
27)]
28pub trait FormSchema {
29    /// The companion schema type generated by the derive.
30    type Schema: Schema<Input = Self, Output = Self> + DynSchema;
31    /// Builds the companion schema.
32    #[must_use]
33    fn form_schema() -> Self::Schema;
34}
35
36/// Lossless Rust-value ↔ [`Value`] bridging used by generated typed parses:
37/// [`to_form_value`](FormBridge::to_form_value) feeds the composed object
38/// schema; [`from_validated`](FormBridge::from_validated) reconstructs fields
39/// from validated output. Total on validated outputs — returns `None` only on
40/// an invariant breach or wrong-variant input.
41///
42/// # f32 narrowing precondition
43///
44/// For `f32`, narrowing in [`from_validated`](Self::from_validated) is sound
45/// ONLY when the `F64` input originated from
46/// [`to_form_value`](Self::to_form_value) of an `f32` (the validated-output
47/// precondition): the widening was lossless, so the inverse cast recovers the
48/// exact original bits. Arbitrary `F64` inputs NOT produced that way may be
49/// truncated or rounded by the narrowing cast.
50///
51/// Implemented exactly for the v0 supported-type matrix (`String`, `bool`,
52/// `i8..i64`, `u8..u32`, `f32`, `f64`). `u64`/`usize` are deliberately
53/// excluded, mirroring the [`crate::value::ToValue`] fidelity rules (DV-1):
54///
55/// ```compile_fail,E0277
56/// use formars_core::prelude::*;
57///
58/// fn require_bridge<T: formars_core::FormBridge>(_: &T) {}
59/// require_bridge(&7u64); // u64/usize deliberately excluded
60/// ```
61pub trait FormBridge: Sized {
62    /// Converts the value into its exact [`Value`] representation.
63    #[must_use]
64    fn to_form_value(&self) -> Value;
65    /// Reconstructs the value from a validated output; `None` when `v` does
66    /// not carry the exact variant/range this type round-trips as.
67    #[must_use]
68    fn from_validated(v: &Value) -> Option<Self>;
69}
70
71macro_rules! bridge_int {
72    ($($t:ty),* $(,)?) => {$(
73        impl FormBridge for $t {
74            fn to_form_value(&self) -> Value {
75                crate::value::ToValue::to_value(*self)
76            }
77
78            fn from_validated(v: &Value) -> Option<Self> {
79                <$t>::try_from(v.as_i64()?).ok()
80            }
81        }
82    )*};
83}
84
85bridge_int!(i8, i16, i32, i64, u8, u16, u32);
86
87impl FormBridge for f32 {
88    /// # Narrowing precondition
89    ///
90    /// `from_validated` narrows `F64` back to `f32`; this is sound ONLY when
91    /// the `F64` originated from [`FormBridge::to_form_value`] of an `f32`
92    /// (validated output — the widening was lossless, so the exact original
93    /// bits are recovered). Arbitrary `F64` inputs may truncate or round.
94    fn to_form_value(&self) -> Value {
95        Value::F64(f64::from(*self))
96    }
97
98    fn from_validated(v: &Value) -> Option<Self> {
99        // Exact recovery despite the cast lint: `to_form_value` widened the
100        // f32 losslessly into F64, so narrowing recovers the original bits.
101        // Precondition documented on this impl: sound only for validated
102        // outputs of `to_form_value`; arbitrary F64 inputs may truncate.
103        #[allow( // exact inverse of a lossless widening
104            clippy::cast_possible_truncation,
105            reason = "to_form_value widened the f32 losslessly into F64; narrowing recovers the original bits"
106        )]
107        Some(v.as_f64()? as Self)
108    }
109}
110
111impl FormBridge for f64 {
112    fn to_form_value(&self) -> Value {
113        Value::F64(*self)
114    }
115
116    fn from_validated(v: &Value) -> Option<Self> {
117        v.as_f64()
118    }
119}
120
121impl FormBridge for String {
122    fn to_form_value(&self) -> Value {
123        Value::from(self.as_str())
124    }
125
126    fn from_validated(v: &Value) -> Option<Self> {
127        v.as_str().map(str::to_owned)
128    }
129}
130
131impl FormBridge for bool {
132    fn to_form_value(&self) -> Value {
133        Value::Bool(*self)
134    }
135
136    fn from_validated(v: &Value) -> Option<Self> {
137        v.as_bool()
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::FormBridge;
144    use crate::value::Value;
145
146    macro_rules! rt {
147        ($t:ty, $v:expr) => {{
148            let original = $v;
149            let wire = <$t as FormBridge>::to_form_value(&original);
150            assert_eq!(
151                <$t as FormBridge>::from_validated(&wire),
152                Some(original),
153                "{} round-trip failed",
154                stringify!($t)
155            );
156        }};
157    }
158
159    #[test]
160    fn a3_round_trips_full_primitive_matrix() {
161        rt!(String, String::from("ada"));
162        rt!(bool, true);
163        rt!(bool, false);
164        rt!(i8, -1i8);
165        rt!(i16, -2i16);
166        rt!(i32, -3i32);
167        rt!(i64, -4i64);
168        rt!(u8, 1u8);
169        rt!(u16, 2u16);
170        rt!(u32, 3u32);
171        rt!(f32, 0.5f32);
172        rt!(f64, 1.5f64);
173    }
174
175    #[test]
176    fn a3_to_form_value_variants_mirror_tovalue_matrix() {
177        assert_eq!(
178            <i32 as FormBridge>::to_form_value(&-3),
179            Value::I64(-3),
180            "integers bridge as I64"
181        );
182        assert_eq!(
183            <u8 as FormBridge>::to_form_value(&1),
184            Value::I64(1),
185            "unsigned bridges as I64"
186        );
187        assert_eq!(
188            <f32 as FormBridge>::to_form_value(&0.5),
189            Value::F64(0.5),
190            "floats bridge as F64"
191        );
192        assert_eq!(
193            <String as FormBridge>::to_form_value(&"hi".to_string()),
194            Value::String("hi".into())
195        );
196        assert_eq!(
197            <bool as FormBridge>::to_form_value(&true),
198            Value::Bool(true)
199        );
200    }
201
202    #[test]
203    fn a3_from_validated_rejects_wrong_variants() {
204        assert_eq!(<String as FormBridge>::from_validated(&Value::I64(1)), None);
205        assert_eq!(
206            <bool as FormBridge>::from_validated(&Value::String("x".into())),
207            None
208        );
209        assert_eq!(<i32 as FormBridge>::from_validated(&Value::F64(1.0)), None);
210        assert_eq!(<u32 as FormBridge>::from_validated(&Value::Null), None);
211        assert_eq!(
212            <f64 as FormBridge>::from_validated(&Value::Bool(true)),
213            None
214        );
215    }
216
217    #[test]
218    fn a3_from_validated_is_range_strict_for_narrow_integers() {
219        assert_eq!(<i8 as FormBridge>::from_validated(&Value::I64(300)), None);
220        assert_eq!(<u8 as FormBridge>::from_validated(&Value::I64(-1)), None);
221        assert_eq!(
222            <i64 as FormBridge>::from_validated(&Value::I64(i64::MIN)),
223            Some(i64::MIN)
224        );
225    }
226}