use serde::{Deserialize, Serialize};
pub use crate::validate::Constraint;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Ir {
pub ir_version: u32,
pub procedures: Vec<Procedure>,
pub types: Vec<TypeDef>,
}
impl Ir {
pub const CURRENT_VERSION: u32 = 1;
#[must_use]
pub fn empty() -> Self {
Self {
ir_version: Self::CURRENT_VERSION,
procedures: vec![],
types: vec![],
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Procedure {
pub name: String,
pub kind: ProcKind,
pub input: TypeRef,
pub output: TypeRef,
pub errors: Vec<TypeRef>,
pub http_method: HttpMethod,
pub doc: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProcKind {
Query,
Mutation,
Subscription,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HttpMethod {
Post,
Get,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TypeDef {
pub name: String,
pub doc: Option<String>,
pub shape: TypeShape,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "data", rename_all = "snake_case")]
pub enum TypeShape {
Struct(Vec<Field>),
Enum(EnumDef),
Tuple(Vec<TypeRef>),
Newtype(TypeRef),
Alias(TypeRef),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Field {
pub name: String,
pub ty: TypeRef,
pub optional: bool,
pub undefined: bool,
pub doc: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub constraints: Vec<Constraint>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EnumDef {
pub tag: String,
pub variants: Vec<Variant>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Variant {
pub name: String,
pub payload: VariantPayload,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "data", rename_all = "snake_case")]
pub enum VariantPayload {
Unit,
Tuple(Vec<TypeRef>),
Struct(Vec<Field>),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "data", rename_all = "snake_case")]
pub enum TypeRef {
Primitive(Primitive),
Named(String),
Option(Box<TypeRef>),
Vec(Box<TypeRef>),
Map {
key: Box<TypeRef>,
value: Box<TypeRef>,
},
Tuple(Vec<TypeRef>),
FixedArray {
elem: Box<TypeRef>,
len: u64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Primitive {
Bool,
U8,
U16,
U32,
U64,
I8,
I16,
I32,
I64,
U128,
I128,
F32,
F64,
String,
Bytes,
Unit,
DateTime,
Uuid,
}
impl std::fmt::Display for Primitive {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Primitive::Bool => "bool",
Primitive::U8 => "u8",
Primitive::U16 => "u16",
Primitive::U32 => "u32",
Primitive::U64 => "u64",
Primitive::I8 => "i8",
Primitive::I16 => "i16",
Primitive::I32 => "i32",
Primitive::I64 => "i64",
Primitive::U128 => "u128",
Primitive::I128 => "i128",
Primitive::F32 => "f32",
Primitive::F64 => "f64",
Primitive::String => "String",
Primitive::Bytes => "Bytes",
Primitive::Unit => "()",
Primitive::DateTime => "DateTime",
Primitive::Uuid => "Uuid",
};
f.write_str(s)
}
}
impl std::fmt::Display for TypeRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TypeRef::Primitive(p) => write!(f, "{p}"),
TypeRef::Named(name) => f.write_str(name),
TypeRef::Vec(inner) => write!(f, "Vec<{inner}>"),
TypeRef::Option(inner) => write!(f, "Option<{inner}>"),
TypeRef::Map { key, value } => write!(f, "HashMap<{key}, {value}>"),
TypeRef::Tuple(elems) => {
f.write_str("(")?;
for (i, t) in elems.iter().enumerate() {
if i > 0 {
f.write_str(", ")?;
}
write!(f, "{t}")?;
}
f.write_str(")")
}
TypeRef::FixedArray { elem, len } => write!(f, "[{elem}; {len}]"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn roundtrip(ir: &Ir) -> Ir {
let json = serde_json::to_string(ir).expect("serialize");
serde_json::from_str(&json).expect("deserialize")
}
#[test]
fn empty_ir_roundtrips() {
let ir = Ir::empty();
assert_eq!(ir.ir_version, Ir::CURRENT_VERSION);
assert!(ir.procedures.is_empty());
assert!(ir.types.is_empty());
assert_eq!(roundtrip(&ir), ir);
}
#[test]
fn query_with_string_input_and_user_output_roundtrips() {
let ir = Ir {
ir_version: Ir::CURRENT_VERSION,
procedures: vec![Procedure {
name: "get_user".to_string(),
kind: ProcKind::Query,
input: TypeRef::Primitive(Primitive::String),
output: TypeRef::Named("User".to_string()),
errors: vec![],
http_method: HttpMethod::Post,
doc: Some("Fetch a user by id.".to_string()),
}],
types: vec![TypeDef {
name: "User".to_string(),
doc: None,
shape: TypeShape::Struct(vec![
Field {
name: "id".to_string(),
ty: TypeRef::Primitive(Primitive::U64),
optional: false,
undefined: false,
doc: None,
constraints: vec![],
},
Field {
name: "name".to_string(),
ty: TypeRef::Primitive(Primitive::String),
optional: false,
undefined: false,
doc: None,
constraints: vec![],
},
Field {
name: "nickname".to_string(),
ty: TypeRef::Option(Box::new(TypeRef::Primitive(Primitive::String))),
optional: true,
undefined: false,
doc: None,
constraints: vec![],
},
]),
}],
};
assert_eq!(roundtrip(&ir), ir);
}
#[test]
fn discriminated_union_enum_roundtrips() {
let ir = Ir {
ir_version: Ir::CURRENT_VERSION,
procedures: vec![],
types: vec![TypeDef {
name: "Event".to_string(),
doc: Some("Server-emitted event variants.".to_string()),
shape: TypeShape::Enum(EnumDef {
tag: "type".to_string(),
variants: vec![
Variant {
name: "Ping".to_string(),
payload: VariantPayload::Unit,
},
Variant {
name: "Message".to_string(),
payload: VariantPayload::Tuple(vec![TypeRef::Primitive(
Primitive::String,
)]),
},
Variant {
name: "User".to_string(),
payload: VariantPayload::Struct(vec![
Field {
name: "id".to_string(),
ty: TypeRef::Primitive(Primitive::Uuid),
optional: false,
undefined: false,
doc: None,
constraints: vec![],
},
Field {
name: "name".to_string(),
ty: TypeRef::Primitive(Primitive::String),
optional: false,
undefined: true,
doc: None,
constraints: vec![],
},
]),
},
],
}),
}],
};
assert_eq!(roundtrip(&ir), ir);
}
#[test]
fn type_ref_display_renders_each_variant() {
assert_eq!(TypeRef::Primitive(Primitive::U64).to_string(), "u64");
assert_eq!(TypeRef::Primitive(Primitive::Bool).to_string(), "bool");
assert_eq!(TypeRef::Primitive(Primitive::String).to_string(), "String");
assert_eq!(TypeRef::Primitive(Primitive::Unit).to_string(), "()");
assert_eq!(TypeRef::Primitive(Primitive::Uuid).to_string(), "Uuid");
assert_eq!(TypeRef::Named("User".to_string()).to_string(), "User");
assert_eq!(
TypeRef::Vec(Box::new(TypeRef::Primitive(Primitive::U32))).to_string(),
"Vec<u32>",
);
assert_eq!(
TypeRef::Option(Box::new(TypeRef::Named("User".to_string()))).to_string(),
"Option<User>",
);
assert_eq!(
TypeRef::Map {
key: Box::new(TypeRef::Primitive(Primitive::String)),
value: Box::new(TypeRef::Primitive(Primitive::U64)),
}
.to_string(),
"HashMap<String, u64>",
);
assert_eq!(
TypeRef::Tuple(vec![
TypeRef::Primitive(Primitive::U64),
TypeRef::Primitive(Primitive::String),
])
.to_string(),
"(u64, String)",
);
assert_eq!(
TypeRef::FixedArray {
elem: Box::new(TypeRef::Primitive(Primitive::U8)),
len: 32,
}
.to_string(),
"[u8; 32]",
);
let nested = TypeRef::Vec(Box::new(TypeRef::Option(Box::new(TypeRef::Map {
key: Box::new(TypeRef::Primitive(Primitive::String)),
value: Box::new(TypeRef::Named("User".to_string())),
}))));
assert_eq!(nested.to_string(), "Vec<Option<HashMap<String, User>>>");
}
#[test]
fn field_roundtrips_with_constraints() {
let field = Field {
name: "score".to_string(),
ty: TypeRef::Primitive(Primitive::F64),
optional: false,
undefined: false,
doc: None,
constraints: vec![Constraint::Min(0.0), Constraint::Max(100.0)],
};
let json = serde_json::to_string(&field).expect("serialize Field");
let back: Field = serde_json::from_str(&json).expect("deserialize Field");
assert_eq!(back, field);
assert_eq!(back.constraints.len(), 2);
assert_eq!(back.constraints[0], Constraint::Min(0.0));
assert_eq!(back.constraints[1], Constraint::Max(100.0));
}
}