1use crate::{PayloadEncoding, UAttributes, UMessageType, UPayloadFormat, UPriority, UUri};
15
16use crate::UAttributesError;
17
18pub trait UAttributesValidator: Send {
28 fn validate(&self, attributes: &UAttributes) -> Result<(), UAttributesError>;
35
36 fn validate_type(&self, attributes: &UAttributes) -> Result<(), UAttributesError> {
42 let expected_type = self.message_type();
43 if expected_type == attributes.type_() {
44 Ok(())
45 } else {
46 Err(UAttributesError::validation_error(format!(
47 "Wrong Message Type [expected {}, got {}]",
48 expected_type.to_cloudevent_type(),
49 attributes.type_().to_cloudevent_type()
50 )))
51 }
52 }
53
54 fn message_type(&self) -> UMessageType;
56
57 fn validate_source(&self, attributes: &UAttributes) -> Result<(), UAttributesError>;
63
64 fn validate_sink(&self, attributes: &UAttributes) -> Result<(), UAttributesError>;
66}
67
68pub fn validate_rpc_priority(attributes: &UAttributes) -> Result<(), UAttributesError> {
74 attributes
75 .priority()
76 .ok_or_else(|| {
77 UAttributesError::ValidationError("RPC message must have a priority".to_string())
78 })
79 .and_then(|prio| {
80 if prio < UPriority::CS4 {
81 Err(UAttributesError::ValidationError(
82 "RPC message must have a priority of at least CS4".to_string(),
83 ))
84 } else {
85 Ok(())
86 }
87 })
88}
89
90fn validate_payload_encoding(attributes: &UAttributes) -> Result<(), UAttributesError> {
91 let legacy_present = matches!(
92 attributes.payload_format(),
93 Some(format) if format != UPayloadFormat::Unspecified
94 );
95 let open_present = attributes.open_payload_encoding_parts() != (None, None, None);
96
97 if legacy_present && open_present {
98 return Err(UAttributesError::validation_error(
99 "payload encoding must use exactly one mechanism: payload_format or open payload-encoding fields",
100 ));
101 }
102
103 if open_present {
104 let (registry_id, literal_id, content_type) = attributes.open_payload_encoding_parts();
105 let encoding = PayloadEncoding::from_parts(
106 registry_id,
107 literal_id.map(ToOwned::to_owned),
108 content_type.map(ToOwned::to_owned),
109 )
110 .map_err(|error| UAttributesError::validation_error(error.to_string()))?;
111 if encoding.to_legacy_format().is_some() {
112 return Err(UAttributesError::validation_error(
113 "registered-range payload encodings must use payload_format, not open payload-encoding fields",
114 ));
115 }
116 }
117
118 Ok(())
119}
120
121#[derive(Debug)]
123pub enum UAttributesValidators {
124 Publish,
126 Notification,
128 Request,
130 Response,
132}
133
134impl UAttributesValidators {
135 #[must_use]
151 pub fn validator(&self) -> Box<dyn UAttributesValidator> {
152 match self {
153 UAttributesValidators::Publish => Box::new(PublishValidator),
154 UAttributesValidators::Notification => Box::new(NotificationValidator),
155 UAttributesValidators::Request => Box::new(RequestValidator),
156 UAttributesValidators::Response => Box::new(ResponseValidator),
157 }
158 }
159
160 #[must_use]
176 pub fn validator_for_attributes(attributes: &UAttributes) -> Box<dyn UAttributesValidator> {
177 Self::validator_for(attributes.type_())
178 }
179
180 #[must_use]
196 pub fn validator_for(message_type: UMessageType) -> Box<dyn UAttributesValidator> {
197 match message_type {
198 UMessageType::Publish => Box::new(PublishValidator),
199 UMessageType::Notification => Box::new(NotificationValidator),
200 UMessageType::Request => Box::new(RequestValidator),
201 UMessageType::Response => Box::new(ResponseValidator),
202 }
203 }
204 #[deprecated(since = "0.11.0", note = "renamed to `validator_for`")]
206 pub fn get_validator(message_type: crate::UMessageType) -> Box<dyn UAttributesValidator> {
207 Self::validator_for(message_type)
208 }
209
210 #[deprecated(since = "0.11.0", note = "renamed to `validator_for_attributes`")]
212 pub fn get_validator_for_attributes(attributes: &UAttributes) -> Box<dyn UAttributesValidator> {
213 Self::validator_for_attributes(attributes)
214 }
215}
216
217#[derive(Debug)]
219pub struct PublishValidator;
220
221impl UAttributesValidator for PublishValidator {
222 fn message_type(&self) -> UMessageType {
223 UMessageType::Publish
224 }
225
226 fn validate(&self, attributes: &UAttributes) -> Result<(), UAttributesError> {
237 let error_message = vec![
238 self.validate_type(attributes),
239 self.validate_source(attributes),
240 self.validate_sink(attributes),
241 validate_payload_encoding(attributes),
242 ]
243 .into_iter()
244 .filter_map(Result::err)
245 .map(|e| e.to_string())
246 .collect::<Vec<_>>()
247 .join("; ");
248
249 if error_message.is_empty() {
250 Ok(())
251 } else {
252 Err(UAttributesError::validation_error(error_message))
253 }
254 }
255
256 fn validate_source(&self, attributes: &UAttributes) -> Result<(), UAttributesError> {
265 attributes
267 .source()
268 .verify_event()
269 .map_err(|e| UAttributesError::validation_error(format!("Invalid source URI: {e}")))
270 }
271
272 fn validate_sink(&self, attributes: &UAttributes) -> Result<(), UAttributesError> {
278 if attributes.sink.as_ref().is_some() {
280 Err(UAttributesError::validation_error(
281 "Attributes for a publish message must not contain a sink URI",
282 ))
283 } else {
284 Ok(())
285 }
286 }
287}
288
289#[derive(Debug)]
291pub struct NotificationValidator;
292
293impl UAttributesValidator for NotificationValidator {
294 fn message_type(&self) -> UMessageType {
295 UMessageType::Notification
296 }
297
298 fn validate(&self, attributes: &UAttributes) -> Result<(), UAttributesError> {
309 let error_message = vec![
310 self.validate_type(attributes),
311 self.validate_source(attributes),
312 self.validate_sink(attributes),
313 validate_payload_encoding(attributes),
314 ]
315 .into_iter()
316 .filter_map(Result::err)
317 .map(|e| e.to_string())
318 .collect::<Vec<_>>()
319 .join("; ");
320
321 if error_message.is_empty() {
322 Ok(())
323 } else {
324 Err(UAttributesError::validation_error(error_message))
325 }
326 }
327
328 fn validate_source(&self, attributes: &UAttributes) -> Result<(), UAttributesError> {
338 let source = attributes.source();
340 if source.is_rpc_response() {
341 Err(UAttributesError::validation_error(
342 "Origin must not be an RPC response URI",
343 ))
344 } else {
345 source
346 .verify_no_wildcards()
347 .map_err(|e| UAttributesError::validation_error(format!("Invalid source URI: {e}")))
348 }
349 }
350
351 fn validate_sink(&self, attributes: &UAttributes) -> Result<(), UAttributesError> {
361 if let Some(sink) = attributes.sink.as_ref() {
363 if !sink.is_notification_destination() {
364 Err(UAttributesError::validation_error(
365 "Destination's resource ID must be 0",
366 ))
367 } else {
368 sink.verify_no_wildcards().map_err(|e| {
369 UAttributesError::validation_error(format!("Invalid sink URI: {e}"))
370 })
371 }
372 } else {
373 Err(UAttributesError::validation_error(
374 "Attributes for a notification message must contain a sink URI",
375 ))
376 }
377 }
378}
379
380#[derive(Debug)]
382pub struct RequestValidator;
383
384impl RequestValidator {
385 pub fn validate_ttl(&self, attributes: &UAttributes) -> Result<(), UAttributesError> {
391 match attributes.ttl {
393 Some(ttl) if ttl > 0 => Ok(()),
394 Some(invalid_ttl) => Err(UAttributesError::validation_error(format!(
395 "RPC request message's TTL must be a positive integer [{invalid_ttl}]"
396 ))),
397 None => Err(UAttributesError::validation_error(
398 "RPC request message must contain a TTL",
399 )),
400 }
401 }
402}
403
404impl UAttributesValidator for RequestValidator {
405 fn message_type(&self) -> UMessageType {
406 UMessageType::Request
407 }
408
409 fn validate(&self, attributes: &UAttributes) -> Result<(), UAttributesError> {
422 let error_message = vec![
423 self.validate_type(attributes),
424 self.validate_ttl(attributes),
425 self.validate_source(attributes),
426 self.validate_sink(attributes),
427 validate_rpc_priority(attributes),
428 validate_payload_encoding(attributes),
429 ]
430 .into_iter()
431 .filter_map(Result::err)
432 .map(|e| e.to_string())
433 .collect::<Vec<_>>()
434 .join("; ");
435
436 if error_message.is_empty() {
437 Ok(())
438 } else {
439 Err(UAttributesError::validation_error(error_message))
440 }
441 }
442
443 fn validate_source(&self, attributes: &UAttributes) -> Result<(), UAttributesError> {
450 attributes
452 .source()
453 .verify_rpc_response()
454 .map_err(|e| UAttributesError::validation_error(format!("Invalid source URI: {e}")))
455 }
456
457 fn validate_sink(&self, attributes: &UAttributes) -> Result<(), UAttributesError> {
464 if let Some(sink) = attributes.sink.as_ref() {
466 UUri::verify_rpc_method(sink)
467 .map_err(|e| UAttributesError::validation_error(format!("Invalid sink URI: {e}")))
468 } else {
469 Err(UAttributesError::validation_error("Attributes for a request message must contain a method-to-invoke in the sink property"))
470 }
471 }
472}
473
474#[derive(Debug)]
476pub struct ResponseValidator;
477
478impl ResponseValidator {
479 pub fn validate_reqid(&self, attributes: &UAttributes) -> Result<(), UAttributesError> {
485 if attributes.reqid.as_ref().is_none() {
486 Err(UAttributesError::validation_error("Request ID is missing"))
487 } else {
488 Ok(())
489 }
490 }
491}
492
493impl UAttributesValidator for ResponseValidator {
494 fn message_type(&self) -> UMessageType {
495 UMessageType::Response
496 }
497
498 fn validate(&self, attributes: &UAttributes) -> Result<(), UAttributesError> {
511 let error_message = vec![
512 self.validate_type(attributes),
513 self.validate_source(attributes),
514 self.validate_sink(attributes),
515 self.validate_reqid(attributes),
516 validate_rpc_priority(attributes),
517 validate_payload_encoding(attributes),
518 ]
519 .into_iter()
520 .filter_map(Result::err)
521 .map(|e| e.to_string())
522 .collect::<Vec<_>>()
523 .join("; ");
524
525 if error_message.is_empty() {
526 Ok(())
527 } else {
528 Err(UAttributesError::validation_error(error_message))
529 }
530 }
531
532 fn validate_source(&self, attributes: &UAttributes) -> Result<(), UAttributesError> {
540 attributes
542 .source()
543 .verify_rpc_method()
544 .map_err(|e| UAttributesError::validation_error(format!("Invalid source URI: {e}")))
545 }
546
547 fn validate_sink(&self, attributes: &UAttributes) -> Result<(), UAttributesError> {
555 if let Some(sink) = &attributes.sink.as_ref() {
557 UUri::verify_rpc_response(sink)
558 .map_err(|e| UAttributesError::validation_error(format!("Invalid sink URI: {e}")))
559 } else {
560 Err(UAttributesError::validation_error("Missing Sink"))
561 }
562 }
563}
564
565#[cfg(test)]
566mod tests {
567 use test_case::test_case;
568
569 use super::*;
570 use crate::{uattributes::TokenString, UCode, UPriority, UUri, UUID};
571
572 #[test_case(UMessageType::Publish, UMessageType::Publish; "succeeds for Publish message")]
573 #[test_case(UMessageType::Notification, UMessageType::Notification; "succeeds for Notification message")]
574 #[test_case(UMessageType::Request, UMessageType::Request; "succeeds for Request message")]
575 #[test_case(UMessageType::Response, UMessageType::Response; "succeeds for Response message")]
576 fn test_get_validator_returns_matching_validator(
577 message_type: UMessageType,
578 expected_validator_type: UMessageType,
579 ) {
580 let validator: Box<dyn UAttributesValidator> =
581 UAttributesValidators::validator_for(message_type);
582 assert_eq!(validator.message_type(), expected_validator_type);
583 }
584
585 #[test_case(publish_topic(), None, None, true; "succeeds for topic only")]
586 #[test_case(publish_topic(), Some(destination()), None, false; "fails for message containing destination")]
588 #[test_case(publish_topic(), None, Some(100), true; "succeeds for valid attributes")]
589 #[test_case(method_to_invoke(), None, None, false; "fails for invalid topic")]
591 fn test_validate_attributes_for_publish_message(
592 source: UUri,
593 sink: Option<UUri>,
594 ttl: Option<u32>,
595 expected_result: bool,
596 ) {
597 let attributes = UAttributes {
598 commstatus: None,
599 id: UUID::build(),
600 payload_format: None,
601 payload_encoding_registry_id: None,
602 payload_encoding: None,
603 payload_content_type: None,
604 permission_level: None,
605 priority: UPriority::CS1.into(),
606 reqid: None,
607 sink,
608 source,
609 token: None,
610 traceparent: None,
611 ttl,
612 type_: UMessageType::Publish,
613 };
614 let validator = UAttributesValidators::Publish.validator();
615 let status = validator.validate(&attributes);
616 assert!(status.is_ok() == expected_result);
617 if status.is_ok() {
618 assert!(UAttributesValidators::Notification
619 .validator()
620 .validate(&attributes)
621 .is_err());
622 assert!(UAttributesValidators::Request
623 .validator()
624 .validate(&attributes)
625 .is_err());
626 assert!(UAttributesValidators::Response
627 .validator()
628 .validate(&attributes)
629 .is_err());
630 }
631 }
632
633 #[test_case(origin(), None, None, false; "fails for missing destination")]
635 #[test_case(origin(), Some(destination()), None, true; "succeeds for both origin and destination")]
636 #[test_case(origin(), Some(destination()), Some(100), true; "succeeds for valid attributes")]
637 #[test_case(reply_to_address(), Some(destination()), None, false; "fails for invalid origin")]
639 #[test_case(origin(), Some(method_to_invoke()), None, false; "fails for invalid destination")]
641 fn test_validate_attributes_for_notification_message(
642 source: UUri,
643 sink: Option<UUri>,
644 ttl: Option<u32>,
645 expected_result: bool,
646 ) {
647 let attributes = UAttributes {
648 commstatus: None,
649 id: UUID::build(),
650 payload_format: None,
651 payload_encoding_registry_id: None,
652 payload_encoding: None,
653 payload_content_type: None,
654 permission_level: None,
655 priority: UPriority::CS1.into(),
656 reqid: None,
657 sink,
658 source,
659 token: None,
660 traceparent: None,
661 ttl,
662 type_: UMessageType::Notification,
663 };
664 let validator = UAttributesValidators::Notification.validator();
665 let status = validator.validate(&attributes);
666 assert!(status.is_ok() == expected_result);
667 if status.is_ok() {
668 assert!(UAttributesValidators::Publish
669 .validator()
670 .validate(&attributes)
671 .is_err());
672 assert!(UAttributesValidators::Request
673 .validator()
674 .validate(&attributes)
675 .is_err());
676 assert!(UAttributesValidators::Response
677 .validator()
678 .validate(&attributes)
679 .is_err());
680 }
681 }
682
683 #[test_case(Some(method_to_invoke()), reply_to_address(), None, Some(2000), Some(UPriority::CS4), None, true; "succeeds for mandatory attributes")]
684 #[test_case(Some(method_to_invoke()), reply_to_address(), Some(1), Some(2000), Some(UPriority::CS4), Some("token"), true; "succeeds for valid attributes")]
685 #[test_case(Some(method_to_invoke()), origin(), None, Some(2000), Some(UPriority::CS4), None, false; "fails for invalid reply-to-address")]
687 #[test_case(None, reply_to_address(), None, Some(2000), Some(UPriority::CS4), None, false; "fails for missing method-to-invoke")]
689 #[test_case(Some(destination()), reply_to_address(), None, Some(2000), Some(UPriority::CS4), None, false; "fails for invalid method-to-invoke")]
691 #[test_case(Some(method_to_invoke()), reply_to_address(), Some(1), Some(2000), None, None, false; "fails for missing priority")]
692 #[test_case(Some(method_to_invoke()), reply_to_address(), Some(1), Some(2000), Some(UPriority::CS3), None, false; "fails for invalid priority")]
693 #[test_case(Some(method_to_invoke()), reply_to_address(), Some(1), None, Some(UPriority::CS4), None, false; "fails for missing ttl")]
695 #[test_case(Some(method_to_invoke()), reply_to_address(), Some(1), Some(0), Some(UPriority::CS4), None, false; "fails for ttl = 0")]
697 #[test_case(Some(method_to_invoke()), reply_to_address(), Some(1), Some(2000), Some(UPriority::CS4), None, true; "succeeds for valid permission level")]
698 #[allow(clippy::too_many_arguments)]
699 fn test_validate_attributes_for_rpc_request_message(
700 method_to_invoke: Option<UUri>,
701 reply_to_address: UUri,
702 perm_level: Option<u32>,
703 ttl: Option<u32>,
704 priority: Option<UPriority>,
705 token: Option<&str>,
706 expected_result: bool,
707 ) {
708 let attributes = UAttributes {
709 commstatus: None,
710 id: UUID::build(),
711 payload_format: None,
712 payload_encoding_registry_id: None,
713 payload_encoding: None,
714 payload_content_type: None,
715 permission_level: perm_level,
716 priority,
717 reqid: None,
718 sink: method_to_invoke,
719 source: reply_to_address,
720 token: token.map(TokenString::from),
721 traceparent: None,
722 ttl,
723 type_: UMessageType::Request,
724 };
725 let status = UAttributesValidators::Request
726 .validator()
727 .validate(&attributes);
728 assert!(status.is_ok() == expected_result);
729 if status.is_ok() {
730 assert!(UAttributesValidators::Publish
731 .validator()
732 .validate(&attributes)
733 .is_err());
734 assert!(UAttributesValidators::Notification
735 .validator()
736 .validate(&attributes)
737 .is_err());
738 assert!(UAttributesValidators::Response
739 .validator()
740 .validate(&attributes)
741 .is_err());
742 }
743 }
744
745 #[test_case(Some(reply_to_address()), method_to_invoke(), Some(UUID::build()), None, None, Some(UPriority::CS4), true; "succeeds for mandatory attributes")]
746 #[test_case(Some(reply_to_address()), method_to_invoke(), Some(UUID::build()), Some(UCode::Cancelled), Some(100), Some(UPriority::CS4), true; "succeeds for valid attributes")]
747 #[test_case(None, method_to_invoke(), Some(UUID::build()), None, None, Some(UPriority::CS4), false; "fails for missing reply-to-address")]
749 #[test_case(Some(origin()), method_to_invoke(), Some(UUID::build()), None, None, Some(UPriority::CS4), false; "fails for invalid reply-to-address")]
751 #[test_case(Some(reply_to_address()), origin(), Some(UUID::build()), None, None, Some(UPriority::CS4), false; "fails for invalid invoked-method")]
753 #[test_case(Some(reply_to_address()), method_to_invoke(), Some(UUID::build()), Some(UCode::Cancelled), None, Some(UPriority::CS4), true; "succeeds for valid commstatus")]
754 #[test_case(Some(reply_to_address()), method_to_invoke(), Some(UUID::build()), None, Some(100), Some(UPriority::CS4), true; "succeeds for ttl > 0)")]
755 #[test_case(Some(reply_to_address()), method_to_invoke(), Some(UUID::build()), None, Some(0), Some(UPriority::CS4), true; "succeeds for ttl = 0")]
756 #[test_case(Some(reply_to_address()), method_to_invoke(), Some(UUID::build()), Some(UCode::Cancelled), Some(100), None, false; "fails for missing priority")]
757 #[test_case(Some(reply_to_address()), method_to_invoke(), Some(UUID::build()), Some(UCode::Cancelled), Some(100), Some(UPriority::CS3), false; "fails for invalid priority")]
758 #[test_case(Some(reply_to_address()), method_to_invoke(), None, None, None, Some(UPriority::CS4), false; "fails for missing request id")]
759 #[allow(clippy::too_many_arguments)]
760 fn test_validate_attributes_for_rpc_response_message(
761 reply_to_address: Option<UUri>,
762 invoked_method: UUri,
763 reqid: Option<UUID>,
764 commstatus: Option<UCode>,
765 ttl: Option<u32>,
766 priority: Option<UPriority>,
767 expected_result: bool,
768 ) {
769 let attributes = UAttributes {
770 commstatus,
771 id: UUID::build(),
772 payload_format: None,
773 payload_encoding_registry_id: None,
774 payload_encoding: None,
775 payload_content_type: None,
776 permission_level: None,
777 priority,
778 reqid,
779 sink: reply_to_address,
780 source: invoked_method,
781 token: None,
782 traceparent: None,
783 ttl,
784 type_: UMessageType::Response,
785 };
786 let status = UAttributesValidators::Response
787 .validator()
788 .validate(&attributes);
789 assert!(status.is_ok() == expected_result);
790 if status.is_ok() {
791 assert!(UAttributesValidators::Publish
792 .validator()
793 .validate(&attributes)
794 .is_err());
795 assert!(UAttributesValidators::Notification
796 .validator()
797 .validate(&attributes)
798 .is_err());
799 assert!(UAttributesValidators::Request
800 .validator()
801 .validate(&attributes)
802 .is_err());
803 }
804 }
805
806 fn publish_topic() -> UUri {
807 UUri::try_from_parts("vcu.somevin", 0x0000_5410, 0x01, 0xa010)
808 .expect("failed to create publish topic URI")
809 }
810
811 fn origin() -> UUri {
812 UUri::try_from_parts("vcu.somevin", 0x0000_3c00, 0x02, 0x9a00)
813 .expect("failed to create origin URI")
814 }
815
816 fn destination() -> UUri {
817 UUri::try_from_parts("vcu.somevin", 0x0000_3d07, 0x01, 0x0000)
818 .expect("failed to create destination URI")
819 }
820
821 fn reply_to_address() -> UUri {
822 UUri::try_from_parts("vcu.somevin", 0x0000_010b, 0x01, 0x0000)
823 .expect("failed to create reply-to-address URI")
824 }
825
826 fn method_to_invoke() -> UUri {
827 UUri::try_from_parts("vcu.somevin", 0x0000_03ae, 0x01, 0x00e2)
828 .expect("failed to create method-to-invoke URI")
829 }
830}