Preview of the proposed up-rust native-frame-model branch (up-rust b6b99c6d, up-spec f0e9b17) — not released documentation. branch · write-up

up_rust/frame/
metadata.rs

1/********************************************************************************
2 * Copyright (c) 2026 Contributors to the Eclipse Foundation
3 *
4 * See the NOTICE file(s) distributed with this work for additional
5 * information regarding copyright ownership.
6 *
7 * This program and the accompanying materials are made available under the
8 * terms of the Apache License Version 2.0 which is available at
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * SPDX-License-Identifier: Apache-2.0
12 ********************************************************************************/
13
14//! Clean semantic native frame metadata.
15//!
16//! [`UFrameMetadata`] is the canonical metadata model for native uProtocol
17//! frames (owned frames, zero-copy frame views, and selected-wire paths).
18//! It carries semantic fields directly and contains **no** legacy
19//! `UAttributes`, no `UPayloadFormat`, and no protobuf concepts.
20//!
21//! Legacy compatibility lives at the edges of this module as *fallible
22//! projections*:
23//!
24//! * [`try_project_umessage_to_frame_metadata`] / [`try_project_frame_to_umessage`]
25//!   convert between `UMessage` and native frames.
26//! * [`try_project_attributes_to_frame_metadata`] /
27//!   [`UFrameMetadata::try_project_to_attributes`] convert between
28//!   `UAttributes` and native frame metadata.
29//!
30//! Projections fail — they never truncate or silently drop information —
31//! when the target model cannot represent the source (for example a native
32//! [`PayloadEncoding`] that has no legacy `UPayloadFormat` equivalent).
33//!
34//! Fixed-layout representations of this metadata (for C/C++ shared-memory
35//! interop) are *derived profiles*, not this type: see the `frame::abi`
36//! module. Byte serializations for transports are produced by selected-wire
37//! metadata codecs: see the `wire` module.
38
39use 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// ---------------------------------------------------------------------------
50// FrameMessageKind
51// ---------------------------------------------------------------------------
52
53/// Semantic kind of a native uProtocol frame.
54///
55/// This is the native-frame vocabulary. The numeric wire codes used by frame
56/// codecs and ABI profiles are defined by [`FrameMessageKind::wire_code`];
57/// the mapping to the legacy `UMessageType` is an explicit projection
58/// (see [`FrameMessageKind::from_legacy_type`]).
59#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
60pub enum FrameMessageKind {
61    /// An event published to a topic.
62    Publish,
63    /// A directed notification to one consumer.
64    Notification,
65    /// An RPC request.
66    Request,
67    /// An RPC response.
68    Response,
69}
70
71impl FrameMessageKind {
72    /// Gets the normative UFrame wire code of this kind.
73    ///
74    /// Codes are defined by the UFrame specification: 1 = Publish,
75    /// 2 = Request, 3 = Response, 4 = Notification, 0 and 5..=255 reserved.
76    /// They deliberately coincide with the legacy protobuf `UMessageType`
77    /// numbering so that projections are value-preserving, but the UFrame
78    /// registry is normative from here on.
79    #[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    /// Gets the kind denoted by a UFrame wire code.
90    #[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    /// Projects a legacy `UMessageType` to a frame message kind.
102    #[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    /// Projects this frame message kind to the legacy `UMessageType`.
113    #[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// ---------------------------------------------------------------------------
125// FramePriority
126// ---------------------------------------------------------------------------
127
128/// Semantic QoS class of a native uProtocol frame.
129#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
130pub enum FramePriority {
131    /// Best effort (lowest priority).
132    CS0,
133    /// Priority class CS1.
134    CS1,
135    /// Priority class CS2.
136    CS2,
137    /// Priority class CS3.
138    CS3,
139    /// Priority class CS4 (streaming).
140    CS4,
141    /// Priority class CS5 (RPC default).
142    CS5,
143    /// Priority class CS6 (highest; network control).
144    CS6,
145}
146
147impl FramePriority {
148    /// Gets the normative UFrame wire code of this priority.
149    ///
150    /// Codes are defined by the UFrame specification: 1..=7 = CS0..=CS6,
151    /// 0 = absent (wire/ABI representations only), 8..=255 reserved. They
152    /// deliberately coincide with the legacy protobuf `UPriority` numbering.
153    #[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    /// Gets the priority denoted by a UFrame wire code.
167    #[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    /// Projects a legacy `UPriority` to a frame priority.
182    #[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    /// Projects this frame priority to the legacy `UPriority`.
196    #[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// ---------------------------------------------------------------------------
211// PayloadEncoding
212// ---------------------------------------------------------------------------
213
214/// Open identity of the payload representation carried by a native frame.
215///
216/// A payload encoding is identified by up to three components:
217///
218/// * `registry_id`: a numeric id registered with the uProtocol payload
219///   encoding registry. Ids `1..=8` are permanently reserved for the
220///   encodings historically expressed by the legacy `UPayloadFormat` enum
221///   (value-compatible with it), `9..=0x7FFF_FFFF` for future registered
222///   encodings, and `0x8000_0000..` for vendor/private use.
223/// * `literal_id`: a language-neutral literal identity such as
224///   `up.stable-container` or `up.xcdr-v2`.
225/// * `content_type`: an RFC 6838 media type, optionally with parameters,
226///   such as `application/vnd.uprotocol.xcdr-v2;endianness=little;version=2`.
227///
228/// At least one component must be present. Unlike the retired closed
229/// `UPayloadFormat` enum, new encodings can be introduced without touching
230/// any SDK enum: register a literal id (and optionally a numeric id), or
231/// use a vendor media type.
232///
233/// The projection to and from the legacy enum is explicit and fallible:
234/// [`PayloadEncoding::try_from_legacy_format`] and
235/// [`PayloadEncoding::to_legacy_format`].
236#[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        /// Protocol Buffers serialized payload wrapped in `google.protobuf.Any`.
257        PROTOBUF_WRAPPED_IN_ANY,
258        1,
259        "up.protobuf-wrapped-in-any",
260        "application/x-protobuf"
261    );
262    well_known_encoding!(
263        /// Protocol Buffers serialized payload.
264        PROTOBUF,
265        2,
266        "up.protobuf",
267        "application/protobuf"
268    );
269    well_known_encoding!(
270        /// JSON payload.
271        JSON,
272        3,
273        "up.json",
274        "application/json"
275    );
276    well_known_encoding!(
277        /// SOME/IP serialized payload.
278        SOMEIP,
279        4,
280        "up.someip",
281        "application/x-someip"
282    );
283    well_known_encoding!(
284        /// SOME/IP TLV serialized payload.
285        SOMEIP_TLV,
286        5,
287        "up.someip-tlv",
288        "application/x-someip_tlv"
289    );
290    well_known_encoding!(
291        /// Raw binary payload.
292        RAW,
293        6,
294        "up.raw",
295        "application/octet-stream"
296    );
297    well_known_encoding!(
298        /// UTF-8 text payload.
299        TEXT,
300        7,
301        "up.text",
302        "text/plain"
303    );
304    well_known_encoding!(
305        /// Shared-memory reference payload.
306        SHM,
307        8,
308        "up.shm",
309        "application/x-shm"
310    );
311
312    /// All encodings with registry ids permanently reserved for legacy
313    /// `UPayloadFormat` compatibility.
314    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    /// Creates a validated custom payload encoding from a literal id and a
383    /// media type.
384    ///
385    /// # Errors
386    ///
387    /// Returns an error when `id` is empty, `content_type` is empty, or
388    /// `content_type` is not a valid media type.
389    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    /// Creates a validated payload encoding from explicit identity components.
411    ///
412    /// # Errors
413    ///
414    /// Returns an error when all components are absent, when a present
415    /// component is empty, or when the content type is not a valid media
416    /// type.
417    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    /// Returns the registered numeric identity, if present.
432    #[must_use]
433    pub fn registry_id(&self) -> Option<u32> {
434        self.registry_id
435    }
436
437    /// Returns the literal identity, if present.
438    #[must_use]
439    pub fn literal_id(&self) -> Option<&str> {
440        self.literal_id.as_deref()
441    }
442
443    /// Returns the media type, if present.
444    #[must_use]
445    pub fn content_type(&self) -> Option<&str> {
446        self.content_type.as_deref()
447    }
448
449    /// Returns the custom encoding identity `(literal_id, content_type)` when
450    /// both are present.
451    #[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    /// Returns whether this encoding can be decoded by a codec expecting `expected`.
460    #[must_use]
461    pub fn is_compatible_with(&self, expected: &Self) -> bool {
462        self == expected
463    }
464
465    /// Projects a legacy `UPayloadFormat` to its reserved registered encoding.
466    ///
467    /// This is a compatibility projection: the returned encoding is the full
468    /// registered identity (numeric id, literal id, and media type) reserved
469    /// for the legacy value.
470    ///
471    /// # Errors
472    ///
473    /// Returns an error for `UPayloadFormat::Unspecified`, which is not a
474    /// concrete payload encoding.
475    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    /// Projects this encoding to the legacy `UPayloadFormat`, if it has a
490    /// legacy equivalent.
491    ///
492    /// This is a compatibility projection for `UMessage`-shaped
493    /// boundaries. Encodings outside the reserved legacy range return `None`;
494    /// callers must treat that as "not representable", never as `Raw`.
495    #[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    /// Returns a diagnostic identity string for error messages.
511    #[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// ---------------------------------------------------------------------------
544// Errors
545// ---------------------------------------------------------------------------
546
547/// Errors returned by native frame metadata operations and projections.
548#[derive(Clone, Debug, Eq, PartialEq)]
549#[non_exhaustive]
550pub enum UFrameMetadataError {
551    /// A payload encoding was constructed without any identity component.
552    EmptyPayloadEncoding,
553    /// A payload encoding literal id is empty.
554    EmptyCustomEncodingId,
555    /// A payload encoding content type is empty.
556    EmptyCustomEncodingContentType,
557    /// A payload encoding content type is not a valid media type.
558    InvalidCustomEncodingContentType(String),
559    /// `UPayloadFormat::Unspecified` is not a concrete payload encoding.
560    UnspecifiedPayloadFormat,
561    /// Payload bytes require a payload encoding.
562    PayloadWithoutEncoding,
563    /// A payload encoding requires payload bytes.
564    EncodingWithoutPayload,
565    /// A native payload encoding cannot be represented by legacy types.
566    EncodingNotRepresentable {
567        /// Diagnostic identity of the offending encoding.
568        encoding: String,
569    },
570    /// An open identity used a literal reserved for a registered legacy row.
571    RegisteredPayloadEncodingAlias {
572        /// Literal identity that aliases a registered legacy row.
573        literal_id: String,
574        /// Canonical registered spelling.
575        registered: String,
576    },
577    /// A semantic field cannot be represented by legacy types.
578    FieldNotRepresentable {
579        /// Name of the offending field.
580        field: &'static str,
581        /// Reason the value is not representable.
582        reason: String,
583    },
584    /// The metadata violates the validity rules for its frame message kind.
585    InvalidMetadata(String),
586    /// Building a `UMessage` from projected metadata failed.
587    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// ---------------------------------------------------------------------------
639// UFrameMetadata
640// ---------------------------------------------------------------------------
641
642/// Canonical semantic metadata of a native uProtocol frame.
643///
644/// This is the model that owned frames (`UOwnedFrame`), zero-copy
645/// frame views, and selected-wire metadata codecs reason about. It owns its
646/// fields ergonomically (strings, options, [`Duration`]) because it is never
647/// reinterpreted as raw bytes across processes or languages; fixed-layout
648/// and byte-serialized representations are derived from it.
649///
650/// The frame's payload presence is a property of the frame carrier
651/// (`UOwnedFrame`, `UFrameView`, TX loan specs), not of the
652/// metadata; the v1 invariant "payload present if and only if
653/// `payload_encoding` is present" is enforced at those boundaries.
654#[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    /// Starts building metadata for a publish frame on `topic`.
672    #[must_use]
673    pub fn publish(topic: UUri) -> UFrameMetadataBuilder {
674        UFrameMetadataBuilder::new(FrameMessageKind::Publish, topic, None)
675    }
676
677    /// Starts building metadata for a notification frame from `origin` to
678    /// `destination`.
679    #[must_use]
680    pub fn notification(origin: UUri, destination: UUri) -> UFrameMetadataBuilder {
681        UFrameMetadataBuilder::new(FrameMessageKind::Notification, origin, Some(destination))
682    }
683
684    /// Starts building metadata for an RPC request frame invoking `method`,
685    /// with responses directed to `reply_to`.
686    ///
687    /// Request frames require a TTL and default to [`FramePriority::CS4`].
688    #[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    /// Starts building metadata for an RPC response frame from `invoked_method`
698    /// to `reply_to`, correlated to the request with id `request_id`.
699    ///
700    /// Response frames default to [`FramePriority::CS4`].
701    #[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    /// Assembles metadata from decoded parts without validation.
715    ///
716    /// Callers (frame codecs, ABI profile conversions) must run
717    /// [`UFrameMetadata::validate`] on the result.
718    #[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    /// Returns the frame message kind.
750    #[must_use]
751    pub fn kind(&self) -> FrameMessageKind {
752        self.kind
753    }
754
755    /// Returns the unique frame id.
756    #[must_use]
757    pub fn id(&self) -> &UUID {
758        &self.id
759    }
760
761    /// Returns the source address.
762    #[must_use]
763    pub fn source(&self) -> &UUri {
764        &self.source
765    }
766
767    /// Returns the sink address, if present.
768    #[must_use]
769    pub fn sink(&self) -> Option<&UUri> {
770        self.sink.as_ref()
771    }
772
773    /// Returns the correlated request id, if present.
774    #[must_use]
775    pub fn reqid(&self) -> Option<&UUID> {
776        self.reqid.as_ref()
777    }
778
779    /// Returns the frame priority, if present.
780    #[must_use]
781    pub fn priority(&self) -> Option<FramePriority> {
782        self.priority
783    }
784
785    /// Returns the time-to-live, if present.
786    #[must_use]
787    pub fn ttl(&self) -> Option<Duration> {
788        self.ttl
789    }
790
791    /// Returns the communication status, if present.
792    #[must_use]
793    pub fn comm_status(&self) -> Option<UCode> {
794        self.comm_status
795    }
796
797    /// Returns the permission level, if present.
798    #[must_use]
799    pub fn permission_level(&self) -> Option<u32> {
800        self.permission_level
801    }
802
803    /// Returns the access token, if present.
804    #[must_use]
805    pub fn token(&self) -> Option<&str> {
806        self.token.as_deref()
807    }
808
809    /// Returns the W3C traceparent, if present.
810    #[must_use]
811    pub fn traceparent(&self) -> Option<&str> {
812        self.traceparent.as_deref()
813    }
814
815    /// Returns the native payload encoding, if one is present.
816    #[must_use]
817    pub fn payload_encoding(&self) -> Option<&PayloadEncoding> {
818        self.payload_encoding.as_ref()
819    }
820
821    /// Consumes this metadata and returns its native payload encoding.
822    #[must_use]
823    pub fn into_payload_encoding(self) -> Option<PayloadEncoding> {
824        self.payload_encoding
825    }
826
827    /// Returns metadata equal to `self` but carrying `payload_encoding`.
828    ///
829    /// # Errors
830    ///
831    /// Returns an error if the encoding or the resulting metadata is invalid.
832    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    /// Returns metadata equal to `self` but without a payload encoding.
843    #[must_use]
844    pub fn without_payload_encoding(mut self) -> Self {
845        self.payload_encoding = None;
846        self
847    }
848
849    /// Validates this metadata against the rules of its frame message kind.
850    ///
851    /// # Errors
852    ///
853    /// Returns an error if the metadata violates the rules for its kind, for
854    /// example a publish frame carrying a sink, a request frame without TTL,
855    /// or an RPC frame with a priority below [`FramePriority::CS4`].
856    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    /// Projects this metadata to legacy `UAttributes`.
947    ///
948    /// This is a compatibility projection for `UTransport`-family (`UMessage`-shaped)
949    /// boundaries and is fallible by design only for fields that cannot be
950    /// represented by legacy types, such as a TTL that does not fit the
951    /// legacy 32-bit millisecond field. Open payload encodings are represented
952    /// by the `UAttributes` payload-encoding identity fields.
953    ///
954    /// # Errors
955    ///
956    /// Returns an error if the metadata is invalid or not representable.
957    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    /// Projects this metadata to legacy `UAttributes` with an explicitly
975    /// chosen `payload_format`.
976    ///
977    /// This exists for codecs that carry the open payload encoding in a
978    /// separate block next to the serialized attributes (so a non-legacy
979    /// encoding does not have to be representable by `UPayloadFormat`).
980    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// ---------------------------------------------------------------------------
1021// UFrameMetadataBuilder
1022// ---------------------------------------------------------------------------
1023
1024/// Builder for native frame metadata.
1025///
1026/// Obtained from [`UFrameMetadata::publish`], [`UFrameMetadata::notification`],
1027/// [`UFrameMetadata::request`], or [`UFrameMetadata::response`]. Native
1028/// selected-wire code should construct metadata through this builder instead
1029/// of going through `UMessageBuilder` and projecting.
1030#[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    /// Sets an explicit frame id instead of a generated one.
1065    #[must_use]
1066    pub fn with_id(mut self, id: UUID) -> Self {
1067        self.id = Some(id);
1068        self
1069    }
1070
1071    /// Sets the frame priority.
1072    #[must_use]
1073    pub fn with_priority(mut self, priority: FramePriority) -> Self {
1074        self.priority = Some(priority);
1075        self
1076    }
1077
1078    /// Sets the time-to-live.
1079    #[must_use]
1080    pub fn with_ttl(mut self, ttl: Duration) -> Self {
1081        self.ttl = Some(ttl);
1082        self
1083    }
1084
1085    /// Sets the communication status.
1086    #[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    /// Sets the permission level.
1093    #[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    /// Sets the access token.
1100    #[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    /// Sets the W3C traceparent.
1107    #[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    /// Sets the payload encoding of the frame's payload bytes.
1114    #[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    /// Builds and validates the frame metadata.
1121    ///
1122    /// A fresh UUIDv7 frame id is generated unless one was supplied via
1123    /// [`Self::with_id`].
1124    ///
1125    /// # Errors
1126    ///
1127    /// Returns an error if the metadata violates the rules of its frame
1128    /// message kind.
1129    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
1149// ---------------------------------------------------------------------------
1150// Legacy projections
1151// ---------------------------------------------------------------------------
1152
1153impl crate::UMessage {
1154    /// Projects this message to frame metadata carrying the given payload
1155    /// encoding.
1156    ///
1157    /// # Errors
1158    ///
1159    /// Returns an error if the message attributes and encoding disagree or
1160    /// the resulting metadata is invalid.
1161    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    /// Projects this message to frame metadata for a payload-free message.
1172    ///
1173    /// # Errors
1174    ///
1175    /// Returns an error if the message carries payload information or the
1176    /// resulting metadata is invalid.
1177    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    /// Projects these attributes to frame metadata carrying the given
1187    /// payload encoding.
1188    ///
1189    /// Convenience for [`try_project_attributes_to_frame_metadata`]; use
1190    /// [`Self::to_frame_metadata_unencoded`] when the message carries no
1191    /// payload.
1192    ///
1193    /// # Errors
1194    ///
1195    /// Returns an error if the attributes and encoding disagree or the
1196    /// resulting metadata is invalid.
1197    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    /// Projects these attributes to frame metadata for a payload-free
1205    /// message.
1206    ///
1207    /// # Errors
1208    ///
1209    /// Returns an error if the attributes carry payload information or the
1210    /// resulting metadata is invalid.
1211    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
1220/// Projects legacy `UAttributes` (plus an optional native payload encoding)
1221/// into native frame metadata.
1222///
1223/// When `payload_encoding` is `None` but the attributes carry a concrete
1224/// `payload_format`, the reserved registered encoding for that format is
1225/// used. When both are present they must agree.
1226///
1227/// # Errors
1228///
1229/// Returns an error if the attributes and encoding disagree or the resulting
1230/// metadata is invalid.
1231pub 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
1295/// Projects a `UMessage` into native frame metadata.
1296///
1297/// # Errors
1298///
1299/// Returns an error when a message carries payload bytes without a concrete
1300/// payload encoding, or a concrete payload encoding without payload bytes.
1301pub 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
1321/// Projects native frame metadata and optional payload bytes into a
1322/// `UMessage`.
1323///
1324/// Native payload encodings without a legacy `UPayloadFormat` equivalent are
1325/// carried by the `UAttributes` open payload-encoding identity fields.
1326///
1327/// # Errors
1328///
1329/// Returns an error if payload bytes and payload encoding are not both
1330/// present or both absent, if the encoding is not representable, or if
1331/// metadata invariants are violated.
1332pub 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        // request without ttl
1381        let error = UFrameMetadata::request(method(), reply_to(), Duration::ZERO)
1382            .build()
1383            .unwrap_err();
1384        assert!(matches!(error, UFrameMetadataError::InvalidMetadata(_)));
1385
1386        // rpc priority below CS4
1387        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), // 1.5 ms
1576        )
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        /// Metadata projection round-trip: a publish message built from any
1700        /// valid parts projects to frame metadata whose canonical field-block
1701        /// encoding decodes back to equal metadata. Hand-written cases pin the
1702        /// known edges; this pins the space between them.
1703        #[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}