use wip_protocol::{ EnumDeclaration, EnumVariantDeclaration, FieldDeclaration, InterfaceDescriptor, InterfaceObservation, JsonMember, JsonValue, MAX_SAFE_INTEGER, NameNamespace, NamedValue, Object, ObjectId, OperationCall, OperationDeclaration, OperationTarget, ParameterDeclaration, PathSegment, RecordDeclaration, ReturnDeclaration, TypeDeclaration, TypeExpr, UnionDeclaration, UnionVariantDeclaration, ValidationErrorKind, Value, ValueKind, }; fn field(name: &str, ty: TypeExpr, optional: bool) -> FieldDeclaration { FieldDeclaration { name: name.into(), documentation: None, ty, optional, } } fn descriptor(name: &str) -> InterfaceDescriptor { InterfaceDescriptor { name: name.into(), documentation: None, declarations: vec![ TypeDeclaration::Enum(EnumDeclaration { name: "Color".into(), documentation: None, variants: vec![ EnumVariantDeclaration { name: "red".into(), documentation: None, }, EnumVariantDeclaration { name: "blue".into(), documentation: None, }, ], }), TypeDeclaration::Record(RecordDeclaration { name: "Payload".into(), documentation: None, fields: vec![ field("title", TypeExpr::String, false), field("color", TypeExpr::Named("Color".into()), true), field( "metadata", TypeExpr::Entry { key: Box::new(TypeExpr::String), value: Box::new(TypeExpr::Json), }, true, ), ], }), TypeDeclaration::Union(UnionDeclaration { name: "Selection".into(), documentation: None, variants: vec![ UnionVariantDeclaration { name: "by_name".into(), documentation: None, ty: TypeExpr::String, }, UnionVariantDeclaration { name: "by_id".into(), documentation: None, ty: TypeExpr::Integer, }, ], }), ], operations: vec![OperationDeclaration { name: "publish".into(), documentation: None, parameters: vec![ ParameterDeclaration { name: "payload".into(), documentation: None, ty: TypeExpr::Named("Payload".into()), optional: false, }, ParameterDeclaration { name: "selection".into(), documentation: None, ty: TypeExpr::Named("Selection".into()), optional: true, }, ], returns: Some(ReturnDeclaration { documentation: None, ty: TypeExpr::Boolean, }), }], } } fn named(name: &str, value: Value) -> NamedValue { NamedValue { name: name.into(), value, } } #[test] fn valid_descriptor_values_call_and_return_are_accepted() { let descriptor = descriptor("catalog"); descriptor.validate().unwrap(); let payload = Value::Record(vec![ named("title", Value::String("hello".into())), named("color", Value::Enum("red".into())), named( "metadata", Value::Entry { key: Box::new(Value::String("source".into())), value: Box::new(Value::Json(JsonValue::Object(vec![JsonMember { name: "trusted".into(), value: JsonValue::Boolean(true), }]))), }, ), ]); descriptor .validate_value(&TypeExpr::Named("Payload".into()), &payload) .unwrap(); let call = OperationCall { target: OperationTarget { object: ObjectId("object-1".into()), interface: "catalog".into(), operation: "publish".into(), }, arguments: vec![named("payload", payload)], }; descriptor.validate_call(&call).unwrap(); descriptor .validate_return("publish", Some(&Value::Boolean(true))) .unwrap(); } #[test] fn duplicate_declaration_names_are_structured_errors() { let mut descriptor = descriptor("catalog"); descriptor .declarations .push(TypeDeclaration::Record(RecordDeclaration { name: "Color".into(), documentation: None, fields: vec![], })); let error = descriptor.validate().unwrap_err(); assert_eq!( error.kind, ValidationErrorKind::DuplicateName { namespace: NameNamespace::TypeDeclaration, name: "Color".into(), } ); assert_eq!( error.path, vec![ PathSegment::Interface("catalog".into()), PathSegment::Declaration("Color".into()) ] ); } #[test] fn member_and_operation_names_are_unique_in_their_own_scopes() { let mut duplicate_field = descriptor("one"); let TypeDeclaration::Record(record) = &mut duplicate_field.declarations[1] else { unreachable!() }; record.fields.push(field("title", TypeExpr::String, false)); assert!(matches!( duplicate_field.validate().unwrap_err().kind, ValidationErrorKind::DuplicateName { namespace: NameNamespace::Field, .. } )); let mut duplicate_operation = descriptor("one"); duplicate_operation .operations .push(duplicate_operation.operations[0].clone()); assert!(matches!( duplicate_operation.validate().unwrap_err().kind, ValidationErrorKind::DuplicateName { namespace: NameNamespace::Operation, .. } )); let mut duplicate_parameter = descriptor("one"); let repeated_parameter = duplicate_parameter.operations[0].parameters[0].clone(); duplicate_parameter.operations[0] .parameters .push(repeated_parameter); assert!(matches!( duplicate_parameter.validate().unwrap_err().kind, ValidationErrorKind::DuplicateName { namespace: NameNamespace::Parameter, .. } )); // Operation names are local to an interface, so another descriptor may // independently declare the same operation name. descriptor("one").validate().unwrap(); descriptor("two").validate().unwrap(); } #[test] fn named_references_must_exist_and_must_not_cycle() { let mut unknown = descriptor("catalog"); let TypeDeclaration::Record(record) = &mut unknown.declarations[1] else { unreachable!() }; record.fields[0].ty = TypeExpr::Named("Elsewhere".into()); let error = unknown.validate().unwrap_err(); assert_eq!( error.kind, ValidationErrorKind::UnknownType { name: "Elsewhere".into() } ); assert_eq!( error.path, vec![ PathSegment::Interface("catalog".into()), PathSegment::Declaration("Payload".into()), PathSegment::Field("title".into()), ] ); let valid = descriptor("catalog"); let error = valid .validate_value(&TypeExpr::Named("Elsewhere".into()), &Value::Record(vec![])) .unwrap_err(); assert_eq!( error.kind, ValidationErrorKind::UnknownType { name: "Elsewhere".into() } ); assert_eq!(error.path, vec![PathSegment::Interface("catalog".into())]); let error = valid .validate_value( &TypeExpr::Entry { key: Box::new(TypeExpr::String), value: Box::new(TypeExpr::List(Box::new(TypeExpr::Named("Nested".into())))), }, &Value::Entry { key: Box::new(Value::String("key".into())), value: Box::new(Value::List(vec![])), }, ) .unwrap_err(); assert_eq!( error.kind, ValidationErrorKind::UnknownType { name: "Nested".into() } ); assert_eq!( error.path, vec![ PathSegment::Interface("catalog".into()), PathSegment::EntryValue, ] ); let cyclic = InterfaceDescriptor { name: "cyclic".into(), documentation: None, declarations: vec![ TypeDeclaration::Record(RecordDeclaration { name: "A".into(), documentation: None, fields: vec![field("b", TypeExpr::Named("B".into()), false)], }), TypeDeclaration::Union(UnionDeclaration { name: "B".into(), documentation: None, variants: vec![UnionVariantDeclaration { name: "a".into(), documentation: None, ty: TypeExpr::List(Box::new(TypeExpr::Named("A".into()))), }], }), ], operations: vec![], }; assert_eq!( cyclic.validate().unwrap_err().kind, ValidationErrorKind::RecursiveType { cycle: vec!["A".into(), "B".into(), "A".into()] } ); } #[test] fn records_reject_unknown_missing_duplicate_and_mistyped_fields() { let descriptor = descriptor("catalog"); let ty = TypeExpr::Named("Payload".into()); let error = descriptor .validate_value( &ty, &Value::Record(vec![ named("title", Value::String("ok".into())), named("extra", Value::Boolean(true)), ]), ) .unwrap_err(); assert_eq!( error.kind, ValidationErrorKind::UnknownField { name: "extra".into() } ); let error = descriptor .validate_value(&ty, &Value::Record(vec![])) .unwrap_err(); assert_eq!( error.kind, ValidationErrorKind::MissingField { name: "title".into() } ); let error = descriptor .validate_value( &ty, &Value::Record(vec![ named("title", Value::String("one".into())), named("title", Value::String("two".into())), ]), ) .unwrap_err(); assert!(matches!( error.kind, ValidationErrorKind::DuplicateName { namespace: NameNamespace::RecordValueField, .. } )); let error = descriptor .validate_value(&ty, &Value::Record(vec![named("title", Value::Integer(1))])) .unwrap_err(); assert_eq!( error.kind, ValidationErrorKind::TypeMismatch { expected: ValueKind::String, actual: ValueKind::Integer, } ); // Optional means omission: there is no general-purpose Value::Null. descriptor .validate_value( &ty, &Value::Record(vec![named("title", Value::String("ok".into()))]), ) .unwrap(); } #[test] fn integers_numbers_enums_unions_entries_and_json_are_validated() { let descriptor = descriptor("catalog"); descriptor .validate_value(&TypeExpr::Integer, &Value::Integer(MAX_SAFE_INTEGER)) .unwrap(); assert_eq!( descriptor .validate_value(&TypeExpr::Integer, &Value::Integer(MAX_SAFE_INTEGER + 1)) .unwrap_err() .kind, ValidationErrorKind::IntegerOutOfRange { value: MAX_SAFE_INTEGER + 1 } ); assert_eq!( descriptor .validate_value(&TypeExpr::Number, &Value::Number(f64::INFINITY)) .unwrap_err() .kind, ValidationErrorKind::NonFiniteNumber ); assert!(matches!( descriptor .validate_value( &TypeExpr::Named("Color".into()), &Value::Enum("green".into()) ) .unwrap_err() .kind, ValidationErrorKind::UnknownVariant { .. } )); descriptor .validate_value( &TypeExpr::Named("Selection".into()), &Value::Union { variant: "by_id".into(), value: Box::new(Value::Integer(7)), }, ) .unwrap(); assert!(matches!( descriptor .validate_value( &TypeExpr::Named("Selection".into()), &Value::Union { variant: "by_id".into(), value: Box::new(Value::String("wrong".into())), }, ) .unwrap_err() .kind, ValidationErrorKind::TypeMismatch { .. } )); let entry_ty = TypeExpr::Entry { key: Box::new(TypeExpr::String), value: Box::new(TypeExpr::Integer), }; let error = descriptor .validate_value( &entry_ty, &Value::Entry { key: Box::new(Value::String("key".into())), value: Box::new(Value::Boolean(false)), }, ) .unwrap_err(); assert_eq!(error.path.last(), Some(&PathSegment::EntryValue)); let json = Value::Json(JsonValue::Object(vec![ JsonMember { name: "same".into(), value: JsonValue::Null, }, JsonMember { name: "same".into(), value: JsonValue::Number(f64::NAN), }, ])); assert!(matches!( descriptor .validate_value(&TypeExpr::Json, &json) .unwrap_err() .kind, ValidationErrorKind::DuplicateName { namespace: NameNamespace::JsonMember, .. } )); } #[test] fn object_interfaces_are_an_ordered_set_and_observations_correspond() { let object = Object { id: ObjectId("object-1".into()), interfaces: vec!["catalog".into(), "search".into()], }; object.validate().unwrap(); assert_eq!(object.interfaces, vec!["catalog", "search"]); let duplicate = Object { id: ObjectId("object-2".into()), interfaces: vec!["catalog".into(), "catalog".into()], }; assert_eq!( duplicate.validate().unwrap_err().kind, ValidationErrorKind::DuplicateInterface { name: "catalog".into() } ); let observation = InterfaceObservation { object: object.clone(), descriptors: vec![descriptor("catalog"), descriptor("search")], }; observation.validate().unwrap(); let missing = InterfaceObservation { object, descriptors: vec![descriptor("catalog")], }; assert_eq!( missing.validate().unwrap_err().kind, ValidationErrorKind::MissingInterfaceDescriptor { name: "search".into() } ); } #[test] fn calls_reject_unknown_missing_duplicate_and_bad_arguments_and_returns() { let catalog = descriptor("catalog"); let target = OperationTarget { object: ObjectId("object-1".into()), interface: "catalog".into(), operation: "publish".into(), }; let missing = OperationCall { target: target.clone(), arguments: vec![], }; assert_eq!( catalog.validate_call(&missing).unwrap_err().kind, ValidationErrorKind::MissingParameter { name: "payload".into() } ); let unknown = OperationCall { target: target.clone(), arguments: vec![named("other", Value::Boolean(true))], }; assert_eq!( catalog.validate_call(&unknown).unwrap_err().kind, ValidationErrorKind::UnknownParameter { name: "other".into() } ); let duplicate = OperationCall { target, arguments: vec![ named("payload", Value::Record(vec![])), named("payload", Value::Record(vec![])), ], }; assert!(matches!( catalog.validate_call(&duplicate).unwrap_err().kind, ValidationErrorKind::DuplicateName { namespace: NameNamespace::Argument, .. } )); assert_eq!( catalog.validate_return("publish", None).unwrap_err().kind, ValidationErrorKind::MissingReturnValue ); let mut no_return = descriptor("catalog"); no_return.operations[0].returns = None; assert_eq!( no_return .validate_return("publish", Some(&Value::Boolean(true))) .unwrap_err() .kind, ValidationErrorKind::UnexpectedReturnValue ); }