serde_field_result 0.1.0

Field-level Serde recovery for schema-drift-tolerant API clients
Documentation
use alloc::string::String;

use serde::{Deserialize, Deserializer};

use super::{decode::FieldDecode, error::FieldError};

/// Field-level result of deserializing an upstream value.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub enum Field<T> {
    /// The field was absent, or the decoder chose to treat the value as missing.
    #[default]
    Missing,
    /// The field was present and decoded successfully.
    Valid(T),
    /// The field was present but did not match the expected shape.
    Invalid(FieldError),
}

impl<T> Field<T> {
    /// Creates an invalid field from an error.
    #[must_use]
    pub fn invalid(error: impl Into<FieldError>) -> Self {
        Self::Invalid(error.into())
    }

    impl_common_field_methods! {
        map_return: Field<U>;
        map_missing: Field::Missing;
        map_valid: Field::Valid;
        invalid_ignore: Self::Invalid(_),
        invalid_error: Self::Invalid(error) => error;
        invalid_map: Self::Invalid(error) => Field::Invalid(error);
    }

    /// Converts a result into a field.
    ///
    /// Successful values become [`Field::Valid`], and errors become
    /// [`Field::Invalid`]. This is useful in custom [`FieldDecode`] visitors
    /// that parse a scalar into a recoverable field.
    #[must_use]
    pub fn from_result(result: Result<T, impl Into<FieldError>>) -> Self {
        match result {
            Ok(value) => Self::Valid(value),
            Err(error) => Self::Invalid(error.into()),
        }
    }
}

impl Field<String> {
    /// Returns the valid string as `&str`, if present.
    #[must_use]
    pub fn as_str(&self) -> Option<&str> {
        self.as_ref().map(String::as_str)
    }
}

impl<T> From<T> for Field<T> {
    fn from(value: T) -> Self {
        Self::Valid(value)
    }
}

impl<'de, T> Deserialize<'de> for Field<T>
where
    T: FieldDecode<'de>,
{
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        T::decode_field(deserializer)
    }
}

#[cfg(test)]
mod tests {
    use alloc::{borrow::ToOwned, string::ToString};

    use super::*;

    #[test]
    fn missing_is_the_default() {
        let field = Field::<u64>::default();

        assert!(field.is_missing());
        assert_eq!(field.as_ref(), None);
        assert_eq!(field.into_option(), None);
    }

    #[test]
    fn invalid_field_exposes_error() {
        let field = Field::<u64>::invalid("bad wire value");

        assert!(field.is_invalid());
        assert_eq!(field.error().unwrap().message(), "bad wire value");
    }

    #[test]
    fn from_result_converts_ok_and_err() {
        let valid = Field::from_result(Ok::<_, FieldError>(42));
        let invalid = Field::<u64>::from_result(Err("bad wire value"));

        assert_eq!(valid.as_ref(), Some(&42));
        assert!(invalid.is_invalid());
        assert_eq!(invalid.error().unwrap().message(), "bad wire value");
    }

    #[test]
    fn invalid_field_equality_compares_displayed_error_messages() {
        assert_eq!(
            Field::<u64>::invalid("bad"),
            Field::<u64>::invalid(String::from("bad")),
        );
    }

    #[test]
    fn valid_field_exposes_mutable_value() {
        let mut field = Field::Valid(41);

        assert!(field.is_valid());
        *field.as_mut().unwrap() += 1;
        assert_eq!(field.as_ref(), Some(&42));
    }

    #[test]
    fn map_preserves_non_valid_states() {
        let missing = Field::<u64>::Missing.map(|value| value.to_string());
        let invalid = Field::<u64>::invalid("bad").map(|value| value.to_string());

        assert!(missing.is_missing());
        assert!(invalid.is_invalid());
    }

    #[test]
    fn result_accessors_preserve_missing_valid_and_invalid() {
        let missing = Field::<u64>::Missing;
        let valid = Field::Valid(42);
        let invalid = Field::<u64>::invalid("bad wire value");

        assert_eq!(missing.as_result(), Ok(None));
        assert_eq!(valid.as_result(), Ok(Some(&42)));
        assert_eq!(
            invalid.as_result().err().unwrap().message(),
            "bad wire value"
        );

        assert_eq!(Field::<u64>::Missing.into_result(), Ok(None));
        assert_eq!(Field::Valid(42).into_result(), Ok(Some(42)));
        assert_eq!(
            Field::<u64>::invalid("bad wire value")
                .into_result()
                .err()
                .map(FieldError::into_message),
            Some("bad wire value".to_owned())
        );
    }
}