formars-core 0.1.0

Typed schema primitives with a dyn-safe erased view for form validation.
Documentation
//! Struct↔schema contracts and value bridging used by the opt-in
//! `formars-derive` crate — macro-free, dependency-free additions to core.
//!
//! Two small traits with disjoint duties:
//! - [`FormSchema`] connects a Rust struct to its companion schema (nesting).
//!   It lives in core because proc-macro crates cannot export runtime items.
//! - [`FormBridge`] connects a Rust value to the erased [`Value`] tree in both
//!   directions (typed-parse bridging), so plain primitives can participate in
//!   derived reconstruction without deriving anything.
//!
//! [`FormSchema`]: crate::form::FormSchema
//! [`FormBridge`]: crate::form::FormBridge
//! [`Value`]: crate::value::Value

use crate::schema::{DynSchema, Schema};
use crate::value::Value;

/// Connects a Rust struct to its companion schema.
///
/// Hand-written impls are supported for types that predate the derive; nested
/// form fields additionally require [`FormBridge`] on the field's type (the
/// generated typed parse bridges through it).
#[diagnostic::on_unimplemented(
    message = "`{Self}` is not a formars `FormSchema`",
    label = "nested form fields require `#[derive(FormSchema)]` (crate `formars-derive`) or a manual `FormSchema` impl",
    note = "add `#[derive(FormSchema)]` on `{Self}`, or use `#[form(schema = <expr>)]` on the field"
)]
pub trait FormSchema {
    /// The companion schema type generated by the derive.
    type Schema: Schema<Input = Self, Output = Self> + DynSchema;
    /// Builds the companion schema.
    #[must_use]
    fn form_schema() -> Self::Schema;
}

/// Lossless Rust-value ↔ [`Value`] bridging used by generated typed parses:
/// [`to_form_value`](FormBridge::to_form_value) feeds the composed object
/// schema; [`from_validated`](FormBridge::from_validated) reconstructs fields
/// from validated output. Total on validated outputs — returns `None` only on
/// an invariant breach or wrong-variant input.
///
/// # f32 narrowing precondition
///
/// For `f32`, narrowing in [`from_validated`](Self::from_validated) is sound
/// ONLY when the `F64` input originated from
/// [`to_form_value`](Self::to_form_value) of an `f32` (the validated-output
/// precondition): the widening was lossless, so the inverse cast recovers the
/// exact original bits. Arbitrary `F64` inputs NOT produced that way may be
/// truncated or rounded by the narrowing cast.
///
/// Implemented exactly for the v0 supported-type matrix (`String`, `bool`,
/// `i8..i64`, `u8..u32`, `f32`, `f64`). `u64`/`usize` are deliberately
/// excluded, mirroring the [`crate::value::ToValue`] fidelity rules (DV-1):
///
/// ```compile_fail,E0277
/// use formars_core::prelude::*;
///
/// fn require_bridge<T: formars_core::FormBridge>(_: &T) {}
/// require_bridge(&7u64); // u64/usize deliberately excluded
/// ```
pub trait FormBridge: Sized {
    /// Converts the value into its exact [`Value`] representation.
    #[must_use]
    fn to_form_value(&self) -> Value;
    /// Reconstructs the value from a validated output; `None` when `v` does
    /// not carry the exact variant/range this type round-trips as.
    #[must_use]
    fn from_validated(v: &Value) -> Option<Self>;
}

macro_rules! bridge_int {
    ($($t:ty),* $(,)?) => {$(
        impl FormBridge for $t {
            fn to_form_value(&self) -> Value {
                crate::value::ToValue::to_value(*self)
            }

            fn from_validated(v: &Value) -> Option<Self> {
                <$t>::try_from(v.as_i64()?).ok()
            }
        }
    )*};
}

bridge_int!(i8, i16, i32, i64, u8, u16, u32);

impl FormBridge for f32 {
    /// # Narrowing precondition
    ///
    /// `from_validated` narrows `F64` back to `f32`; this is sound ONLY when
    /// the `F64` originated from [`FormBridge::to_form_value`] of an `f32`
    /// (validated output — the widening was lossless, so the exact original
    /// bits are recovered). Arbitrary `F64` inputs may truncate or round.
    fn to_form_value(&self) -> Value {
        Value::F64(f64::from(*self))
    }

    fn from_validated(v: &Value) -> Option<Self> {
        // Exact recovery despite the cast lint: `to_form_value` widened the
        // f32 losslessly into F64, so narrowing recovers the original bits.
        // Precondition documented on this impl: sound only for validated
        // outputs of `to_form_value`; arbitrary F64 inputs may truncate.
        #[allow( // exact inverse of a lossless widening
            clippy::cast_possible_truncation,
            reason = "to_form_value widened the f32 losslessly into F64; narrowing recovers the original bits"
        )]
        Some(v.as_f64()? as Self)
    }
}

impl FormBridge for f64 {
    fn to_form_value(&self) -> Value {
        Value::F64(*self)
    }

    fn from_validated(v: &Value) -> Option<Self> {
        v.as_f64()
    }
}

impl FormBridge for String {
    fn to_form_value(&self) -> Value {
        Value::from(self.as_str())
    }

    fn from_validated(v: &Value) -> Option<Self> {
        v.as_str().map(str::to_owned)
    }
}

impl FormBridge for bool {
    fn to_form_value(&self) -> Value {
        Value::Bool(*self)
    }

    fn from_validated(v: &Value) -> Option<Self> {
        v.as_bool()
    }
}

#[cfg(test)]
mod tests {
    use super::FormBridge;
    use crate::value::Value;

    macro_rules! rt {
        ($t:ty, $v:expr) => {{
            let original = $v;
            let wire = <$t as FormBridge>::to_form_value(&original);
            assert_eq!(
                <$t as FormBridge>::from_validated(&wire),
                Some(original),
                "{} round-trip failed",
                stringify!($t)
            );
        }};
    }

    #[test]
    fn a3_round_trips_full_primitive_matrix() {
        rt!(String, String::from("ada"));
        rt!(bool, true);
        rt!(bool, false);
        rt!(i8, -1i8);
        rt!(i16, -2i16);
        rt!(i32, -3i32);
        rt!(i64, -4i64);
        rt!(u8, 1u8);
        rt!(u16, 2u16);
        rt!(u32, 3u32);
        rt!(f32, 0.5f32);
        rt!(f64, 1.5f64);
    }

    #[test]
    fn a3_to_form_value_variants_mirror_tovalue_matrix() {
        assert_eq!(
            <i32 as FormBridge>::to_form_value(&-3),
            Value::I64(-3),
            "integers bridge as I64"
        );
        assert_eq!(
            <u8 as FormBridge>::to_form_value(&1),
            Value::I64(1),
            "unsigned bridges as I64"
        );
        assert_eq!(
            <f32 as FormBridge>::to_form_value(&0.5),
            Value::F64(0.5),
            "floats bridge as F64"
        );
        assert_eq!(
            <String as FormBridge>::to_form_value(&"hi".to_string()),
            Value::String("hi".into())
        );
        assert_eq!(
            <bool as FormBridge>::to_form_value(&true),
            Value::Bool(true)
        );
    }

    #[test]
    fn a3_from_validated_rejects_wrong_variants() {
        assert_eq!(<String as FormBridge>::from_validated(&Value::I64(1)), None);
        assert_eq!(
            <bool as FormBridge>::from_validated(&Value::String("x".into())),
            None
        );
        assert_eq!(<i32 as FormBridge>::from_validated(&Value::F64(1.0)), None);
        assert_eq!(<u32 as FormBridge>::from_validated(&Value::Null), None);
        assert_eq!(
            <f64 as FormBridge>::from_validated(&Value::Bool(true)),
            None
        );
    }

    #[test]
    fn a3_from_validated_is_range_strict_for_narrow_integers() {
        assert_eq!(<i8 as FormBridge>::from_validated(&Value::I64(300)), None);
        assert_eq!(<u8 as FormBridge>::from_validated(&Value::I64(-1)), None);
        assert_eq!(
            <i64 as FormBridge>::from_validated(&Value::I64(i64::MIN)),
            Some(i64::MIN)
        );
    }
}