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:
@@ -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());
|
||||
}
|
||||
Reference in New Issue
Block a user