use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PsiTermDto {
pub term_id: String,
pub sort_id: String,
pub sort_name: String,
pub features: BTreeMap<String, FeatureValueDto>,
pub display: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum FeatureValueDto {
List(Vec<FeatureValueDto>),
String(String),
Integer(i64),
Real(f64),
Boolean(bool),
Null,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TermInputRef {
pub term_id: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TermInputInline {
pub sort_id: String,
pub features: BTreeMap<String, FeatureInputValueDto>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TermInputInlineByName {
pub sort_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub features: Option<BTreeMap<String, FeatureInputValueDto>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum TermInputDto {
Reference(TermInputRef),
Inline(TermInputInline),
InlineByName(TermInputInlineByName),
}
impl TermInputDto {
pub fn reference(term_id: impl Into<String>) -> Self {
Self::Reference(TermInputRef {
term_id: term_id.into(),
})
}
pub fn by_name(sort_name: impl Into<String>) -> Self {
Self::InlineByName(TermInputInlineByName {
sort_name: sort_name.into(),
features: None,
})
}
pub fn by_name_with(
sort_name: impl Into<String>,
features: BTreeMap<String, FeatureInputValueDto>,
) -> Self {
Self::InlineByName(TermInputInlineByName {
sort_name: sort_name.into(),
features: Some(features),
})
}
pub fn by_sort_id(
sort_id: impl Into<String>,
features: BTreeMap<String, FeatureInputValueDto>,
) -> Self {
Self::Inline(TermInputInline {
sort_id: sort_id.into(),
features,
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FeatureInputTermRef {
pub term_id: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FeatureInputConstrainedVariable {
pub name: String,
pub constraint: Box<TermInputDto>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FeatureInputVariable {
pub name: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FeatureInputInlineTerm {
pub sort_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub features: Option<BTreeMap<String, FeatureInputValueDto>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FeatureInputInlineTermByName {
pub sort_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub features: Option<BTreeMap<String, FeatureInputValueDto>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum FeatureInputValueDto {
TermRef(FeatureInputTermRef),
ConstrainedVariable(FeatureInputConstrainedVariable),
Variable(FeatureInputVariable),
InlineTerm(FeatureInputInlineTerm),
InlineTermByName(FeatureInputInlineTermByName),
List(Vec<FeatureInputValueDto>),
String(String),
Integer(i64),
Real(f64),
Boolean(bool),
Null,
}
impl FeatureInputValueDto {
pub fn string(s: impl Into<String>) -> Self {
Self::String(s.into())
}
pub fn variable(name: impl Into<String>) -> Self {
Self::Variable(FeatureInputVariable { name: name.into() })
}
pub fn constrained(name: impl Into<String>, constraint: TermInputDto) -> Self {
Self::ConstrainedVariable(FeatureInputConstrainedVariable {
name: name.into(),
constraint: Box::new(constraint),
})
}
pub fn term_ref(term_id: impl Into<String>) -> Self {
Self::TermRef(FeatureInputTermRef {
term_id: term_id.into(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn variable_roundtrip() {
let v = FeatureInputValueDto::variable("?X");
let j = serde_json::to_value(&v).unwrap();
assert_eq!(j, json!({"name": "?X"}));
let back: FeatureInputValueDto = serde_json::from_value(j).unwrap();
assert_eq!(back, v);
}
#[test]
fn constrained_variable_routed_correctly() {
let v = FeatureInputValueDto::constrained("?S", TermInputDto::by_name("guard_constraint"));
let j = serde_json::to_value(&v).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:?}"),
}
}
#[test]
fn term_ref_roundtrip() {
let v = FeatureInputValueDto::term_ref("term-uuid");
let j = serde_json::to_value(&v).unwrap();
assert_eq!(j, json!({"term_id": "term-uuid"}));
}
#[test]
fn primitive_values_roundtrip() {
for v in [
FeatureInputValueDto::String("hello".into()),
FeatureInputValueDto::Integer(42),
FeatureInputValueDto::Real(2.5),
FeatureInputValueDto::Boolean(true),
FeatureInputValueDto::Null,
] {
let s = serde_json::to_string(&v).unwrap();
let back: FeatureInputValueDto = serde_json::from_str(&s).unwrap();
assert_eq!(back, v);
}
}
#[test]
fn inline_term_by_name_optional_features() {
let v = TermInputDto::by_name("person");
let j = serde_json::to_value(&v).unwrap();
assert_eq!(j, json!({"sort_name": "person"}));
let mut features = BTreeMap::new();
features.insert("name".into(), FeatureInputValueDto::String("Alice".into()));
let v = TermInputDto::by_name_with("person", features);
let j = serde_json::to_value(&v).unwrap();
assert_eq!(
j,
json!({"sort_name": "person", "features": {"name": "Alice"}})
);
}
#[test]
fn term_input_reference_roundtrip() {
let v = TermInputDto::reference("some-uuid");
let j = serde_json::to_value(&v).unwrap();
assert_eq!(j, json!({"term_id": "some-uuid"}));
}
}