Skip to main content

imferno_core/cpl/
codes.rs

1//! Typed validation-code catalogue for SMPTE ST 2067-3 (Composition Playlist).
2
3use crate::diagnostics::codes::ValidationCode;
4use crate::diagnostics::{Category, Severity};
5
6// ─── Spec-agnostic reason enum ────────────────────────────────────────────────
7
8/// Reason codes for ST 2067-3 validators, independent of spec edition year.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum St2067_3Code {
11    /// ST 2067-3 §5.5.1.2: ContentKind uses an unrecognized value under the SMPTE scope.
12    ContentKindUnknown,
13    /// ST 2067-3 §6.4.2: SourceEncoding present but EssenceDescriptorList absent.
14    SourceEncodingNoEssenceDescriptorList,
15    /// ST 2067-3 §6.4.2: SourceEncoding does not match any EssenceDescriptor Id.
16    SourceEncodingUnresolved,
17    /// ST 2067-3 §6.4.2: EssenceDescriptorList present but contains no descriptors.
18    EssenceDescriptorListEmpty,
19    /// ST 2067-3 §6.11: ContentVersionList present but empty.
20    ContentVersionListEmpty,
21    /// ST 2067-3 §6.11: ContentVersion/Id is empty (shall be a URI).
22    ContentVersionIdInvalid,
23    /// ST 2067-3 §6.11: ContentVersion/LabelText is absent.
24    ContentVersionLabelTextMissing,
25    /// ST 2067-3 §6.12: Locale language tag does not conform to RFC 5646.
26    LocaleLanguageTagInvalid,
27    /// ST 2067-3 §7.3: TrackId is not unique within a segment.
28    TrackIdNotUnique,
29    /// ST 2067-3 §7.4: Marker offset exceeds resource effective duration.
30    MarkerOffsetOutOfRange,
31    /// ST 2067-3 §7.4: Marker label is not a recognized SMPTE standard value.
32    MarkerLabelUnknown,
33    /// ST 2067-3 §7.2.2: All virtual tracks in a segment must span the same edit units.
34    SegmentDuration,
35    /// ST 2067-3 §6.1.9: Two ContentVersion elements share the same Id value.
36    ContentVersionIdDuplicate,
37    /// ST 2067-3 §7.3: Sequence duration is not an integer number of Composition Edit Units.
38    SegmentDurationIntegerEditUnits,
39}
40
41// ─── Per-edition enums ────────────────────────────────────────────────────────
42
43macro_rules! define_st2067_3_enum {
44    // Edition with no identical predecessor — defaults to the
45    // trait-provided `previous_identical_edition` = None.
46    ($name:ident, $prefix:literal) => {
47        define_st2067_3_enum!(@inner $name, $prefix, None);
48    };
49    // Edition whose code set matches a prior edition byte-for-byte
50    // (per the snapshot diff in docs/catalogue-todos.md Item 2).
51    ($name:ident, $prefix:literal, $previous:literal) => {
52        define_st2067_3_enum!(@inner $name, $prefix, Some($previous));
53    };
54    (@inner $name:ident, $prefix:literal, $previous:expr) => {
55        #[allow(non_camel_case_types)]
56        #[derive(Debug, Clone, Copy, PartialEq, Eq, strum::EnumIter)]
57        pub enum $name {
58            ContentKindUnknown,
59            SourceEncodingNoEssenceDescriptorList,
60            SourceEncodingUnresolved,
61            EssenceDescriptorListEmpty,
62            ContentVersionListEmpty,
63            ContentVersionIdInvalid,
64            ContentVersionLabelTextMissing,
65            LocaleLanguageTagInvalid,
66            TrackIdNotUnique,
67            MarkerOffsetOutOfRange,
68            MarkerLabelUnknown,
69            SegmentDuration,
70            ContentVersionIdDuplicate,
71            SegmentDurationIntegerEditUnits,
72        }
73
74        impl $name {
75            pub fn for_code(r: St2067_3Code) -> &'static str {
76                match r {
77                    St2067_3Code::ContentKindUnknown => {
78                        concat!($prefix, ":5.5.1.2/ContentKindUnknown")
79                    }
80                    St2067_3Code::SourceEncodingNoEssenceDescriptorList => {
81                        concat!($prefix, ":6.4.2/SourceEncodingNoEssenceDescriptorList")
82                    }
83                    St2067_3Code::SourceEncodingUnresolved => {
84                        concat!($prefix, ":6.4.2/SourceEncodingUnresolved")
85                    }
86                    St2067_3Code::EssenceDescriptorListEmpty => {
87                        concat!($prefix, ":6.4.2/EssenceDescriptorListEmpty")
88                    }
89                    St2067_3Code::ContentVersionListEmpty => {
90                        concat!($prefix, ":6.11/ContentVersionListEmpty")
91                    }
92                    St2067_3Code::ContentVersionIdInvalid => {
93                        concat!($prefix, ":6.11/ContentVersionIdInvalid")
94                    }
95                    St2067_3Code::ContentVersionLabelTextMissing => {
96                        concat!($prefix, ":6.11/ContentVersionLabelTextMissing")
97                    }
98                    St2067_3Code::LocaleLanguageTagInvalid => {
99                        concat!($prefix, ":6.12/LocaleLanguageTagInvalid")
100                    }
101                    St2067_3Code::TrackIdNotUnique => concat!($prefix, ":7.3/TrackIdNotUnique"),
102                    St2067_3Code::MarkerOffsetOutOfRange => {
103                        concat!($prefix, ":7.4/MarkerOffsetOutOfRange")
104                    }
105                    St2067_3Code::MarkerLabelUnknown => concat!($prefix, ":7.4/MarkerLabelUnknown"),
106                    St2067_3Code::SegmentDuration => concat!($prefix, ":7.2.2/SegmentDuration"),
107                    St2067_3Code::ContentVersionIdDuplicate => {
108                        concat!($prefix, ":6.1.9/ContentVersionIdDuplicate")
109                    }
110                    St2067_3Code::SegmentDurationIntegerEditUnits => {
111                        concat!($prefix, ":7.3/SegmentDurationIntegerEditUnits")
112                    }
113                }
114            }
115
116            pub const ALL: &'static [Self] = &[
117                Self::ContentKindUnknown,
118                Self::SourceEncodingNoEssenceDescriptorList,
119                Self::SourceEncodingUnresolved,
120                Self::EssenceDescriptorListEmpty,
121                Self::ContentVersionListEmpty,
122                Self::ContentVersionIdInvalid,
123                Self::ContentVersionLabelTextMissing,
124                Self::LocaleLanguageTagInvalid,
125                Self::TrackIdNotUnique,
126                Self::MarkerOffsetOutOfRange,
127                Self::MarkerLabelUnknown,
128                Self::SegmentDuration,
129                Self::ContentVersionIdDuplicate,
130                Self::SegmentDurationIntegerEditUnits,
131            ];
132        }
133
134        impl ValidationCode for $name {
135            fn code(&self) -> &'static str {
136                match self {
137                    Self::ContentKindUnknown => concat!($prefix, ":5.5.1.2/ContentKindUnknown"),
138                    Self::SourceEncodingNoEssenceDescriptorList => {
139                        concat!($prefix, ":6.4.2/SourceEncodingNoEssenceDescriptorList")
140                    }
141                    Self::SourceEncodingUnresolved => {
142                        concat!($prefix, ":6.4.2/SourceEncodingUnresolved")
143                    }
144                    Self::EssenceDescriptorListEmpty => {
145                        concat!($prefix, ":6.4.2/EssenceDescriptorListEmpty")
146                    }
147                    Self::ContentVersionListEmpty => {
148                        concat!($prefix, ":6.11/ContentVersionListEmpty")
149                    }
150                    Self::ContentVersionIdInvalid => {
151                        concat!($prefix, ":6.11/ContentVersionIdInvalid")
152                    }
153                    Self::ContentVersionLabelTextMissing => {
154                        concat!($prefix, ":6.11/ContentVersionLabelTextMissing")
155                    }
156                    Self::LocaleLanguageTagInvalid => {
157                        concat!($prefix, ":6.12/LocaleLanguageTagInvalid")
158                    }
159                    Self::TrackIdNotUnique => concat!($prefix, ":7.3/TrackIdNotUnique"),
160                    Self::MarkerOffsetOutOfRange => concat!($prefix, ":7.4/MarkerOffsetOutOfRange"),
161                    Self::MarkerLabelUnknown => concat!($prefix, ":7.4/MarkerLabelUnknown"),
162                    Self::SegmentDuration => concat!($prefix, ":7.2.2/SegmentDuration"),
163                    Self::ContentVersionIdDuplicate => {
164                        concat!($prefix, ":6.1.9/ContentVersionIdDuplicate")
165                    }
166                    Self::SegmentDurationIntegerEditUnits => {
167                        concat!($prefix, ":7.3/SegmentDurationIntegerEditUnits")
168                    }
169                }
170            }
171            fn description(&self) -> &'static str {
172                match self {
173                    Self::ContentKindUnknown => {
174                        "ContentKind uses an unrecognized value under the SMPTE scope."
175                    }
176                    Self::SourceEncodingNoEssenceDescriptorList => {
177                        "SourceEncoding present but EssenceDescriptorList absent."
178                    }
179                    Self::SourceEncodingUnresolved => {
180                        "SourceEncoding does not match any EssenceDescriptor Id."
181                    }
182                    Self::EssenceDescriptorListEmpty => {
183                        "EssenceDescriptorList present but contains no descriptors."
184                    }
185                    Self::ContentVersionListEmpty => "ContentVersionList present but empty.",
186                    Self::ContentVersionIdInvalid => "ContentVersion/Id is empty (shall be a URI).",
187                    Self::ContentVersionLabelTextMissing => "ContentVersion/LabelText is absent.",
188                    Self::LocaleLanguageTagInvalid => {
189                        "Locale language tag does not conform to RFC 5646."
190                    }
191                    Self::TrackIdNotUnique => "TrackId is not unique within a segment.",
192                    Self::MarkerOffsetOutOfRange => {
193                        "Marker offset exceeds resource effective duration."
194                    }
195                    Self::MarkerLabelUnknown => {
196                        "Marker label is not a recognized SMPTE standard value."
197                    }
198                    Self::SegmentDuration => {
199                        "All virtual tracks in a segment must span the same number of edit units."
200                    }
201                    Self::ContentVersionIdDuplicate => {
202                        "No two ContentVersion elements shall have identical Id values."
203                    }
204                    Self::SegmentDurationIntegerEditUnits => {
205                        "Sequence duration shall be an integer number of Composition Edit Units."
206                    }
207                }
208            }
209            fn default_severity(&self) -> Severity {
210                match self {
211                    Self::ContentKindUnknown => Severity::Warning,
212                    Self::SourceEncodingNoEssenceDescriptorList => Severity::Error,
213                    Self::SourceEncodingUnresolved => Severity::Error,
214                    Self::EssenceDescriptorListEmpty => Severity::Error,
215                    Self::ContentVersionListEmpty => Severity::Error,
216                    Self::ContentVersionIdInvalid => Severity::Error,
217                    Self::ContentVersionLabelTextMissing => Severity::Warning,
218                    Self::LocaleLanguageTagInvalid => Severity::Warning,
219                    Self::TrackIdNotUnique => Severity::Error,
220                    Self::MarkerOffsetOutOfRange => Severity::Error,
221                    Self::MarkerLabelUnknown => Severity::Warning,
222                    Self::SegmentDuration => Severity::Error,
223                    Self::ContentVersionIdDuplicate => Severity::Error,
224                    Self::SegmentDurationIntegerEditUnits => Severity::Error,
225                }
226            }
227            fn category(&self) -> Category {
228                match self {
229                    Self::ContentKindUnknown => Category::Metadata,
230                    Self::SourceEncodingNoEssenceDescriptorList => Category::Reference,
231                    Self::SourceEncodingUnresolved => Category::Reference,
232                    Self::EssenceDescriptorListEmpty => Category::Structure,
233                    Self::ContentVersionListEmpty => Category::Structure,
234                    Self::ContentVersionIdInvalid => Category::Metadata,
235                    Self::ContentVersionLabelTextMissing => Category::Metadata,
236                    Self::LocaleLanguageTagInvalid => Category::Metadata,
237                    Self::TrackIdNotUnique => Category::Structure,
238                    Self::MarkerOffsetOutOfRange => Category::Timing,
239                    Self::MarkerLabelUnknown => Category::Metadata,
240                    Self::SegmentDuration => Category::Timing,
241                    Self::ContentVersionIdDuplicate => Category::Structure,
242                    Self::SegmentDurationIntegerEditUnits => Category::Timing,
243                }
244            }
245            fn previous_identical_edition(&self) -> Option<&'static str> {
246                $previous
247            }
248            fn example(&self) -> Option<&'static str> {
249                Some(match self {
250                    Self::ContentKindUnknown =>
251                        "<ContentKind scope=\"http://www.smpte-ra.org/schemas/2067-3/2013/content-kind\">Banana</ContentKind>",
252                    Self::SourceEncodingNoEssenceDescriptorList =>
253                        "<Resource><SourceEncoding>urn:uuid:…</SourceEncoding></Resource>  <!-- but the CPL has no <EssenceDescriptorList> -->",
254                    Self::SourceEncodingUnresolved =>
255                        "<SourceEncoding>urn:uuid:aaaa…</SourceEncoding>  <!-- no EssenceDescriptor with that Id -->",
256                    Self::EssenceDescriptorListEmpty =>
257                        "<EssenceDescriptorList></EssenceDescriptorList>",
258                    Self::ContentVersionListEmpty =>
259                        "<ContentVersionList></ContentVersionList>",
260                    Self::ContentVersionIdInvalid =>
261                        "<ContentVersion><Id></Id>…</ContentVersion>",
262                    Self::ContentVersionLabelTextMissing =>
263                        "<ContentVersion><Id>urn:uuid:…</Id></ContentVersion>  <!-- no <LabelText> -->",
264                    Self::LocaleLanguageTagInvalid =>
265                        "<Locale><LanguageList><Language>english</Language></LanguageList></Locale>  <!-- not RFC 5646 -->",
266                    Self::TrackIdNotUnique =>
267                        "<Sequence><TrackId>urn:uuid:abc…</TrackId>…</Sequence><Sequence><TrackId>urn:uuid:abc…</TrackId>…</Sequence>  <!-- same TrackId in two sequences of one segment -->",
268                    Self::MarkerOffsetOutOfRange =>
269                        "<Marker><Offset>120</Offset></Marker>  <!-- resource IntrinsicDuration=100 -->",
270                    Self::MarkerLabelUnknown =>
271                        "<Marker><Label>NotARealMarker</Label></Marker>",
272                    Self::SegmentDuration =>
273                        "Segment with MainImageSequence duration = 24f and MainAudioSequence duration = 25f",
274                    Self::ContentVersionIdDuplicate =>
275                        "<ContentVersion><Id>urn:uuid:a…</Id></ContentVersion><ContentVersion><Id>urn:uuid:a…</Id></ContentVersion>",
276                    Self::SegmentDurationIntegerEditUnits =>
277                        "Sequence duration = 12.5 edit units (must be an integer multiple of the Composition Edit Unit)",
278                })
279            }
280        }
281
282        impl From<$name> for String {
283            fn from(c: $name) -> String {
284                c.code().to_string()
285            }
286        }
287    };
288}
289
290define_st2067_3_enum!(St2067_3_2013, "ST2067-3:2013");
291// ST 2067-3:2020 reuses the 2016 namespace and the canonical XSD is byte-identical
292// to 2016, so 2016 + 2020 share this rule set. Also bit-for-bit identical to the
293// 2013 catalogue (audit snapshot diff, 2026-06-04), recorded via the second
294// macro arm.
295define_st2067_3_enum!(St2067_3_2016, "ST2067-3:2016", "ST2067-3:2013");