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}