reasoninglayer 1.0.3

Rust client SDK for the Reasoning Layer API
Documentation
//! Tagged value types used by term CRUD, query, and fuzzy endpoints.
//!
//! These types serialize as `{"type": "Kind", "value": ...}` with the discriminator field `type`,
//! matching the backend `ValueDto` in `dto/term.rs`. Use with: term CRUD (`/api/v1/terms`),
//! queries (`/api/v1/query`), fuzzy operations. Do **not** use with inference — inference uses the
//! untagged [`FeatureInputValueDto`](super::homoiconic::FeatureInputValueDto) format.

use serde::{Deserialize, Serialize};

/// Tagged discriminated union for all value types.
///
/// # Serialization
///
/// Tagged by `type`, with the payload under `value` for variants that carry data:
///
/// ```json
/// {"type": "String", "value": "hello"}
/// {"type": "Integer", "value": 42}
/// {"type": "Uninstantiated"}
/// ```
///
/// The backend may convert domain-internal types (BigInteger, DateTime, Geometry, etc.) to
/// `String` with Rust debug format. You may encounter strings like
/// `"DateTime(2024-01-15T10:30:00Z)"`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", content = "value")]
pub enum ValueDto {
    /// A string value.
    String(String),
    /// An integer value (i64).
    Integer(i64),
    /// A real (floating-point) value (f64).
    Real(f64),
    /// A boolean value.
    Boolean(bool),
    /// An uninstantiated (unknown) value — serializes as `{"type": "Uninstantiated"}`.
    Uninstantiated,
    /// A reference to another term by UUID.
    Reference(String),
    /// A list of values.
    List(Vec<ValueDto>),
    /// A fuzzy scalar with a value and membership degree.
    FuzzyScalar(FuzzyScalar),
    /// A fuzzy number defined by a membership function shape.
    FuzzyNumber(FuzzyNumber),
    /// A set value with lower and upper bounds.
    Set(SetValue),
}

/// Payload for [`ValueDto::FuzzyScalar`].
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FuzzyScalar {
    /// The scalar value.
    pub value: f64,
    /// Membership degree in `[0.0, 1.0]`.
    pub membership: f64,
}

/// Payload for [`ValueDto::FuzzyNumber`].
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FuzzyNumber {
    /// Fuzzy membership function shape.
    pub shape: FuzzyShapeDto,
}

/// Payload for [`ValueDto::Set`] — Smyth powerdomain-style set with lower/upper bounds.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SetValue {
    /// Lower bound — definite members (term UUIDs).
    pub lower: Vec<String>,
    /// Upper bound — possible members (term UUIDs).
    pub upper: Vec<String>,
    /// Optional sort constraint for set members.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub sort_constraint: Option<String>,
}

/// Fuzzy membership function shape.
///
/// **Discriminator field is `kind`**, not `type` — this differs from most other tagged unions
/// in the API. The backend uses `#[serde(tag = "kind")]`.
///
/// ```json
/// {"kind": "Triangular", "a": 20, "b": 22, "c": 24}
/// {"kind": "Gaussian", "mean": 100, "std_dev": 15}
/// ```
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum FuzzyShapeDto {
    /// Triangular: `a → peak at b → c`.
    Triangular { a: f64, b: f64, c: f64 },
    /// Trapezoidal: `a → plateau [b,c] → d`.
    Trapezoidal { a: f64, b: f64, c: f64, d: f64 },
    /// Gaussian: bell curve centred at `mean` with width `std_dev`.
    Gaussian { mean: f64, std_dev: f64 },
    /// Cyclic Gaussian with periodic wrapping.
    CyclicGaussian {
        mean: f64,
        std_dev: f64,
        period: f64,
    },
    /// Sigmoid (logistic) S-curve transition.
    Sigmoid { midpoint: f64, steepness: f64 },
    /// Generalised bell with tunable slope.
    Bell { center: f64, width: f64, slope: f64 },
    /// Difference of two sigmoids — smooth bounded region.
    SigmoidDifference {
        midpoint1: f64,
        steepness1: f64,
        midpoint2: f64,
        steepness2: f64,
    },
    /// Product of two Gaussians — asymmetric flat-top bell.
    GaussianProduct {
        mean1: f64,
        std_dev1: f64,
        mean2: f64,
        std_dev2: f64,
    },
    /// Product of two sigmoids — always non-negative bounded region.
    SigmoidProduct {
        midpoint1: f64,
        steepness1: f64,
        midpoint2: f64,
        steepness2: f64,
    },
    /// Cosine with compact support.
    Cosine { center: f64, width: f64 },
    /// Spike (Laplacian) with exponential tails.
    Spike { center: f64, width: f64 },
    /// Cauchy (Lorentzian) with heavy tails.
    Cauchy { center: f64, gamma: f64 },
    /// S-shaped: piecewise quadratic 0→1.
    SShape { a: f64, b: f64 },
    /// Z-shaped: piecewise quadratic 1→0.
    ZShape { a: f64, b: f64 },
    /// Pi-shaped: smooth flat-top bump.
    PiShape { a: f64, b: f64, c: f64, d: f64 },
    /// Piecewise linear membership function.
    PiecewiseLinear { points: Vec<(f64, f64)> },
}

/// Tagged feature value used in `OsfConstraintDto::Feature`. Like [`ValueDto`] but with only the
/// six variants that are meaningful as constraint targets.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", content = "value")]
pub enum TaggedFeatureValueDto {
    /// A string value.
    String(String),
    /// An integer value.
    Integer(i64),
    /// A real (f64) value.
    Real(f64),
    /// A boolean value.
    Boolean(bool),
    /// A reference to another term.
    Reference(String),
    /// A list of feature values.
    List(Vec<TaggedFeatureValueDto>),
}

/// Feature target in `OsfConstraintDto` — either a tagged feature value or a feature path string.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum FeatureTargetDto {
    /// A tagged value.
    Value(TaggedFeatureValueDto),
    /// A feature path reference string.
    Path(String),
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::{json, Value};

    #[test]
    fn string_value_serializes_tagged() {
        let v = ValueDto::String("hello".into());
        assert_eq!(
            serde_json::to_value(&v).unwrap(),
            json!({"type": "String", "value": "hello"})
        );
    }

    #[test]
    fn integer_value_serializes_tagged() {
        let v = ValueDto::Integer(42);
        assert_eq!(
            serde_json::to_value(&v).unwrap(),
            json!({"type": "Integer", "value": 42})
        );
    }

    #[test]
    fn uninstantiated_serializes_without_value() {
        let v = ValueDto::Uninstantiated;
        let j: Value = serde_json::to_value(&v).unwrap();
        assert_eq!(j, json!({"type": "Uninstantiated"}));
    }

    #[test]
    fn real_value_roundtrip() {
        let v = ValueDto::Real(2.5);
        let j = serde_json::to_string(&v).unwrap();
        let back: ValueDto = serde_json::from_str(&j).unwrap();
        assert_eq!(back, v);
    }

    #[test]
    fn list_value_roundtrip() {
        let v = ValueDto::List(vec![ValueDto::String("a".into()), ValueDto::Integer(1)]);
        let j = serde_json::to_string(&v).unwrap();
        let back: ValueDto = serde_json::from_str(&j).unwrap();
        assert_eq!(back, v);
    }

    #[test]
    fn fuzzy_shape_uses_kind_tag() {
        let v = FuzzyShapeDto::Triangular {
            a: 20.0,
            b: 22.0,
            c: 24.0,
        };
        let j = serde_json::to_value(&v).unwrap();
        assert_eq!(
            j,
            json!({"kind": "Triangular", "a": 20.0, "b": 22.0, "c": 24.0})
        );
    }

    #[test]
    fn gaussian_uses_snake_case_std_dev() {
        let v = FuzzyShapeDto::Gaussian {
            mean: 100.0,
            std_dev: 15.0,
        };
        let j = serde_json::to_value(&v).unwrap();
        assert_eq!(
            j,
            json!({"kind": "Gaussian", "mean": 100.0, "std_dev": 15.0})
        );
    }

    #[test]
    fn set_value_roundtrip() {
        let v = ValueDto::Set(SetValue {
            lower: vec!["a".into()],
            upper: vec!["a".into(), "b".into()],
            sort_constraint: Some("sort-1".into()),
        });
        let j = serde_json::to_string(&v).unwrap();
        let back: ValueDto = serde_json::from_str(&j).unwrap();
        assert_eq!(back, v);
    }
}