232 lines
6.7 KiB
Rust
232 lines
6.7 KiB
Rust
use std::collections::BTreeMap;
|
|
|
|
use wip_interface::wip_protocol::{TypeExpr, UNION_CASE_FIELD, UNION_VALUE_FIELD, Value};
|
|
use wip_interface::{Bytes, Entry, JsonMember, JsonValue, WipType};
|
|
|
|
/// A request record.
|
|
#[derive(Debug, Clone, PartialEq, WipType)]
|
|
struct Request {
|
|
/// Required title.
|
|
title: String,
|
|
/// Optional priority.
|
|
priority: Option<i32>,
|
|
/// Related Worldspace entries.
|
|
related: Vec<Entry>,
|
|
/// 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 {
|
|
/// Work has not completed and carries no payload.
|
|
Pending,
|
|
/// Application success.
|
|
Published(Receipt),
|
|
/// Domain rejection, not a host failure.
|
|
Rejected(String),
|
|
}
|
|
|
|
fn field<'a>(record: &'a BTreeMap<String, Value>, name: &str) -> &'a Value {
|
|
record
|
|
.get(name)
|
|
.unwrap_or_else(|| panic!("missing `{name}`"))
|
|
}
|
|
|
|
#[test]
|
|
fn primitive_unit_record_list_entry_and_json_values_round_trip() {
|
|
assert_eq!(().encode().unwrap(), Value::Unit);
|
|
assert_eq!(<()>::decode(Value::Unit).unwrap(), ());
|
|
assert_eq!(<()>::type_expr(), TypeExpr::Unit);
|
|
|
|
let request = Request {
|
|
title: "hello".into(),
|
|
priority: None,
|
|
related: vec![Entry::new("/articles/1").unwrap()],
|
|
body: Bytes(vec![1, 2, 3]),
|
|
metadata: JsonValue::Object(vec![JsonMember {
|
|
name: "source".into(),
|
|
value: JsonValue::Array(vec![JsonValue::String("test".into()), JsonValue::Null]),
|
|
}]),
|
|
};
|
|
|
|
let encoded = request.clone().encode().unwrap();
|
|
let Value::Record(fields) = &encoded else {
|
|
panic!("record derive must encode a raw protocol record")
|
|
};
|
|
assert!(!fields.contains_key("priority"));
|
|
assert_eq!(
|
|
field(fields, "related"),
|
|
&Value::List(vec![Value::String("/articles/1".into())])
|
|
);
|
|
assert_eq!(
|
|
field(fields, "metadata"),
|
|
&Value::Record(BTreeMap::from([(
|
|
"source".into(),
|
|
Value::List(vec![Value::String("test".into()), Value::Unit]),
|
|
)]))
|
|
);
|
|
assert_eq!(Request::decode(encoded).unwrap(), request);
|
|
|
|
let number = 42.5_f64;
|
|
assert_eq!(f64::type_expr(), TypeExpr::Number);
|
|
assert_eq!(f64::decode(number.encode().unwrap()).unwrap(), number);
|
|
|
|
let narrow_number = 1.5_f32;
|
|
assert_eq!(f32::type_expr(), TypeExpr::Number);
|
|
assert_eq!(
|
|
f32::decode(narrow_number.encode().unwrap()).unwrap(),
|
|
narrow_number
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn enums_are_strings_and_unions_are_discriminated_records() {
|
|
for color in [Color::Red, Color::Blue] {
|
|
let encoded = color.clone().encode().unwrap();
|
|
assert!(matches!(encoded, Value::String(_)));
|
|
assert_eq!(Color::decode(encoded).unwrap(), color);
|
|
}
|
|
|
|
let pending = PublishOutcome::Pending.encode().unwrap();
|
|
assert_eq!(
|
|
pending,
|
|
Value::Record(BTreeMap::from([(
|
|
UNION_CASE_FIELD.into(),
|
|
Value::String("Pending".into()),
|
|
)]))
|
|
);
|
|
|
|
let published = PublishOutcome::Published(Receipt { number: 7 })
|
|
.encode()
|
|
.unwrap();
|
|
let Value::Record(fields) = &published else {
|
|
panic!("union must encode as a record")
|
|
};
|
|
assert_eq!(
|
|
fields.get(UNION_CASE_FIELD),
|
|
Some(&Value::String("Published".into()))
|
|
);
|
|
assert!(matches!(
|
|
fields.get(UNION_VALUE_FIELD),
|
|
Some(Value::Record(_))
|
|
));
|
|
|
|
for outcome in [
|
|
PublishOutcome::Pending,
|
|
PublishOutcome::Published(Receipt { number: 7 }),
|
|
PublishOutcome::Rejected("policy".into()),
|
|
] {
|
|
let encoded = outcome.clone().encode().unwrap();
|
|
assert_eq!(PublishOutcome::decode(encoded).unwrap(), outcome);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn derived_declarations_use_canonical_named_type_expressions() {
|
|
let mut declarations = Vec::new();
|
|
Request::collect_declarations(&mut declarations);
|
|
Color::collect_declarations(&mut declarations);
|
|
PublishOutcome::collect_declarations(&mut declarations);
|
|
|
|
let request = declarations
|
|
.iter()
|
|
.find(|declaration| declaration.name == "Request")
|
|
.unwrap();
|
|
assert_eq!(
|
|
request.documentation.as_ref().unwrap().summary,
|
|
"A request record."
|
|
);
|
|
let TypeExpr::Record { fields } = &request.definition else {
|
|
panic!("Request must be a record declaration")
|
|
};
|
|
assert_eq!(
|
|
fields[0].documentation.as_ref().unwrap().summary,
|
|
"Required title."
|
|
);
|
|
assert!(fields[0].required);
|
|
assert!(!fields[1].required);
|
|
assert_eq!(fields[1].r#type, TypeExpr::Integer);
|
|
assert_eq!(
|
|
fields[2].r#type,
|
|
TypeExpr::List {
|
|
items: Box::new(TypeExpr::Entry),
|
|
}
|
|
);
|
|
|
|
let color = declarations
|
|
.iter()
|
|
.find(|declaration| declaration.name == "Color")
|
|
.unwrap();
|
|
let TypeExpr::Enum { cases } = &color.definition else {
|
|
panic!("Color must be an enum declaration")
|
|
};
|
|
assert_eq!(
|
|
cases[0].documentation.as_ref().unwrap().summary,
|
|
"Warm red."
|
|
);
|
|
|
|
let outcome = declarations
|
|
.iter()
|
|
.find(|declaration| declaration.name == "PublishOutcome")
|
|
.unwrap();
|
|
let TypeExpr::Union { cases } = &outcome.definition else {
|
|
panic!("PublishOutcome must be a union declaration")
|
|
};
|
|
assert_eq!(cases[0].payload, None);
|
|
assert_eq!(
|
|
cases[1].payload,
|
|
Some(TypeExpr::Named {
|
|
name: "Receipt".into(),
|
|
})
|
|
);
|
|
assert!(
|
|
declarations
|
|
.iter()
|
|
.any(|declaration| declaration.name == "Receipt")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn codecs_reject_invalid_boundaries_and_json_bytes() {
|
|
assert!(i64::MAX.encode().is_err());
|
|
assert!(f64::NAN.encode().is_err());
|
|
assert!(Request::decode(Value::Record(BTreeMap::new())).is_err());
|
|
assert!(
|
|
Request::decode(Value::Record(BTreeMap::from([(
|
|
"unknown".into(),
|
|
Value::String("value".into()),
|
|
)])))
|
|
.is_err()
|
|
);
|
|
assert!(Color::decode(Value::String("Green".into())).is_err());
|
|
assert!(Entry::new("articles/1").is_err());
|
|
assert!(Entry::decode(Value::String("/articles/../1".into())).is_err());
|
|
assert!(JsonValue::decode(Value::Bytes(vec![1])).is_err());
|
|
assert!(
|
|
JsonValue::decode(Value::Record(BTreeMap::from([(
|
|
"nested".into(),
|
|
Value::List(vec![Value::Bytes(vec![1])]),
|
|
)])))
|
|
.is_err()
|
|
);
|
|
}
|