frame-conv 0.2.0

Conversation patterns — request-response, subscription, pub/sub, and workflow over liminal
Documentation
//! Publish-side finite-float wall.
//!
//! `serde_json` maps non-finite floats (`NaN`, `±inf`) to `null` SILENTLY —
//! observed at the bytes by this crate's vector suite — which would corrupt
//! a typed payload before the wire with no refusal anywhere (an
//! `Option`-typed receiver would observe `None`, end-to-end silent). JSON —
//! and the canonical form the design commits browser-crossing payloads to
//! (RFC 8785) — has no non-finite representation, so the honest surface is
//! a typed refusal at publish (F-3a R1: schema-invalid is refused BEFORE
//! the wire). This module is a minimal `serde` serializer that accepts
//! everything except non-finite floats.

use std::fmt;

use serde::Serialize;
use serde::ser;

/// A typed refusal from the finite-float probe.
#[derive(Debug)]
pub(crate) struct NonFinite {
    detail: String,
}

impl NonFinite {
    fn new(value: f64) -> Self {
        Self {
            detail: format!("non-finite float {value} has no wire representation"),
        }
    }

    pub(crate) fn detail(&self) -> &str {
        &self.detail
    }
}

impl fmt::Display for NonFinite {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(&self.detail)
    }
}

impl std::error::Error for NonFinite {}

impl ser::Error for NonFinite {
    fn custom<T: fmt::Display>(message: T) -> Self {
        Self {
            detail: message.to_string(),
        }
    }
}

/// Refuses typed when `body` contains any non-finite float anywhere.
pub(crate) fn check<T: Serialize + ?Sized>(body: &T) -> Result<(), NonFinite> {
    body.serialize(Probe)
}

struct Probe;

struct Compound;

impl ser::Serializer for Probe {
    type Ok = ();
    type Error = NonFinite;
    type SerializeSeq = Compound;
    type SerializeTuple = Compound;
    type SerializeTupleStruct = Compound;
    type SerializeTupleVariant = Compound;
    type SerializeMap = Compound;
    type SerializeStruct = Compound;
    type SerializeStructVariant = Compound;

    fn serialize_bool(self, _: bool) -> Result<(), NonFinite> {
        Ok(())
    }
    fn serialize_i8(self, _: i8) -> Result<(), NonFinite> {
        Ok(())
    }
    fn serialize_i16(self, _: i16) -> Result<(), NonFinite> {
        Ok(())
    }
    fn serialize_i32(self, _: i32) -> Result<(), NonFinite> {
        Ok(())
    }
    fn serialize_i64(self, _: i64) -> Result<(), NonFinite> {
        Ok(())
    }
    fn serialize_i128(self, _: i128) -> Result<(), NonFinite> {
        Ok(())
    }
    fn serialize_u8(self, _: u8) -> Result<(), NonFinite> {
        Ok(())
    }
    fn serialize_u16(self, _: u16) -> Result<(), NonFinite> {
        Ok(())
    }
    fn serialize_u32(self, _: u32) -> Result<(), NonFinite> {
        Ok(())
    }
    fn serialize_u64(self, _: u64) -> Result<(), NonFinite> {
        Ok(())
    }
    fn serialize_u128(self, _: u128) -> Result<(), NonFinite> {
        Ok(())
    }
    fn serialize_f32(self, value: f32) -> Result<(), NonFinite> {
        if value.is_finite() {
            Ok(())
        } else {
            Err(NonFinite::new(f64::from(value)))
        }
    }
    fn serialize_f64(self, value: f64) -> Result<(), NonFinite> {
        if value.is_finite() {
            Ok(())
        } else {
            Err(NonFinite::new(value))
        }
    }
    fn serialize_char(self, _: char) -> Result<(), NonFinite> {
        Ok(())
    }
    fn serialize_str(self, _: &str) -> Result<(), NonFinite> {
        Ok(())
    }
    fn serialize_bytes(self, _: &[u8]) -> Result<(), NonFinite> {
        Ok(())
    }
    fn serialize_none(self) -> Result<(), NonFinite> {
        Ok(())
    }
    fn serialize_some<T: Serialize + ?Sized>(self, value: &T) -> Result<(), NonFinite> {
        value.serialize(Self)
    }
    fn serialize_unit(self) -> Result<(), NonFinite> {
        Ok(())
    }
    fn serialize_unit_struct(self, _: &'static str) -> Result<(), NonFinite> {
        Ok(())
    }
    fn serialize_unit_variant(
        self,
        _: &'static str,
        _: u32,
        _: &'static str,
    ) -> Result<(), NonFinite> {
        Ok(())
    }
    fn serialize_newtype_struct<T: Serialize + ?Sized>(
        self,
        _: &'static str,
        value: &T,
    ) -> Result<(), NonFinite> {
        value.serialize(Self)
    }
    fn serialize_newtype_variant<T: Serialize + ?Sized>(
        self,
        _: &'static str,
        _: u32,
        _: &'static str,
        value: &T,
    ) -> Result<(), NonFinite> {
        value.serialize(Self)
    }
    fn serialize_seq(self, _: Option<usize>) -> Result<Compound, NonFinite> {
        Ok(Compound)
    }
    fn serialize_tuple(self, _: usize) -> Result<Compound, NonFinite> {
        Ok(Compound)
    }
    fn serialize_tuple_struct(self, _: &'static str, _: usize) -> Result<Compound, NonFinite> {
        Ok(Compound)
    }
    fn serialize_tuple_variant(
        self,
        _: &'static str,
        _: u32,
        _: &'static str,
        _: usize,
    ) -> Result<Compound, NonFinite> {
        Ok(Compound)
    }
    fn serialize_map(self, _: Option<usize>) -> Result<Compound, NonFinite> {
        Ok(Compound)
    }
    fn serialize_struct(self, _: &'static str, _: usize) -> Result<Compound, NonFinite> {
        Ok(Compound)
    }
    fn serialize_struct_variant(
        self,
        _: &'static str,
        _: u32,
        _: &'static str,
        _: usize,
    ) -> Result<Compound, NonFinite> {
        Ok(Compound)
    }
    fn collect_str<T: fmt::Display + ?Sized>(self, _: &T) -> Result<(), NonFinite> {
        Ok(())
    }
}

impl ser::SerializeSeq for Compound {
    type Ok = ();
    type Error = NonFinite;
    fn serialize_element<T: Serialize + ?Sized>(&mut self, value: &T) -> Result<(), NonFinite> {
        value.serialize(Probe)
    }
    fn end(self) -> Result<(), NonFinite> {
        Ok(())
    }
}

impl ser::SerializeTuple for Compound {
    type Ok = ();
    type Error = NonFinite;
    fn serialize_element<T: Serialize + ?Sized>(&mut self, value: &T) -> Result<(), NonFinite> {
        value.serialize(Probe)
    }
    fn end(self) -> Result<(), NonFinite> {
        Ok(())
    }
}

impl ser::SerializeTupleStruct for Compound {
    type Ok = ();
    type Error = NonFinite;
    fn serialize_field<T: Serialize + ?Sized>(&mut self, value: &T) -> Result<(), NonFinite> {
        value.serialize(Probe)
    }
    fn end(self) -> Result<(), NonFinite> {
        Ok(())
    }
}

impl ser::SerializeTupleVariant for Compound {
    type Ok = ();
    type Error = NonFinite;
    fn serialize_field<T: Serialize + ?Sized>(&mut self, value: &T) -> Result<(), NonFinite> {
        value.serialize(Probe)
    }
    fn end(self) -> Result<(), NonFinite> {
        Ok(())
    }
}

impl ser::SerializeMap for Compound {
    type Ok = ();
    type Error = NonFinite;
    fn serialize_key<T: Serialize + ?Sized>(&mut self, key: &T) -> Result<(), NonFinite> {
        key.serialize(Probe)
    }
    fn serialize_value<T: Serialize + ?Sized>(&mut self, value: &T) -> Result<(), NonFinite> {
        value.serialize(Probe)
    }
    fn end(self) -> Result<(), NonFinite> {
        Ok(())
    }
}

impl ser::SerializeStruct for Compound {
    type Ok = ();
    type Error = NonFinite;
    fn serialize_field<T: Serialize + ?Sized>(
        &mut self,
        _: &'static str,
        value: &T,
    ) -> Result<(), NonFinite> {
        value.serialize(Probe)
    }
    fn end(self) -> Result<(), NonFinite> {
        Ok(())
    }
}

impl ser::SerializeStructVariant for Compound {
    type Ok = ();
    type Error = NonFinite;
    fn serialize_field<T: Serialize + ?Sized>(
        &mut self,
        _: &'static str,
        value: &T,
    ) -> Result<(), NonFinite> {
        value.serialize(Probe)
    }
    fn end(self) -> Result<(), NonFinite> {
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::check;
    use serde::Serialize;

    #[derive(Serialize)]
    struct Nested {
        values: Vec<f32>,
        maybe: Option<f64>,
        label: String,
    }

    #[test]
    fn finite_structures_pass() {
        let value = Nested {
            values: vec![1.5, -2.25],
            maybe: Some(0.0),
            label: "ok".to_owned(),
        };
        assert!(check(&value).is_ok());
    }

    #[test]
    fn nan_anywhere_refuses() {
        let value = Nested {
            values: vec![1.5, f32::NAN],
            maybe: None,
            label: "bad".to_owned(),
        };
        let Err(refusal) = check(&value) else {
            unreachable!("NaN must refuse")
        };
        assert!(refusal.detail().contains("non-finite"));
    }

    #[test]
    fn infinity_in_option_refuses() {
        let value = Nested {
            values: Vec::new(),
            maybe: Some(f64::INFINITY),
            label: "bad".to_owned(),
        };
        assert!(check(&value).is_err());
    }
}