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, /// Related Worldspace entries. related: Vec, /// 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, 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() ); }