feat(interface): add authoring macros and codecs

Add the wip-interface facade and proc-macro implementation for typed interface descriptors, DTO value codecs, validated dispatch, metadata extraction, and span-aware compile failures.
This commit is contained in:
2026-09-28 23:11:17 +09:00
parent 6b6e96c303
commit 18ac8844b0
32 changed files with 2991 additions and 1 deletions
+157
View File
@@ -0,0 +1,157 @@
use wip_interface::wip_protocol::{
EnumDeclaration, JsonMember, JsonValue, NamedValue, RecordDeclaration, TypeDeclaration,
TypeExpr, UnionDeclaration, Value,
};
use wip_interface::{Bytes, Entry, WipType};
/// A request record.
#[derive(Debug, Clone, PartialEq, WipType)]
struct Request {
/// Required title.
title: String,
/// Optional priority.
priority: Option<i32>,
/// Related entries.
related: Vec<Entry<String, bool>>,
/// Opaque bytes.
body: Bytes,
/// Arbitrary metadata.
metadata: JsonValue,
}
/// Available colors.
#[derive(Debug, Clone, PartialEq, WipType)]
enum Color {
/// Warm red.
Red,
/// Cool blue.
Blue,
}
/// A receipt record.
#[derive(Debug, Clone, PartialEq, WipType)]
struct Receipt {
/// Receipt number.
number: u32,
}
/// Application-level publication outcome.
#[derive(Debug, Clone, PartialEq, WipType)]
enum PublishOutcome {
/// Application success.
Published(Receipt),
/// Domain rejection, not a host failure.
Rejected(String),
}
#[test]
fn supported_values_round_trip() {
let request = Request {
title: "hello".into(),
priority: None,
related: vec![Entry {
key: "/articles/1".into(),
value: true,
}],
body: Bytes(vec![1, 2, 3]),
metadata: JsonValue::Object(vec![JsonMember {
name: "source".into(),
value: JsonValue::String("test".into()),
}]),
};
let encoded = request.clone().encode().unwrap();
let Value::Record(fields) = &encoded else {
panic!("record derive must encode a protocol record")
};
assert!(fields.iter().all(|field| field.name != "priority"));
assert_eq!(Request::decode(encoded).unwrap(), request);
for color in [Color::Red, Color::Blue] {
let encoded = color.clone().encode().unwrap();
let decoded = Color::decode(encoded).unwrap();
assert_eq!(decoded, color);
}
for outcome in [
PublishOutcome::Published(Receipt { number: 7 }),
PublishOutcome::Rejected("policy".into()),
] {
let encoded = outcome.clone().encode().unwrap();
let decoded = PublishOutcome::decode(encoded).unwrap();
assert_eq!(decoded, outcome);
}
}
#[test]
fn derived_declarations_include_docs_optionality_and_dependencies() {
let mut declarations = Vec::new();
Request::collect_declarations(&mut declarations);
Color::collect_declarations(&mut declarations);
PublishOutcome::collect_declarations(&mut declarations);
let TypeDeclaration::Record(RecordDeclaration {
documentation,
fields,
..
}) = &declarations[0]
else {
panic!("Request must be a record declaration")
};
assert_eq!(documentation.as_ref().unwrap().summary, "A request record.");
assert_eq!(
fields[0].documentation.as_ref().unwrap().summary,
"Required title."
);
assert!(!fields[0].optional);
assert!(fields[1].optional);
assert_eq!(fields[1].ty, TypeExpr::Integer);
assert_eq!(
fields[2].ty,
TypeExpr::List(Box::new(TypeExpr::Entry {
key: Box::new(TypeExpr::String),
value: Box::new(TypeExpr::Boolean),
}))
);
let color = declarations
.iter()
.find(|declaration| declaration.name() == "Color")
.unwrap();
let TypeDeclaration::Enum(EnumDeclaration { variants, .. }) = color else {
panic!("Color must be an enum declaration")
};
assert_eq!(
variants[0].documentation.as_ref().unwrap().summary,
"Warm red."
);
let outcome = declarations
.iter()
.find(|declaration| declaration.name() == "PublishOutcome")
.unwrap();
let TypeDeclaration::Union(UnionDeclaration { variants, .. }) = outcome else {
panic!("PublishOutcome must be a union declaration")
};
assert_eq!(variants[0].ty, TypeExpr::Named("Receipt".into()));
assert!(
declarations
.iter()
.any(|declaration| declaration.name() == "Receipt")
);
}
#[test]
fn codecs_reject_invalid_boundaries() {
assert!(i64::MAX.encode().is_err());
assert!(f64::NAN.encode().is_err());
assert!(Request::decode(Value::Record(vec![])).is_err());
assert!(
Request::decode(Value::Record(vec![NamedValue {
name: "unknown".into(),
value: Value::String("value".into()),
}]))
.is_err()
);
assert!(Color::decode(Value::Enum("Green".into())).is_err());
}