serde_field_result 0.1.0

Field-level Serde recovery for schema-drift-tolerant API clients
Documentation
#![cfg_attr(not(feature = "std"), no_std)]
#![forbid(unsafe_code)]
#![warn(missing_docs)]

//! Field-level Serde recovery for schema-drift-tolerant API clients.
//!
//! `Field<T>` is for values where this crate knows how to consume both valid
//! and invalid representations without aborting the parent struct. A malformed
//! field becomes [`Field::Invalid`], while the rest of the response can keep
//! deserializing.
//!
//! ```
//! use serde::Deserialize;
//! use serde_field_result::Field;
//!
//! #[derive(Deserialize)]
//! struct Quote {
//!     #[serde(default)] // required if absent fields should become Field::Missing
//!     price: Field<f64>,
//! }
//!
//! let quote: Quote = serde_json::from_str(r#"{"price":{"raw":"bad"}}"#).unwrap();
//! assert!(quote.price.is_invalid());
//! ```
//!
//! Serde resolves struct field presence before [`Field`]'s deserializer runs,
//! so `#[serde(default)]` is required for absent fields to become
//! [`Field::Missing`]. Without it, the parent struct fails first:
//!
//! ```
//! use serde::Deserialize;
//! use serde_field_result::Field;
//!
//! #[derive(Deserialize)]
//! struct Quote {
//!     price: Field<f64>,
//! }
//!
//! let error = match serde_json::from_str::<Quote>("{}") {
//!     Ok(_) => panic!("missing field unexpectedly decoded"),
//!     Err(error) => error,
//! };
//! assert_eq!(error.to_string(), "missing field `price` at line 1 column 2");
//! ```
//!
//! The core type is not a generic buffering layer over arbitrary
//! `T: Deserialize`. Serde does not expose that abstraction in a
//! format-agnostic way. Implement [`ScalarFieldDecode`] for custom scalar-like
//! types, implement [`FieldDecode`] for custom non-scalar recovery, or enable
//! the `json` feature and use `JsonField` or `BorrowedJsonField` for
//! arbitrary JSON values with raw-value capture.
//!
//! For ordinary `Field<T>` values, `null` and Serde unit decode as
//! [`Field::Missing`]. Use `Field<Option<T>>` when explicit `null` or unit
//! should decode as `Field::Valid(None)` while absent fields still require
//! `#[serde(default)]` to become [`Field::Missing`].
//!
//! The built-in scalar decoders use `Deserializer::deserialize_any`, so they are
//! meant for self-describing formats such as JSON.

#[cfg(not(feature = "alloc"))]
compile_error!("serde_field_result requires either the `std` feature or the `alloc` feature");

extern crate alloc;

macro_rules! impl_common_field_methods {
    (
        map_return: $map_return:ty;
        map_missing: $map_missing:path;
        map_valid: $map_valid:path;
        invalid_ignore: $invalid_ignore:pat,
        invalid_error: $invalid_error:pat => $error:expr;
        invalid_map: $invalid_map:pat => $mapped_invalid:expr;
        $(into_result_extra: $into_result_extra:literal;)?
    ) => {
        /// Returns the valid value, if present.
        #[must_use]
        pub const fn as_ref(&self) -> Option<&T> {
            match self {
                Self::Valid(value) => Some(value),
                Self::Missing => None,
                $invalid_ignore => None,
            }
        }

        /// Returns the valid value mutably, if present.
        #[must_use]
        pub fn as_mut(&mut self) -> Option<&mut T> {
            match self {
                Self::Valid(value) => Some(value),
                Self::Missing => None,
                $invalid_ignore => None,
            }
        }

        /// Converts a valid field into `Some(T)` and all other states into `None`.
        #[must_use]
        pub fn into_option(self) -> Option<T> {
            match self {
                Self::Valid(value) => Some(value),
                Self::Missing => None,
                $invalid_ignore => None,
            }
        }

        /// Converts the field into `Result<Option<T>, FieldError>`.
        ///
        /// Missing fields become `Ok(None)`, valid fields become `Ok(Some(T))`,
        /// and invalid fields become `Err(FieldError)`.
        $(#[doc = $into_result_extra])?
        #[must_use = "inspect the result to distinguish missing, valid, and invalid fields"]
        pub fn into_result(self) -> Result<Option<T>, FieldError> {
            match self {
                Self::Missing => Ok(None),
                Self::Valid(value) => Ok(Some(value)),
                $invalid_error => Err($error),
            }
        }

        /// Borrows the field as `Result<Option<&T>, &FieldError>`.
        ///
        /// Missing fields become `Ok(None)`, valid fields become `Ok(Some(&T))`,
        /// and invalid fields become `Err(&FieldError)`.
        #[must_use = "inspect the result to distinguish missing, valid, and invalid fields"]
        pub const fn as_result(&self) -> Result<Option<&T>, &FieldError> {
            match self {
                Self::Missing => Ok(None),
                Self::Valid(value) => Ok(Some(value)),
                $invalid_error => Err($error),
            }
        }

        /// Returns the invalid-field error, if present.
        #[must_use]
        pub const fn error(&self) -> Option<&FieldError> {
            match self {
                $invalid_error => Some($error),
                Self::Missing | Self::Valid(_) => None,
            }
        }

        /// Returns `true` when the field decoded successfully.
        #[must_use]
        pub const fn is_valid(&self) -> bool {
            matches!(self, Self::Valid(_))
        }

        /// Returns `true` when the field was missing or `null`.
        #[must_use]
        pub const fn is_missing(&self) -> bool {
            matches!(self, Self::Missing)
        }

        /// Returns `true` when the field was present but invalid.
        #[must_use]
        pub const fn is_invalid(&self) -> bool {
            matches!(self, $invalid_ignore)
        }

        /// Maps a valid value while preserving missing and invalid states.
        #[must_use]
        pub fn map<U>(self, map: impl FnOnce(T) -> U) -> $map_return {
            match self {
                Self::Missing => $map_missing,
                Self::Valid(value) => $map_valid(map(value)),
                $invalid_map => $mapped_invalid,
            }
        }
    };
}

mod field;

pub use field::{
    Field, FieldDecode, FieldError, ScalarFieldDecode, drain_map, drain_seq, invalid_map,
    invalid_seq,
};

#[cfg(feature = "json")]
mod json;

#[cfg(feature = "json")]
pub use json::{BorrowedJsonField, JsonField};