reasoninglayer 1.0.3

Rust client SDK for the Reasoning Layer API
Documentation
//! Builders for inference `TermInputDto` / `FeatureInputValueDto` — the untagged homoiconic format.
//!
//! # Example
//!
//! ```
//! use reasoninglayer::{psi, var, guard, constrained, GuardOp};
//! use reasoninglayer::types::homoiconic::FeatureInputValueDto;
//!
//! // Simple term:
//! let alice = psi("person", [
//!     ("name", FeatureInputValueDto::string("Alice")),
//!     ("age", FeatureInputValueDto::Integer(30)),
//! ]);
//!
//! // With a variable and a guard-constrained variable:
//! let rule_body = psi("employee", [
//!     ("name", var("?X")),
//!     ("salary", constrained("?S", guard(GuardOp::Gt, 80_000))),
//! ]);
//! # let _ = (alice, rule_body);
//! ```

use std::collections::BTreeMap;

use crate::types::homoiconic::{
    FeatureInputConstrainedVariable, FeatureInputTermRef, FeatureInputValueDto,
    FeatureInputVariable, TermInputDto,
};
use crate::types::inference::GuardOp;

/// Build a [`TermInputDto::InlineByName`] with the given sort name and features.
///
/// `features` can be any iterable of `(name, value)` pairs. Values must already be
/// [`FeatureInputValueDto`]; use the constructors on that type or helpers like [`var`] and
/// [`constrained`] to build them. For literal values, `FeatureInputValueDto::string("Alice")`,
/// `FeatureInputValueDto::Integer(30)`, etc. work directly.
pub fn psi<I, K>(sort_name: impl Into<String>, features: I) -> TermInputDto
where
    I: IntoIterator<Item = (K, FeatureInputValueDto)>,
    K: Into<String>,
{
    let mut map = BTreeMap::new();
    for (k, v) in features {
        map.insert(k.into(), v);
    }
    if map.is_empty() {
        TermInputDto::by_name(sort_name)
    } else {
        TermInputDto::by_name_with(sort_name, map)
    }
}

/// Build a [`TermInputDto::Inline`] by sort UUID with the given features.
pub fn psi_by_id<I, K>(sort_id: impl Into<String>, features: I) -> TermInputDto
where
    I: IntoIterator<Item = (K, FeatureInputValueDto)>,
    K: Into<String>,
{
    let mut map = BTreeMap::new();
    for (k, v) in features {
        map.insert(k.into(), v);
    }
    TermInputDto::by_sort_id(sort_id, map)
}

/// Shorthand for an unconstrained variable. Convention: prefix the name with `?`.
pub fn var(name: impl Into<String>) -> FeatureInputValueDto {
    FeatureInputValueDto::Variable(FeatureInputVariable { name: name.into() })
}

/// Shorthand for a constrained variable.
pub fn constrained(name: impl Into<String>, constraint: TermInputDto) -> FeatureInputValueDto {
    FeatureInputValueDto::ConstrainedVariable(FeatureInputConstrainedVariable {
        name: name.into(),
        constraint: Box::new(constraint),
    })
}

/// Shorthand for a term reference by UUID.
pub fn term_ref(term_id: impl Into<String>) -> FeatureInputValueDto {
    FeatureInputValueDto::TermRef(FeatureInputTermRef {
        term_id: term_id.into(),
    })
}

/// Build a `guard_constraint` [`TermInputDto`] for use with [`constrained`].
///
/// Produces the untagged equivalent of `{"sort_name": "guard_constraint", "features": {"op": "gt", "right": 100}}`.
pub fn guard(op: GuardOp, right: impl Into<GuardRhs>) -> TermInputDto {
    let rhs: GuardRhs = right.into();
    let mut features: BTreeMap<String, FeatureInputValueDto> = BTreeMap::new();
    features.insert(
        "op".into(),
        FeatureInputValueDto::String(op.as_str().to_string()),
    );
    features.insert("right".into(), rhs.into_feature());
    TermInputDto::by_name_with("guard_constraint", features)
}

/// Right-hand side of a [`guard`] — accepts integers, reals, strings, and booleans.
#[derive(Debug, Clone)]
pub enum GuardRhs {
    Integer(i64),
    Real(f64),
    String(String),
    Boolean(bool),
}

impl GuardRhs {
    fn into_feature(self) -> FeatureInputValueDto {
        match self {
            Self::Integer(i) => FeatureInputValueDto::Integer(i),
            Self::Real(r) => FeatureInputValueDto::Real(r),
            Self::String(s) => FeatureInputValueDto::String(s),
            Self::Boolean(b) => FeatureInputValueDto::Boolean(b),
        }
    }
}

impl From<i64> for GuardRhs {
    fn from(i: i64) -> Self {
        Self::Integer(i)
    }
}

impl From<i32> for GuardRhs {
    fn from(i: i32) -> Self {
        Self::Integer(i.into())
    }
}

impl From<f64> for GuardRhs {
    fn from(f: f64) -> Self {
        Self::Real(f)
    }
}

impl From<bool> for GuardRhs {
    fn from(b: bool) -> Self {
        Self::Boolean(b)
    }
}

impl From<&str> for GuardRhs {
    fn from(s: &str) -> Self {
        Self::String(s.to_string())
    }
}

impl From<String> for GuardRhs {
    fn from(s: String) -> Self {
        Self::String(s)
    }
}

// ─── FeatureInputValueDto From conveniences ───────────────────────────────────

impl From<&str> for FeatureInputValueDto {
    fn from(s: &str) -> Self {
        Self::String(s.to_string())
    }
}

impl From<String> for FeatureInputValueDto {
    fn from(s: String) -> Self {
        Self::String(s)
    }
}

impl From<i64> for FeatureInputValueDto {
    fn from(i: i64) -> Self {
        Self::Integer(i)
    }
}

impl From<i32> for FeatureInputValueDto {
    fn from(i: i32) -> Self {
        Self::Integer(i.into())
    }
}

impl From<f64> for FeatureInputValueDto {
    fn from(f: f64) -> Self {
        Self::Real(f)
    }
}

impl From<bool> for FeatureInputValueDto {
    fn from(b: bool) -> Self {
        Self::Boolean(b)
    }
}

impl<T: Into<FeatureInputValueDto>> From<Vec<T>> for FeatureInputValueDto {
    fn from(v: Vec<T>) -> Self {
        FeatureInputValueDto::List(v.into_iter().map(Into::into).collect())
    }
}

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

    #[test]
    fn psi_by_name_with_features() {
        let term = psi(
            "person",
            [
                ("name", FeatureInputValueDto::string("Alice")),
                ("age", FeatureInputValueDto::Integer(30)),
            ],
        );
        let j = serde_json::to_value(&term).unwrap();
        assert_eq!(
            j,
            json!({"sort_name": "person", "features": {"age": 30, "name": "Alice"}})
        );
    }

    #[test]
    fn psi_by_name_without_features() {
        let term = psi::<[(String, _); 0], String>("person", []);
        let j = serde_json::to_value(&term).unwrap();
        assert_eq!(j, json!({"sort_name": "person"}));
    }

    #[test]
    fn guard_produces_expected_shape() {
        let g = guard(GuardOp::Gt, 80_000_i64);
        let j = serde_json::to_value(&g).unwrap();
        assert_eq!(
            j,
            json!({
                "sort_name": "guard_constraint",
                "features": { "op": "gt", "right": 80000 }
            })
        );
    }

    #[test]
    fn constrained_variable_roundtrip() {
        let cv = constrained("?S", guard(GuardOp::Gte, 100_i64));
        let j = serde_json::to_value(&cv).unwrap();
        let back: FeatureInputValueDto = serde_json::from_value(j).unwrap();
        match back {
            FeatureInputValueDto::ConstrainedVariable(cv) => {
                assert_eq!(cv.name, "?S");
            }
            other => panic!("expected ConstrainedVariable, got {other:?}"),
        }
    }
}