1use std::borrow::Cow;
40use std::time::Duration;
41
42use bytes::Bytes;
43
44use crate::{
45 UAttributes, UCode, UMessage, UMessageError, UMessageType, UPayloadFormat, UPriority, UUri,
46 UUID,
47};
48
49#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
60pub enum FrameMessageKind {
61 Publish,
63 Notification,
65 Request,
67 Response,
69}
70
71impl FrameMessageKind {
72 #[must_use]
80 pub fn wire_code(self) -> u8 {
81 match self {
82 Self::Publish => 1,
83 Self::Request => 2,
84 Self::Response => 3,
85 Self::Notification => 4,
86 }
87 }
88
89 #[must_use]
91 pub fn from_wire_code(code: u8) -> Option<Self> {
92 match code {
93 1 => Some(Self::Publish),
94 2 => Some(Self::Request),
95 3 => Some(Self::Response),
96 4 => Some(Self::Notification),
97 _ => None,
98 }
99 }
100
101 #[must_use]
103 pub fn from_legacy_type(value: UMessageType) -> Self {
104 match value {
105 UMessageType::Publish => Self::Publish,
106 UMessageType::Notification => Self::Notification,
107 UMessageType::Request => Self::Request,
108 UMessageType::Response => Self::Response,
109 }
110 }
111
112 #[must_use]
114 pub fn to_legacy_type(self) -> UMessageType {
115 match self {
116 Self::Publish => UMessageType::Publish,
117 Self::Notification => UMessageType::Notification,
118 Self::Request => UMessageType::Request,
119 Self::Response => UMessageType::Response,
120 }
121 }
122}
123
124#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
130pub enum FramePriority {
131 CS0,
133 CS1,
135 CS2,
137 CS3,
139 CS4,
141 CS5,
143 CS6,
145}
146
147impl FramePriority {
148 #[must_use]
154 pub fn wire_code(self) -> u8 {
155 match self {
156 Self::CS0 => 1,
157 Self::CS1 => 2,
158 Self::CS2 => 3,
159 Self::CS3 => 4,
160 Self::CS4 => 5,
161 Self::CS5 => 6,
162 Self::CS6 => 7,
163 }
164 }
165
166 #[must_use]
168 pub fn from_wire_code(code: u8) -> Option<Self> {
169 match code {
170 1 => Some(Self::CS0),
171 2 => Some(Self::CS1),
172 3 => Some(Self::CS2),
173 4 => Some(Self::CS3),
174 5 => Some(Self::CS4),
175 6 => Some(Self::CS5),
176 7 => Some(Self::CS6),
177 _ => None,
178 }
179 }
180
181 #[must_use]
183 pub fn from_legacy_priority(value: UPriority) -> Self {
184 match value {
185 UPriority::CS0 => Self::CS0,
186 UPriority::CS1 => Self::CS1,
187 UPriority::CS2 => Self::CS2,
188 UPriority::CS3 => Self::CS3,
189 UPriority::CS4 => Self::CS4,
190 UPriority::CS5 => Self::CS5,
191 UPriority::CS6 => Self::CS6,
192 }
193 }
194
195 #[must_use]
197 pub fn to_legacy_priority(self) -> UPriority {
198 match self {
199 Self::CS0 => UPriority::CS0,
200 Self::CS1 => UPriority::CS1,
201 Self::CS2 => UPriority::CS2,
202 Self::CS3 => UPriority::CS3,
203 Self::CS4 => UPriority::CS4,
204 Self::CS5 => UPriority::CS5,
205 Self::CS6 => UPriority::CS6,
206 }
207 }
208}
209
210#[derive(Clone, Debug, Eq, Hash, PartialEq)]
237pub struct PayloadEncoding {
238 registry_id: Option<u32>,
239 literal_id: Option<Cow<'static, str>>,
240 content_type: Option<Cow<'static, str>>,
241}
242
243macro_rules! well_known_encoding {
244 ($(#[$attr:meta])* $name:ident, $registry_id:literal, $literal:literal, $content_type:literal) => {
245 $(#[$attr])*
246 pub const $name: PayloadEncoding = PayloadEncoding {
247 registry_id: Some($registry_id),
248 literal_id: Some(Cow::Borrowed($literal)),
249 content_type: Some(Cow::Borrowed($content_type)),
250 };
251 };
252}
253
254impl PayloadEncoding {
255 well_known_encoding!(
256 PROTOBUF_WRAPPED_IN_ANY,
258 1,
259 "up.protobuf-wrapped-in-any",
260 "application/x-protobuf"
261 );
262 well_known_encoding!(
263 PROTOBUF,
265 2,
266 "up.protobuf",
267 "application/protobuf"
268 );
269 well_known_encoding!(
270 JSON,
272 3,
273 "up.json",
274 "application/json"
275 );
276 well_known_encoding!(
277 SOMEIP,
279 4,
280 "up.someip",
281 "application/x-someip"
282 );
283 well_known_encoding!(
284 SOMEIP_TLV,
286 5,
287 "up.someip-tlv",
288 "application/x-someip_tlv"
289 );
290 well_known_encoding!(
291 RAW,
293 6,
294 "up.raw",
295 "application/octet-stream"
296 );
297 well_known_encoding!(
298 TEXT,
300 7,
301 "up.text",
302 "text/plain"
303 );
304 well_known_encoding!(
305 SHM,
307 8,
308 "up.shm",
309 "application/x-shm"
310 );
311
312 pub const LEGACY_COMPATIBLE: [PayloadEncoding; 8] = [
315 Self::PROTOBUF_WRAPPED_IN_ANY,
316 Self::PROTOBUF,
317 Self::JSON,
318 Self::SOMEIP,
319 Self::SOMEIP_TLV,
320 Self::RAW,
321 Self::TEXT,
322 Self::SHM,
323 ];
324
325 fn legacy_by_registry_id(registry_id: u32) -> Option<Self> {
326 match registry_id {
327 1 => Some(Self::PROTOBUF_WRAPPED_IN_ANY),
328 2 => Some(Self::PROTOBUF),
329 3 => Some(Self::JSON),
330 4 => Some(Self::SOMEIP),
331 5 => Some(Self::SOMEIP_TLV),
332 6 => Some(Self::RAW),
333 7 => Some(Self::TEXT),
334 8 => Some(Self::SHM),
335 _ => None,
336 }
337 }
338
339 fn legacy_by_literal_id(literal_id: &str) -> Option<Self> {
340 Self::LEGACY_COMPATIBLE
341 .into_iter()
342 .find(|encoding| encoding.literal_id() == Some(literal_id))
343 }
344
345 fn canonicalize(self) -> Result<Self, UFrameMetadataError> {
346 if let Some(registry_id) = self.registry_id {
347 if let Some(legacy) = Self::legacy_by_registry_id(registry_id) {
348 if self
349 .literal_id()
350 .is_some_and(|literal_id| Some(literal_id) != legacy.literal_id())
351 || self
352 .content_type()
353 .is_some_and(|content_type| Some(content_type) != legacy.content_type())
354 {
355 return Err(UFrameMetadataError::InvalidMetadata(format!(
356 "payload encoding registry id {registry_id} conflicts with `{}`",
357 self.describe()
358 )));
359 }
360 return Ok(legacy);
361 }
362 }
363
364 if let Some(literal_id) = self.literal_id() {
365 if let Some(legacy) = Self::legacy_by_literal_id(literal_id) {
366 if self
367 .content_type()
368 .is_some_and(|content_type| Some(content_type) != legacy.content_type())
369 {
370 return Err(UFrameMetadataError::InvalidMetadata(format!(
371 "payload encoding literal `{literal_id}` conflicts with content type `{}`",
372 self.content_type().unwrap_or("<absent>")
373 )));
374 }
375 return Ok(legacy);
376 }
377 }
378
379 Ok(self)
380 }
381
382 pub fn custom(
390 id: impl Into<String>,
391 content_type: impl Into<String>,
392 ) -> Result<Self, UFrameMetadataError> {
393 let encoding = Self {
394 registry_id: None,
395 literal_id: Some(Cow::Owned(id.into())),
396 content_type: Some(Cow::Owned(content_type.into())),
397 };
398 encoding.validate()?;
399 if let Some(literal_id) = encoding.literal_id() {
400 if let Some(legacy) = Self::legacy_by_literal_id(literal_id) {
401 return Err(UFrameMetadataError::RegisteredPayloadEncodingAlias {
402 literal_id: literal_id.to_string(),
403 registered: legacy.describe(),
404 });
405 }
406 }
407 encoding.canonicalize()
408 }
409
410 pub fn from_parts(
418 registry_id: Option<u32>,
419 literal_id: Option<String>,
420 content_type: Option<String>,
421 ) -> Result<Self, UFrameMetadataError> {
422 let encoding = Self {
423 registry_id,
424 literal_id: literal_id.map(Cow::Owned),
425 content_type: content_type.map(Cow::Owned),
426 };
427 encoding.validate()?;
428 encoding.canonicalize()
429 }
430
431 #[must_use]
433 pub fn registry_id(&self) -> Option<u32> {
434 self.registry_id
435 }
436
437 #[must_use]
439 pub fn literal_id(&self) -> Option<&str> {
440 self.literal_id.as_deref()
441 }
442
443 #[must_use]
445 pub fn content_type(&self) -> Option<&str> {
446 self.content_type.as_deref()
447 }
448
449 #[must_use]
452 pub fn custom_identity(&self) -> Option<(&str, &str)> {
453 match (self.literal_id(), self.content_type()) {
454 (Some(id), Some(content_type)) => Some((id, content_type)),
455 _ => None,
456 }
457 }
458
459 #[must_use]
461 pub fn is_compatible_with(&self, expected: &Self) -> bool {
462 self == expected
463 }
464
465 pub fn try_from_legacy_format(format: UPayloadFormat) -> Result<Self, UFrameMetadataError> {
476 match format {
477 UPayloadFormat::Unspecified => Err(UFrameMetadataError::UnspecifiedPayloadFormat),
478 UPayloadFormat::ProtobufWrappedInAny => Ok(Self::PROTOBUF_WRAPPED_IN_ANY),
479 UPayloadFormat::Protobuf => Ok(Self::PROTOBUF),
480 UPayloadFormat::Json => Ok(Self::JSON),
481 UPayloadFormat::Someip => Ok(Self::SOMEIP),
482 UPayloadFormat::SomeipTlv => Ok(Self::SOMEIP_TLV),
483 UPayloadFormat::Raw => Ok(Self::RAW),
484 UPayloadFormat::Text => Ok(Self::TEXT),
485 UPayloadFormat::Shm => Ok(Self::SHM),
486 }
487 }
488
489 #[must_use]
496 pub fn to_legacy_format(&self) -> Option<UPayloadFormat> {
497 match self.registry_id {
498 Some(1) => Some(UPayloadFormat::ProtobufWrappedInAny),
499 Some(2) => Some(UPayloadFormat::Protobuf),
500 Some(3) => Some(UPayloadFormat::Json),
501 Some(4) => Some(UPayloadFormat::Someip),
502 Some(5) => Some(UPayloadFormat::SomeipTlv),
503 Some(6) => Some(UPayloadFormat::Raw),
504 Some(7) => Some(UPayloadFormat::Text),
505 Some(8) => Some(UPayloadFormat::Shm),
506 _ => None,
507 }
508 }
509
510 #[must_use]
512 pub fn describe(&self) -> String {
513 if let Some(literal) = self.literal_id() {
514 return literal.to_string();
515 }
516 if let Some(id) = self.registry_id {
517 return format!("registry:{id}");
518 }
519 self.content_type().unwrap_or("<empty>").to_string()
520 }
521
522 pub(crate) fn validate(&self) -> Result<(), UFrameMetadataError> {
523 if self.registry_id.is_none() && self.literal_id.is_none() && self.content_type.is_none() {
524 return Err(UFrameMetadataError::EmptyPayloadEncoding);
525 }
526 if let Some(literal_id) = self.literal_id() {
527 if literal_id.is_empty() {
528 return Err(UFrameMetadataError::EmptyCustomEncodingId);
529 }
530 }
531 if let Some(content_type) = self.content_type() {
532 if content_type.is_empty() {
533 return Err(UFrameMetadataError::EmptyCustomEncodingContentType);
534 }
535 mediatype::MediaType::parse(content_type).map_err(|error| {
536 UFrameMetadataError::InvalidCustomEncodingContentType(error.to_string())
537 })?;
538 }
539 Ok(())
540 }
541}
542
543#[derive(Clone, Debug, Eq, PartialEq)]
549#[non_exhaustive]
550pub enum UFrameMetadataError {
551 EmptyPayloadEncoding,
553 EmptyCustomEncodingId,
555 EmptyCustomEncodingContentType,
557 InvalidCustomEncodingContentType(String),
559 UnspecifiedPayloadFormat,
561 PayloadWithoutEncoding,
563 EncodingWithoutPayload,
565 EncodingNotRepresentable {
567 encoding: String,
569 },
570 RegisteredPayloadEncodingAlias {
572 literal_id: String,
574 registered: String,
576 },
577 FieldNotRepresentable {
579 field: &'static str,
581 reason: String,
583 },
584 InvalidMetadata(String),
586 MessageBuildError(String),
588}
589
590impl std::fmt::Display for UFrameMetadataError {
591 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
592 match self {
593 Self::EmptyPayloadEncoding => {
594 f.write_str("payload encoding must have at least one identity component")
595 }
596 Self::EmptyCustomEncodingId => f.write_str("custom payload encoding id is empty"),
597 Self::EmptyCustomEncodingContentType => {
598 f.write_str("custom payload encoding content_type is empty")
599 }
600 Self::InvalidCustomEncodingContentType(error) => f.write_fmt(format_args!(
601 "custom payload encoding content_type is invalid: {error}"
602 )),
603 Self::UnspecifiedPayloadFormat => {
604 f.write_str("UPayloadFormat::Unspecified is not a concrete payload encoding")
605 }
606 Self::PayloadWithoutEncoding => f.write_str("payload bytes require a payload encoding"),
607 Self::EncodingWithoutPayload => f.write_str("payload encoding requires payload bytes"),
608 Self::EncodingNotRepresentable { encoding } => f.write_fmt(format_args!(
609 "payload encoding `{encoding}` cannot be represented by legacy UPayloadFormat"
610 )),
611 Self::RegisteredPayloadEncodingAlias {
612 literal_id,
613 registered,
614 } => f.write_fmt(format_args!(
615 "payload encoding literal `{literal_id}` aliases registered encoding `{registered}`; use the registered spelling"
616 )),
617 Self::FieldNotRepresentable { field, reason } => f.write_fmt(format_args!(
618 "frame metadata field `{field}` cannot be represented by legacy types: {reason}"
619 )),
620 Self::InvalidMetadata(error) => {
621 f.write_fmt(format_args!("invalid frame metadata: {error}"))
622 }
623 Self::MessageBuildError(error) => f.write_fmt(format_args!(
624 "failed to build UMessage from frame metadata: {error}"
625 )),
626 }
627 }
628}
629
630impl std::error::Error for UFrameMetadataError {}
631
632impl From<UMessageError> for UFrameMetadataError {
633 fn from(value: UMessageError) -> Self {
634 Self::MessageBuildError(value.to_string())
635 }
636}
637
638#[derive(Clone, Debug, PartialEq)]
655pub struct UFrameMetadata {
656 kind: FrameMessageKind,
657 id: UUID,
658 source: UUri,
659 sink: Option<UUri>,
660 reqid: Option<UUID>,
661 priority: Option<FramePriority>,
662 ttl: Option<Duration>,
663 comm_status: Option<UCode>,
664 permission_level: Option<u32>,
665 token: Option<String>,
666 traceparent: Option<String>,
667 payload_encoding: Option<PayloadEncoding>,
668}
669
670impl UFrameMetadata {
671 #[must_use]
673 pub fn publish(topic: UUri) -> UFrameMetadataBuilder {
674 UFrameMetadataBuilder::new(FrameMessageKind::Publish, topic, None)
675 }
676
677 #[must_use]
680 pub fn notification(origin: UUri, destination: UUri) -> UFrameMetadataBuilder {
681 UFrameMetadataBuilder::new(FrameMessageKind::Notification, origin, Some(destination))
682 }
683
684 #[must_use]
689 pub fn request(method: UUri, reply_to: UUri, ttl: Duration) -> UFrameMetadataBuilder {
690 let mut builder =
691 UFrameMetadataBuilder::new(FrameMessageKind::Request, reply_to, Some(method));
692 builder.priority = Some(FramePriority::CS4);
693 builder.ttl = Some(ttl);
694 builder
695 }
696
697 #[must_use]
702 pub fn response(
703 invoked_method: UUri,
704 reply_to: UUri,
705 request_id: UUID,
706 ) -> UFrameMetadataBuilder {
707 let mut builder =
708 UFrameMetadataBuilder::new(FrameMessageKind::Response, invoked_method, Some(reply_to));
709 builder.priority = Some(FramePriority::CS4);
710 builder.reqid = Some(request_id);
711 builder
712 }
713
714 #[allow(clippy::too_many_arguments)]
719 pub(crate) fn from_decoded_parts(
720 kind: FrameMessageKind,
721 id: UUID,
722 source: UUri,
723 sink: Option<UUri>,
724 reqid: Option<UUID>,
725 priority: Option<FramePriority>,
726 ttl: Option<Duration>,
727 comm_status: Option<UCode>,
728 permission_level: Option<u32>,
729 token: Option<String>,
730 traceparent: Option<String>,
731 payload_encoding: Option<PayloadEncoding>,
732 ) -> Self {
733 Self {
734 kind,
735 id,
736 source,
737 sink,
738 reqid,
739 priority,
740 ttl,
741 comm_status,
742 permission_level,
743 token,
744 traceparent,
745 payload_encoding,
746 }
747 }
748
749 #[must_use]
751 pub fn kind(&self) -> FrameMessageKind {
752 self.kind
753 }
754
755 #[must_use]
757 pub fn id(&self) -> &UUID {
758 &self.id
759 }
760
761 #[must_use]
763 pub fn source(&self) -> &UUri {
764 &self.source
765 }
766
767 #[must_use]
769 pub fn sink(&self) -> Option<&UUri> {
770 self.sink.as_ref()
771 }
772
773 #[must_use]
775 pub fn reqid(&self) -> Option<&UUID> {
776 self.reqid.as_ref()
777 }
778
779 #[must_use]
781 pub fn priority(&self) -> Option<FramePriority> {
782 self.priority
783 }
784
785 #[must_use]
787 pub fn ttl(&self) -> Option<Duration> {
788 self.ttl
789 }
790
791 #[must_use]
793 pub fn comm_status(&self) -> Option<UCode> {
794 self.comm_status
795 }
796
797 #[must_use]
799 pub fn permission_level(&self) -> Option<u32> {
800 self.permission_level
801 }
802
803 #[must_use]
805 pub fn token(&self) -> Option<&str> {
806 self.token.as_deref()
807 }
808
809 #[must_use]
811 pub fn traceparent(&self) -> Option<&str> {
812 self.traceparent.as_deref()
813 }
814
815 #[must_use]
817 pub fn payload_encoding(&self) -> Option<&PayloadEncoding> {
818 self.payload_encoding.as_ref()
819 }
820
821 #[must_use]
823 pub fn into_payload_encoding(self) -> Option<PayloadEncoding> {
824 self.payload_encoding
825 }
826
827 pub fn with_payload_encoding(
833 mut self,
834 payload_encoding: PayloadEncoding,
835 ) -> Result<Self, UFrameMetadataError> {
836 payload_encoding.validate()?;
837 self.payload_encoding = Some(payload_encoding);
838 self.validate()?;
839 Ok(self)
840 }
841
842 #[must_use]
844 pub fn without_payload_encoding(mut self) -> Self {
845 self.payload_encoding = None;
846 self
847 }
848
849 pub fn validate(&self) -> Result<(), UFrameMetadataError> {
857 if let Some(encoding) = &self.payload_encoding {
858 encoding.validate()?;
859 }
860 let mut errors: Vec<String> = Vec::new();
861 match self.kind {
862 FrameMessageKind::Publish => {
863 if let Err(e) = self.source.verify_event() {
864 errors.push(format!("invalid source URI: {e}"));
865 }
866 if self.sink.is_some() {
867 errors.push("publish frame must not have a sink".to_string());
868 }
869 }
870 FrameMessageKind::Notification => {
871 if self.source.is_rpc_response() {
872 errors.push("source must not be an RPC response URI".to_string());
873 } else if let Err(e) = self.source.verify_no_wildcards() {
874 errors.push(format!("invalid source URI: {e}"));
875 }
876 match &self.sink {
877 Some(sink) => {
878 if !sink.is_notification_destination() {
879 errors.push("sink is not a valid notification destination".to_string());
880 } else if let Err(e) = sink.verify_no_wildcards() {
881 errors.push(format!("invalid sink URI: {e}"));
882 }
883 }
884 None => errors.push("notification frame must have a sink".to_string()),
885 }
886 }
887 FrameMessageKind::Request => {
888 if let Err(e) = self.source.verify_rpc_response() {
889 errors.push(format!("invalid source URI: {e}"));
890 }
891 match &self.sink {
892 Some(sink) => {
893 if let Err(e) = sink.verify_rpc_method() {
894 errors.push(format!("invalid sink URI: {e}"));
895 }
896 }
897 None => errors
898 .push("request frame must have a method-to-invoke in the sink".to_string()),
899 }
900 match self.ttl {
901 Some(ttl) if !ttl.is_zero() => {}
902 Some(_) => {
903 errors.push("request frame TTL must be greater than zero".to_string())
904 }
905 None => errors.push("request frame must have a TTL".to_string()),
906 }
907 if let Err(e) = self.validate_rpc_priority() {
908 errors.push(e);
909 }
910 }
911 FrameMessageKind::Response => {
912 if let Err(e) = self.source.verify_rpc_method() {
913 errors.push(format!("invalid source URI: {e}"));
914 }
915 match &self.sink {
916 Some(sink) => {
917 if let Err(e) = sink.verify_rpc_response() {
918 errors.push(format!("invalid sink URI: {e}"));
919 }
920 }
921 None => errors.push("response frame must have a sink".to_string()),
922 }
923 if self.reqid.is_none() {
924 errors.push("response frame must have a request id".to_string());
925 }
926 if let Err(e) = self.validate_rpc_priority() {
927 errors.push(e);
928 }
929 }
930 }
931 if errors.is_empty() {
932 Ok(())
933 } else {
934 Err(UFrameMetadataError::InvalidMetadata(errors.join("; ")))
935 }
936 }
937
938 fn validate_rpc_priority(&self) -> Result<(), String> {
939 match self.priority {
940 Some(priority) if priority >= FramePriority::CS4 => Ok(()),
941 Some(_) => Err("RPC frame must have a priority of at least CS4".to_string()),
942 None => Err("RPC frame must have a priority".to_string()),
943 }
944 }
945
946 pub fn try_project_to_attributes(&self) -> Result<UAttributes, UFrameMetadataError> {
958 let (payload_format, open_encoding) = match &self.payload_encoding {
959 None => (Some(UPayloadFormat::Unspecified), None),
960 Some(encoding) => match encoding.to_legacy_format() {
961 Some(format) => (Some(format), None),
962 None => (None, Some(encoding)),
963 },
964 };
965 let mut attributes = self.project_to_attributes_with_payload_format(payload_format)?;
966 if let Some(encoding) = open_encoding {
967 attributes.payload_encoding_registry_id = encoding.registry_id();
968 attributes.payload_encoding = encoding.literal_id().map(str::to_owned);
969 attributes.payload_content_type = encoding.content_type().map(str::to_owned);
970 }
971 Ok(attributes)
972 }
973
974 pub(crate) fn project_to_attributes_with_payload_format(
981 &self,
982 payload_format: Option<UPayloadFormat>,
983 ) -> Result<UAttributes, UFrameMetadataError> {
984 self.validate()?;
985 Ok(UAttributes {
986 type_: self.kind.to_legacy_type(),
987 id: self.id.clone(),
988 source: self.source.clone(),
989 sink: self.sink.clone(),
990 priority: self.priority.map(FramePriority::to_legacy_priority),
991 commstatus: self.comm_status,
992 ttl: self.ttl.map(project_ttl_to_legacy_millis).transpose()?,
993 permission_level: self.permission_level,
994 token: self.token.clone(),
995 traceparent: self.traceparent.clone(),
996 reqid: self.reqid.clone(),
997 payload_format,
998 payload_encoding_registry_id: None,
999 payload_encoding: None,
1000 payload_content_type: None,
1001 })
1002 }
1003}
1004
1005fn project_ttl_to_legacy_millis(ttl: Duration) -> Result<u32, UFrameMetadataError> {
1006 if !ttl.subsec_nanos().is_multiple_of(1_000_000) {
1007 return Err(UFrameMetadataError::FieldNotRepresentable {
1008 field: "ttl",
1009 reason: format!(
1010 "{ttl:?} has sub-millisecond precision; legacy TTL is in whole milliseconds"
1011 ),
1012 });
1013 }
1014 u32::try_from(ttl.as_millis()).map_err(|_| UFrameMetadataError::FieldNotRepresentable {
1015 field: "ttl",
1016 reason: format!("{ttl:?} exceeds the legacy 32-bit millisecond TTL range"),
1017 })
1018}
1019
1020#[derive(Clone, Debug)]
1031pub struct UFrameMetadataBuilder {
1032 kind: FrameMessageKind,
1033 id: Option<UUID>,
1034 source: UUri,
1035 sink: Option<UUri>,
1036 reqid: Option<UUID>,
1037 priority: Option<FramePriority>,
1038 ttl: Option<Duration>,
1039 comm_status: Option<UCode>,
1040 permission_level: Option<u32>,
1041 token: Option<String>,
1042 traceparent: Option<String>,
1043 payload_encoding: Option<PayloadEncoding>,
1044}
1045
1046impl UFrameMetadataBuilder {
1047 fn new(kind: FrameMessageKind, source: UUri, sink: Option<UUri>) -> Self {
1048 Self {
1049 kind,
1050 id: None,
1051 source,
1052 sink,
1053 reqid: None,
1054 priority: None,
1055 ttl: None,
1056 comm_status: None,
1057 permission_level: None,
1058 token: None,
1059 traceparent: None,
1060 payload_encoding: None,
1061 }
1062 }
1063
1064 #[must_use]
1066 pub fn with_id(mut self, id: UUID) -> Self {
1067 self.id = Some(id);
1068 self
1069 }
1070
1071 #[must_use]
1073 pub fn with_priority(mut self, priority: FramePriority) -> Self {
1074 self.priority = Some(priority);
1075 self
1076 }
1077
1078 #[must_use]
1080 pub fn with_ttl(mut self, ttl: Duration) -> Self {
1081 self.ttl = Some(ttl);
1082 self
1083 }
1084
1085 #[must_use]
1087 pub fn with_comm_status(mut self, comm_status: UCode) -> Self {
1088 self.comm_status = Some(comm_status);
1089 self
1090 }
1091
1092 #[must_use]
1094 pub fn with_permission_level(mut self, permission_level: u32) -> Self {
1095 self.permission_level = Some(permission_level);
1096 self
1097 }
1098
1099 #[must_use]
1101 pub fn with_token(mut self, token: impl Into<String>) -> Self {
1102 self.token = Some(token.into());
1103 self
1104 }
1105
1106 #[must_use]
1108 pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
1109 self.traceparent = Some(traceparent.into());
1110 self
1111 }
1112
1113 #[must_use]
1115 pub fn with_payload_encoding(mut self, payload_encoding: PayloadEncoding) -> Self {
1116 self.payload_encoding = Some(payload_encoding);
1117 self
1118 }
1119
1120 pub fn build(self) -> Result<UFrameMetadata, UFrameMetadataError> {
1130 let metadata = UFrameMetadata {
1131 kind: self.kind,
1132 id: self.id.unwrap_or_else(UUID::build),
1133 source: self.source,
1134 sink: self.sink,
1135 reqid: self.reqid,
1136 priority: self.priority,
1137 ttl: self.ttl,
1138 comm_status: self.comm_status,
1139 permission_level: self.permission_level,
1140 token: self.token,
1141 traceparent: self.traceparent,
1142 payload_encoding: self.payload_encoding,
1143 };
1144 metadata.validate()?;
1145 Ok(metadata)
1146 }
1147}
1148
1149impl crate::UMessage {
1154 pub fn to_frame_metadata(
1162 &self,
1163 payload_encoding: PayloadEncoding,
1164 ) -> Result<UFrameMetadata, UFrameMetadataError> {
1165 if self.payload().is_none() {
1166 return Err(UFrameMetadataError::EncodingWithoutPayload);
1167 }
1168 self.attributes().to_frame_metadata(payload_encoding)
1169 }
1170
1171 pub fn to_frame_metadata_unencoded(&self) -> Result<UFrameMetadata, UFrameMetadataError> {
1178 if self.payload().is_some() {
1179 return Err(UFrameMetadataError::PayloadWithoutEncoding);
1180 }
1181 self.attributes().to_frame_metadata_unencoded()
1182 }
1183}
1184
1185impl crate::UAttributes {
1186 pub fn to_frame_metadata(
1198 &self,
1199 payload_encoding: PayloadEncoding,
1200 ) -> Result<UFrameMetadata, UFrameMetadataError> {
1201 try_project_attributes_to_frame_metadata(self, Some(payload_encoding))
1202 }
1203
1204 pub fn to_frame_metadata_unencoded(&self) -> Result<UFrameMetadata, UFrameMetadataError> {
1212 let metadata = try_project_attributes_to_frame_metadata(self, None)?;
1213 if metadata.payload_encoding().is_some() {
1214 return Err(UFrameMetadataError::EncodingWithoutPayload);
1215 }
1216 Ok(metadata)
1217 }
1218}
1219
1220pub fn try_project_attributes_to_frame_metadata(
1232 attributes: &UAttributes,
1233 payload_encoding: Option<PayloadEncoding>,
1234) -> Result<UFrameMetadata, UFrameMetadataError> {
1235 let legacy_encoding = match attributes.payload_format() {
1236 None | Some(UPayloadFormat::Unspecified) => None,
1237 Some(format) => Some(PayloadEncoding::try_from_legacy_format(format)?),
1238 };
1239 let open_present = attributes.open_payload_encoding_parts() != (None, None, None);
1240 let attributes_encoding = match (legacy_encoding, open_present) {
1241 (Some(_), true) => {
1242 return Err(UFrameMetadataError::InvalidMetadata(
1243 "attributes declare both a concrete payload_format and open payload-encoding fields; exactly one mechanism is allowed".into(),
1244 ));
1245 }
1246 (Some(legacy), false) => Some(legacy),
1247 (None, true) => {
1248 let encoding = PayloadEncoding::from_parts(
1249 attributes.payload_encoding_registry_id,
1250 attributes.payload_encoding.clone(),
1251 attributes.payload_content_type.clone(),
1252 )?;
1253 if encoding.to_legacy_format().is_some() {
1254 return Err(UFrameMetadataError::InvalidMetadata(
1255 "registered-range payload encodings must use payload_format, not open payload-encoding fields".into(),
1256 ));
1257 }
1258 Some(encoding)
1259 }
1260 (None, false) => None,
1261 };
1262 let payload_encoding = match (payload_encoding, attributes_encoding) {
1263 (Some(explicit), Some(from_attributes)) => {
1264 if explicit != from_attributes {
1265 return Err(UFrameMetadataError::InvalidMetadata(format!(
1266 "attribute payload format `{}` does not match payload encoding `{}`",
1267 from_attributes.describe(),
1268 explicit.describe()
1269 )));
1270 }
1271 Some(explicit)
1272 }
1273 (Some(explicit), None) => Some(explicit),
1274 (None, from_attributes) => from_attributes,
1275 };
1276
1277 let metadata = UFrameMetadata {
1278 kind: FrameMessageKind::from_legacy_type(attributes.type_),
1279 id: attributes.id.clone(),
1280 source: attributes.source.clone(),
1281 sink: attributes.sink.clone(),
1282 reqid: attributes.reqid.clone(),
1283 priority: attributes.priority.map(FramePriority::from_legacy_priority),
1284 ttl: attributes.ttl.map(u64::from).map(Duration::from_millis),
1285 comm_status: attributes.commstatus,
1286 permission_level: attributes.permission_level,
1287 token: attributes.token.clone(),
1288 traceparent: attributes.traceparent.clone(),
1289 payload_encoding,
1290 };
1291 metadata.validate()?;
1292 Ok(metadata)
1293}
1294
1295pub fn try_project_umessage_to_frame_metadata(
1302 message: &UMessage,
1303) -> Result<UFrameMetadata, UFrameMetadataError> {
1304 let open_present = message.attributes().open_payload_encoding_parts() != (None, None, None);
1305 let legacy_present = matches!(
1306 message.payload_format(),
1307 Some(format) if format != UPayloadFormat::Unspecified
1308 );
1309 match (message.payload().is_some(), legacy_present || open_present) {
1310 (true, false) => {
1311 return Err(UFrameMetadataError::PayloadWithoutEncoding);
1312 }
1313 (false, true) => {
1314 return Err(UFrameMetadataError::EncodingWithoutPayload);
1315 }
1316 _ => {}
1317 }
1318 try_project_attributes_to_frame_metadata(message.attributes(), None)
1319}
1320
1321pub fn try_project_frame_to_umessage(
1333 metadata: UFrameMetadata,
1334 payload: Option<Bytes>,
1335) -> Result<UMessage, UFrameMetadataError> {
1336 match (payload.is_some(), metadata.payload_encoding().is_some()) {
1337 (true, true) | (false, false) => {}
1338 (true, false) => return Err(UFrameMetadataError::PayloadWithoutEncoding),
1339 (false, true) => return Err(UFrameMetadataError::EncodingWithoutPayload),
1340 }
1341 let attributes = metadata.try_project_to_attributes()?;
1342 UMessage::new(attributes, payload).map_err(UFrameMetadataError::from)
1343}
1344
1345#[cfg(test)]
1346mod tests {
1347 use super::*;
1348 use crate::{UCode, UMessageBuilder, UUri};
1349
1350 fn topic() -> UUri {
1351 UUri::try_from_parts("vehicle", 0x4210, 0x01, 0x9000).expect("failed to create test URI")
1352 }
1353
1354 fn method() -> UUri {
1355 UUri::try_from_parts("vehicle", 0x4210, 0x01, 0x00b1).expect("failed to create method URI")
1356 }
1357
1358 fn reply_to() -> UUri {
1359 UUri::try_from_parts("cloud", 0x10ab, 0x02, 0x0000).expect("failed to create reply URI")
1360 }
1361
1362 #[test]
1363 fn builder_constructs_native_publish_metadata() {
1364 let metadata = UFrameMetadata::publish(topic())
1365 .with_priority(FramePriority::CS1)
1366 .with_traceparent("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01")
1367 .with_payload_encoding(PayloadEncoding::JSON)
1368 .build()
1369 .expect("metadata");
1370
1371 assert_eq!(metadata.kind(), FrameMessageKind::Publish);
1372 assert_eq!(metadata.source(), &topic());
1373 assert!(metadata.sink().is_none());
1374 assert_eq!(metadata.priority(), Some(FramePriority::CS1));
1375 assert_eq!(metadata.payload_encoding(), Some(&PayloadEncoding::JSON));
1376 }
1377
1378 #[test]
1379 fn builder_rejects_publish_with_low_rpc_shape() {
1380 let error = UFrameMetadata::request(method(), reply_to(), Duration::ZERO)
1382 .build()
1383 .unwrap_err();
1384 assert!(matches!(error, UFrameMetadataError::InvalidMetadata(_)));
1385
1386 let error = UFrameMetadata::request(method(), reply_to(), Duration::from_secs(5))
1388 .with_priority(FramePriority::CS2)
1389 .build()
1390 .unwrap_err();
1391 assert!(matches!(error, UFrameMetadataError::InvalidMetadata(_)));
1392 }
1393
1394 #[test]
1395 fn builder_constructs_request_and_response_metadata() {
1396 let request = UFrameMetadata::request(method(), reply_to(), Duration::from_secs(5))
1397 .build()
1398 .expect("request metadata");
1399 assert_eq!(request.kind(), FrameMessageKind::Request);
1400 assert_eq!(request.priority(), Some(FramePriority::CS4));
1401 assert_eq!(request.ttl(), Some(Duration::from_secs(5)));
1402
1403 let response = UFrameMetadata::response(method(), reply_to(), request.id().clone())
1404 .build()
1405 .expect("response metadata");
1406 assert_eq!(response.kind(), FrameMessageKind::Response);
1407 assert_eq!(response.reqid(), Some(request.id()));
1408 }
1409
1410 #[test]
1411 fn message_without_payload_projects_to_metadata_without_encoding() {
1412 let message = UMessageBuilder::publish(topic()).build().expect("message");
1413
1414 let metadata = try_project_umessage_to_frame_metadata(&message).expect("metadata");
1415
1416 assert!(message.payload().is_none());
1417 assert!(metadata.payload_encoding().is_none());
1418 assert_eq!(metadata.id(), message.id());
1419 assert_eq!(metadata.kind(), FrameMessageKind::Publish);
1420 }
1421
1422 #[test]
1423 fn message_method_without_payload_projects_to_metadata_without_encoding() {
1424 let message = UMessageBuilder::publish(topic()).build().expect("message");
1425
1426 let metadata = message.to_frame_metadata_unencoded().expect("metadata");
1427
1428 assert!(metadata.payload_encoding().is_none());
1429 }
1430
1431 #[test]
1432 fn message_method_with_payload_requires_explicit_encoding() {
1433 let message = UMessageBuilder::publish(topic())
1434 .build_with_payload(Bytes::from_static(b"payload"), UPayloadFormat::Unspecified)
1435 .expect("message");
1436
1437 assert_eq!(
1438 message.to_frame_metadata_unencoded().unwrap_err(),
1439 UFrameMetadataError::PayloadWithoutEncoding
1440 );
1441 assert_eq!(
1442 message
1443 .to_frame_metadata(PayloadEncoding::RAW)
1444 .expect("explicit encoding")
1445 .payload_encoding(),
1446 Some(&PayloadEncoding::RAW)
1447 );
1448 }
1449
1450 #[test]
1451 fn message_method_rejects_encoding_without_payload() {
1452 let message = UMessageBuilder::publish(topic()).build().expect("message");
1453
1454 assert_eq!(
1455 message.to_frame_metadata(PayloadEncoding::RAW).unwrap_err(),
1456 UFrameMetadataError::EncodingWithoutPayload
1457 );
1458 }
1459
1460 #[test]
1461 fn attributes_unencoded_method_rejects_declared_encoding() {
1462 let message = UMessageBuilder::publish(topic())
1463 .build_with_payload(Bytes::new(), UPayloadFormat::Raw)
1464 .expect("message");
1465
1466 assert_eq!(
1467 message
1468 .attributes()
1469 .to_frame_metadata_unencoded()
1470 .unwrap_err(),
1471 UFrameMetadataError::EncodingWithoutPayload
1472 );
1473 }
1474
1475 #[test]
1476 fn empty_present_payload_keeps_encoding() {
1477 let message = UMessageBuilder::publish(topic())
1478 .build_with_payload(Bytes::new(), UPayloadFormat::Raw)
1479 .expect("message");
1480
1481 let metadata = try_project_umessage_to_frame_metadata(&message).expect("metadata");
1482
1483 assert_eq!(message.payload(), Some(Bytes::new()));
1484 assert_eq!(metadata.payload_encoding(), Some(&PayloadEncoding::RAW));
1485
1486 let projected =
1487 try_project_frame_to_umessage(metadata, Some(Bytes::new())).expect("projected message");
1488 assert_eq!(projected.payload(), Some(Bytes::new()));
1489 assert_eq!(projected.payload_format(), Some(UPayloadFormat::Raw));
1490 }
1491
1492 #[test]
1493 fn standard_payload_formats_round_trip() {
1494 for format in [
1495 UPayloadFormat::Protobuf,
1496 UPayloadFormat::ProtobufWrappedInAny,
1497 UPayloadFormat::Raw,
1498 UPayloadFormat::Json,
1499 UPayloadFormat::Text,
1500 UPayloadFormat::Someip,
1501 UPayloadFormat::SomeipTlv,
1502 UPayloadFormat::Shm,
1503 ] {
1504 let message = UMessageBuilder::publish(topic())
1505 .build_with_payload(Bytes::from_static(b"payload"), format)
1506 .expect("message");
1507 let metadata = try_project_umessage_to_frame_metadata(&message).expect("metadata");
1508 let encoding = metadata.payload_encoding().expect("encoding");
1509 assert_eq!(encoding.to_legacy_format(), Some(format));
1510 assert!(encoding.registry_id().is_some());
1511 assert!(encoding.literal_id().is_some());
1512 assert!(encoding.content_type().is_some());
1513
1514 let projected =
1515 try_project_frame_to_umessage(metadata, Some(Bytes::from_static(b"payload")))
1516 .expect("projected message");
1517 assert_eq!(projected.payload(), Some(Bytes::from_static(b"payload")));
1518 assert_eq!(projected.payload_format(), Some(format));
1519 }
1520 }
1521
1522 #[test]
1523 fn unspecified_payload_format_with_payload_is_rejected() {
1524 let message = UMessageBuilder::publish(topic())
1525 .build_with_payload(Bytes::from_static(b"payload"), UPayloadFormat::Unspecified)
1526 .expect("message");
1527
1528 let error = try_project_umessage_to_frame_metadata(&message).unwrap_err();
1529
1530 assert_eq!(error, UFrameMetadataError::PayloadWithoutEncoding);
1531 }
1532
1533 #[test]
1534 fn attribute_projection_rejects_payload_format_mismatch() {
1535 let message = UMessageBuilder::publish(topic())
1536 .build_with_payload(Bytes::from_static(b"payload"), UPayloadFormat::Raw)
1537 .expect("message");
1538
1539 let error = try_project_attributes_to_frame_metadata(
1540 message.attributes(),
1541 Some(PayloadEncoding::JSON),
1542 )
1543 .unwrap_err();
1544
1545 assert!(matches!(error, UFrameMetadataError::InvalidMetadata(_)));
1546 }
1547
1548 #[test]
1549 fn custom_encoding_projection_to_umessage_carries_open_identity() {
1550 let metadata = UFrameMetadata::publish(topic())
1551 .with_payload_encoding(
1552 PayloadEncoding::custom("up.native", "application/vnd.example.native").unwrap(),
1553 )
1554 .build()
1555 .expect("metadata");
1556
1557 let message = try_project_frame_to_umessage(metadata, Some(Bytes::from_static(b"payload")))
1558 .expect("projection");
1559
1560 assert_eq!(
1561 message.attributes().open_payload_encoding_parts(),
1562 (
1563 None,
1564 Some("up.native"),
1565 Some("application/vnd.example.native")
1566 )
1567 );
1568 }
1569
1570 #[test]
1571 fn sub_millisecond_ttl_is_rejected_by_legacy_projection_not_truncated() {
1572 let metadata = UFrameMetadata::request(
1573 method(),
1574 reply_to(),
1575 Duration::from_micros(1_500), )
1577 .build()
1578 .expect("metadata");
1579
1580 let error = metadata.try_project_to_attributes().unwrap_err();
1581 assert!(matches!(
1582 error,
1583 UFrameMetadataError::FieldNotRepresentable { field: "ttl", .. }
1584 ));
1585 }
1586
1587 #[test]
1588 fn custom_encoding_is_validated() {
1589 assert_eq!(
1590 PayloadEncoding::custom("", "application/vnd.example.native").unwrap_err(),
1591 UFrameMetadataError::EmptyCustomEncodingId
1592 );
1593 assert_eq!(
1594 PayloadEncoding::custom("native", "").unwrap_err(),
1595 UFrameMetadataError::EmptyCustomEncodingContentType
1596 );
1597 assert!(matches!(
1598 PayloadEncoding::custom("native", "not a media type"),
1599 Err(UFrameMetadataError::InvalidCustomEncodingContentType(_))
1600 ));
1601 assert_eq!(
1602 PayloadEncoding::from_parts(None, None, None).unwrap_err(),
1603 UFrameMetadataError::EmptyPayloadEncoding
1604 );
1605 assert!(matches!(
1606 PayloadEncoding::custom("up.raw", "application/octet-stream"),
1607 Err(UFrameMetadataError::RegisteredPayloadEncodingAlias { .. })
1608 ));
1609 }
1610
1611 #[test]
1612 fn registered_literal_decodes_to_canonical_encoding() {
1613 let encoding = PayloadEncoding::from_parts(
1614 None,
1615 Some("up.raw".to_string()),
1616 Some("application/octet-stream".to_string()),
1617 )
1618 .expect("canonicalized");
1619
1620 assert_eq!(encoding, PayloadEncoding::RAW);
1621 }
1622
1623 #[test]
1624 fn attributes_reject_both_encoding_mechanisms() {
1625 let message = UMessageBuilder::publish(topic())
1626 .build_with_payload(Bytes::from_static(b"x"), UPayloadFormat::Raw)
1627 .expect("message");
1628 let mut attributes = message.attributes().clone();
1629 attributes.payload_encoding = Some("up.xcdr-v2".to_string());
1630
1631 let error = try_project_attributes_to_frame_metadata(&attributes, None).unwrap_err();
1632
1633 assert!(matches!(error, UFrameMetadataError::InvalidMetadata(_)));
1634 }
1635
1636 #[test]
1637 fn attributes_reject_registered_encoding_in_open_fields() {
1638 let message = UMessageBuilder::publish(topic())
1639 .build_with_payload_encoding(
1640 Bytes::from_static(b"x"),
1641 PayloadEncoding::custom("up.xcdr-v2", "application/vnd.uprotocol.xcdr-v2")
1642 .expect("encoding"),
1643 )
1644 .expect("message");
1645 let mut attributes = message.attributes().clone();
1646 attributes.payload_encoding = Some("up.raw".to_string());
1647 attributes.payload_content_type = Some("application/octet-stream".to_string());
1648
1649 let error = try_project_attributes_to_frame_metadata(&attributes, None).unwrap_err();
1650
1651 assert!(matches!(error, UFrameMetadataError::InvalidMetadata(_)));
1652 }
1653
1654 #[test]
1655 fn legacy_reserved_registry_ids_are_value_compatible() {
1656 for (encoding, format) in [
1657 (
1658 PayloadEncoding::PROTOBUF_WRAPPED_IN_ANY,
1659 UPayloadFormat::ProtobufWrappedInAny,
1660 ),
1661 (PayloadEncoding::PROTOBUF, UPayloadFormat::Protobuf),
1662 (PayloadEncoding::JSON, UPayloadFormat::Json),
1663 (PayloadEncoding::SOMEIP, UPayloadFormat::Someip),
1664 (PayloadEncoding::SOMEIP_TLV, UPayloadFormat::SomeipTlv),
1665 (PayloadEncoding::RAW, UPayloadFormat::Raw),
1666 (PayloadEncoding::TEXT, UPayloadFormat::Text),
1667 (PayloadEncoding::SHM, UPayloadFormat::Shm),
1668 ] {
1669 assert_eq!(
1670 encoding.registry_id(),
1671 Some(u32::try_from(format.as_i32()).expect("legacy format code is positive"))
1672 );
1673 assert_eq!(
1674 PayloadEncoding::try_from_legacy_format(format).expect("projection"),
1675 encoding
1676 );
1677 assert_eq!(encoding.to_legacy_format(), Some(format));
1678 }
1679 assert!(PayloadEncoding::try_from_legacy_format(UPayloadFormat::Unspecified).is_err());
1680 }
1681
1682 #[test]
1683 fn pr328_enum_names_compile() {
1684 assert_eq!(UCode::InvalidArgument as i32, 3);
1685 assert_eq!(UCode::Unimplemented as i32, 12);
1686 assert_eq!(UPayloadFormat::ProtobufWrappedInAny.as_i32(), 1);
1687 assert_eq!(UPayloadFormat::Someip.as_i32(), 4);
1688 assert_eq!(UPayloadFormat::SomeipTlv.as_i32(), 5);
1689 }
1690}
1691
1692#[cfg(test)]
1693mod projection_round_trip_properties {
1694 use proptest::prelude::*;
1695
1696 use crate::{PayloadEncoding, UMessageBuilder, UPayloadFormat, UUri};
1697
1698 proptest! {
1699 #[test]
1704 fn projected_metadata_survives_field_block_round_trip(
1705 authority in "[a-z][a-z0-9]{0,11}",
1706 ue_id in 1u32..0xFFFF,
1707 version in 1u8..=0xFE,
1708 resource in 0x8000u16..0xFFFE,
1709 payload in proptest::collection::vec(any::<u8>(), 0..64),
1710 ) {
1711 let topic = UUri::try_from_parts(&authority, ue_id, version, resource)
1712 .expect("parts chosen from the valid range");
1713 let message = if payload.is_empty() {
1714 UMessageBuilder::publish(topic).build().expect("valid publish message")
1715 } else {
1716 UMessageBuilder::publish(topic)
1717 .build_with_payload(payload, UPayloadFormat::Raw)
1718 .expect("valid publish message with payload")
1719 };
1720
1721 let metadata = if message.payload().is_some() {
1722 message.to_frame_metadata(PayloadEncoding::RAW)
1723 } else {
1724 message.to_frame_metadata_unencoded()
1725 }
1726 .expect("valid message must project");
1727
1728 let encoded = crate::frame::codec::encode_frame_metadata_fields(&metadata)
1729 .expect("projected metadata must encode");
1730 let decoded = crate::frame::codec::decode_frame_metadata_fields(&encoded)
1731 .expect("encoded metadata must decode");
1732
1733 prop_assert_eq!(metadata, decoded);
1734 }
1735 }
1736}