use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
pub struct Span {
pub line: usize,
pub col: usize,
pub end_line: usize,
pub end_col: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TypeField {
pub name: String,
pub ty: TypeRef,
pub docs: Option<String>,
pub span: Span,
#[serde(default)]
pub constraints: Vec<FieldConstraint>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FieldConstraint {
Tier1 {
phrase: String,
args: serde_json::Value,
span: Span,
},
ProseRule { text: String, span: Span },
ValidateWith { name: String, span: Span },
}
impl FieldConstraint {
pub fn span(&self) -> &Span {
match self {
FieldConstraint::Tier1 { span, .. } => span,
FieldConstraint::ProseRule { span, .. } => span,
FieldConstraint::ValidateWith { span, .. } => span,
}
}
}
pub const MAX_UNION_ARMS: usize = 8;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TypeRef {
pub name: String,
pub inner: Option<Box<TypeRef>>,
pub choices: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub variants: Option<Vec<TypeRef>>,
#[serde(default)]
pub span: Span,
}
pub const VARIANT_UNION_SENTINEL: &str = "variant_union";
pub const OPTIONAL_SENTINEL: &str = "optional";
impl TypeRef {
pub fn primitive(name: impl Into<String>) -> Self {
Self {
name: name.into(),
inner: None,
choices: None,
variants: None,
span: Span::default(),
}
}
pub fn primitive_with_span(name: impl Into<String>, span: Span) -> Self {
Self {
name: name.into(),
inner: None,
choices: None,
variants: None,
span,
}
}
pub fn optional(inner: TypeRef) -> Self {
if inner.is_optional() {
return inner;
}
Self {
name: OPTIONAL_SENTINEL.to_string(),
inner: Some(Box::new(inner)),
choices: None,
variants: None,
span: Span::default(),
}
}
pub fn optional_with_span(inner: TypeRef, span: Span) -> Self {
if inner.is_optional() {
return inner;
}
Self {
name: OPTIONAL_SENTINEL.to_string(),
inner: Some(Box::new(inner)),
choices: None,
variants: None,
span,
}
}
pub fn is_optional(&self) -> bool {
self.name == OPTIONAL_SENTINEL && self.inner.is_some()
}
pub fn optional_inner(&self) -> Option<&TypeRef> {
if self.is_optional() {
self.inner.as_deref()
} else {
None
}
}
pub fn variant_union(arms: Vec<TypeRef>) -> Self {
debug_assert!(arms.len() >= 2, "variant union requires >= 2 arms");
Self {
name: VARIANT_UNION_SENTINEL.to_string(),
inner: None,
choices: None,
variants: Some(arms),
span: Span::default(),
}
}
pub fn variant_union_with_span(arms: Vec<TypeRef>, span: Span) -> Self {
debug_assert!(arms.len() >= 2, "variant union requires >= 2 arms");
Self {
name: VARIANT_UNION_SENTINEL.to_string(),
inner: None,
choices: None,
variants: Some(arms),
span,
}
}
pub fn union_with_unable(success: TypeRef) -> Self {
Self::variant_union(vec![success, TypeRef::primitive("Unable")])
}
pub fn is_variant_union(&self) -> bool {
self.name == VARIANT_UNION_SENTINEL && self.variants.is_some()
}
pub fn union_arms(&self) -> Option<&[TypeRef]> {
self.variants.as_deref()
}
pub fn is_union_with_unable(&self) -> bool {
match self.variants.as_deref() {
Some(arms) if arms.len() == 2 => {
(arms[0].name == "Unable") ^ (arms[1].name == "Unable")
}
_ => false,
}
}
pub fn unwrap_union_success(&self) -> Option<&TypeRef> {
match self.variants.as_deref() {
Some(arms) if arms.len() == 2 => {
if arms[0].name == "Unable" && arms[1].name != "Unable" {
Some(&arms[1])
} else if arms[1].name == "Unable" && arms[0].name != "Unable" {
Some(&arms[0])
} else {
None
}
}
_ => None,
}
}
pub fn union_success_arm(&self) -> Option<&TypeRef> {
self.variants.as_deref().and_then(|arms| arms.first())
}
pub fn display(&self) -> String {
if let Some(inner) = self.optional_inner() {
return format!("{}?", inner.display());
}
if let Some(arms) = &self.variants {
return arms
.iter()
.map(|a| a.display())
.collect::<Vec<_>>()
.join(" | ");
}
if let Some(choices) = &self.choices {
choices
.iter()
.map(|c| format!("\"{}\"", c))
.collect::<Vec<_>>()
.join(" | ")
} else if let Some(inner) = &self.inner {
format!("{}[{}]", self.name, inner.display())
} else {
self.name.clone()
}
}
}
impl fmt::Display for TypeRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.display())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ActorHint {
Human,
Any,
Client(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn span_default_is_all_zero() {
let s = Span::default();
assert_eq!(s.line, 0);
assert_eq!(s.col, 0);
assert_eq!(s.end_line, 0);
assert_eq!(s.end_col, 0);
}
#[test]
fn typeref_primitive_has_default_span() {
let ty = TypeRef::primitive("str");
assert_eq!(ty.span, Span::default());
}
#[test]
fn typeref_primitive_with_span_carries_span() {
let s = Span {
line: 4,
col: 7,
end_line: 4,
end_col: 10,
};
let ty = TypeRef::primitive_with_span("str", s.clone());
assert_eq!(ty.span, s);
}
#[test]
fn typeref_serde_roundtrip_preserves_span() {
let s = Span {
line: 2,
col: 5,
end_line: 2,
end_col: 9,
};
let ty = TypeRef::primitive_with_span("str", s.clone());
let json = serde_json::to_string(&ty).unwrap();
let back: TypeRef = serde_json::from_str(&json).unwrap();
assert_eq!(back.name, "str");
assert_eq!(back.span, s);
}
#[test]
fn typeref_deserializes_legacy_payload_without_span_field() {
let legacy = r#"{"name":"str","inner":null,"choices":null}"#;
let ty: TypeRef = serde_json::from_str(legacy).unwrap();
assert_eq!(ty.name, "str");
assert_eq!(ty.span, Span::default());
}
#[test]
fn typeref_deserializes_legacy_list_payload_without_span() {
let legacy =
r#"{"name":"list","inner":{"name":"str","inner":null,"choices":null},"choices":null}"#;
let ty: TypeRef = serde_json::from_str(legacy).unwrap();
assert_eq!(ty.name, "list");
assert_eq!(ty.span, Span::default());
let inner = ty.inner.as_deref().unwrap();
assert_eq!(inner.name, "str");
assert_eq!(inner.span, Span::default());
}
#[test]
fn typeref_deserializes_legacy_choice_payload_without_span() {
let legacy = r#"{"name":"choice","inner":null,"choices":["a","b"]}"#;
let ty: TypeRef = serde_json::from_str(legacy).unwrap();
assert_eq!(ty.name, "choice");
assert_eq!(
ty.choices.as_deref().unwrap(),
&["a".to_string(), "b".to_string()]
);
assert_eq!(ty.span, Span::default());
}
#[test]
fn typeref_deserializes_legacy_payload_without_variants_or_span() {
let legacy = r#"{"name":"str","inner":null,"choices":null}"#;
let ty: TypeRef = serde_json::from_str(legacy).unwrap();
assert_eq!(ty.name, "str");
assert!(ty.variants.is_none());
assert_eq!(ty.span, Span::default());
}
#[test]
fn typeref_variant_union_roundtrip_preserves_arm_spans() {
let arm_a = TypeRef::primitive_with_span(
"A",
Span {
line: 1,
col: 1,
end_line: 1,
end_col: 2,
},
);
let arm_b = TypeRef::primitive_with_span(
"B",
Span {
line: 1,
col: 5,
end_line: 1,
end_col: 6,
},
);
let outer = TypeRef::variant_union_with_span(
vec![arm_a, arm_b],
Span {
line: 1,
col: 1,
end_line: 1,
end_col: 6,
},
);
let json = serde_json::to_string(&outer).unwrap();
let back: TypeRef = serde_json::from_str(&json).unwrap();
assert!(back.is_variant_union());
let arms = back.union_arms().unwrap();
assert_eq!(arms[0].name, "A");
assert_eq!(arms[0].span.col, 1);
assert_eq!(arms[1].name, "B");
assert_eq!(arms[1].span.col, 5);
assert_eq!(back.span.end_col, 6);
}
#[test]
fn typeref_optional_with_span_returns_inner_if_already_optional() {
let inner = TypeRef::primitive_with_span(
"int",
Span {
line: 1,
col: 1,
end_line: 1,
end_col: 4,
},
);
let first = TypeRef::optional_with_span(
inner,
Span {
line: 1,
col: 1,
end_line: 1,
end_col: 5,
},
);
let twice = TypeRef::optional_with_span(
first.clone(),
Span {
line: 9,
col: 9,
end_line: 9,
end_col: 9,
},
);
assert_eq!(twice.span, first.span);
}
}