Skip to main content

etdl_parser/
ast.rs

1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use std::collections::BTreeMap;
3
4pub use crate::ecel::{parse_condition, Condition};
5
6pub type NodeId = String;
7pub type GateId = String;
8
9#[derive(Debug, Clone, Serialize)]
10pub struct EtlDocument {
11    pub etdl: String,
12    pub info: Info,
13    #[serde(default)]
14    pub asyncapi_imports: BTreeMap<String, String>,
15
16    #[serde(default)]
17    pub supplements: Vec<Supplement>,
18
19    /// Declared ETDL Standard Library / domain / optional / user library
20    /// imports (see `etdl-compiler::stdlib`). Distinct from `supplements`:
21    /// a supplement is a compiled-in Rust extension identified by id; a
22    /// library is (typically) ETDL source resolved and merged before the
23    /// rest of the pipeline runs, so nothing downstream of parsing needs to
24    /// know libraries exist.
25    #[serde(default)]
26    pub libraries: Vec<LibraryImport>,
27
28    #[serde(default)]
29    pub components: Option<Components>,
30
31    pub event_trees: BTreeMap<String, EventTree>,
32
33    #[serde(default)]
34    pub fault_trees: Option<BTreeMap<String, FaultTree>>,
35
36    pub extensions: BTreeMap<String, serde_yaml::Value>,
37}
38
39/// A declared supplement/extension (core Section 5.1.1).
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub struct Supplement {
42    pub id: String,
43    pub version: String,
44    #[serde(default)]
45    pub required: bool,
46}
47
48/// A declared library import: `{ name: "std.events", version: "1.0" }`.
49///
50/// Mirrors [`Supplement`]'s shape and required/optional semantics
51/// deliberately: both are "a named, versioned external capability declared
52/// in the document" — a supplement resolves to a compiled-in Rust extension,
53/// a library resolves to ETDL source (built-in, optional, or user-provided).
54/// See `etdl-compiler::stdlib` for resolution.
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56pub struct LibraryImport {
57    /// The library's dotted name, e.g. `std.events`. Names starting with
58    /// `std.` are reserved for the built-in standard library and can never
59    /// resolve to an optional or user library (see `stdlib::LibraryError::Shadowing`).
60    pub name: String,
61    /// The requested library version, e.g. `"1.0"`. Compatibility is
62    /// major-version-gated, the same rule already used for `doc.etdl` and
63    /// for `Supplement::version`.
64    pub version: String,
65    #[serde(default)]
66    pub required: bool,
67}
68
69impl<'de> Deserialize<'de> for EtlDocument {
70    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
71    where
72        D: Deserializer<'de>,
73    {
74        use serde::de::{Error, MapAccess, Visitor};
75        use std::fmt;
76
77        struct DocVisitor;
78
79        impl<'de> Visitor<'de> for DocVisitor {
80            type Value = EtlDocument;
81
82            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
83                f.write_str("an ETDL document")
84            }
85
86            fn visit_map<A>(self, mut map: A) -> Result<EtlDocument, A::Error>
87            where
88                A: MapAccess<'de>,
89            {
90                let mut etdl: Option<String> = None;
91                let mut info: Option<Info> = None;
92                let mut asyncapi_imports: BTreeMap<String, String> = BTreeMap::new();
93                let mut supplements: Vec<Supplement> = Vec::new();
94                let mut libraries: Vec<LibraryImport> = Vec::new();
95                let mut components: Option<Components> = None;
96                let mut event_trees_map: BTreeMap<String, EventTree> = BTreeMap::new();
97                let mut event_tree_legacy: Option<EventTree> = None;
98                let mut fault_trees: Option<BTreeMap<String, FaultTree>> = None;
99                let mut extensions: BTreeMap<String, serde_yaml::Value> = BTreeMap::new();
100
101                while let Some(key) = map.next_key::<String>()? {
102                    match key.as_str() {
103                        "etdl" => {
104                            if etdl.is_some() {
105                                return Err(Error::duplicate_field("etdl"));
106                            }
107                            etdl = Some(map.next_value()?);
108                        }
109                        "info" => {
110                            if info.is_some() {
111                                return Err(Error::duplicate_field("info"));
112                            }
113                            info = Some(map.next_value()?);
114                        }
115                        "asyncapi_imports" => {
116                            asyncapi_imports = map.next_value()?;
117                        }
118                        "supplements" => {
119                            supplements = map.next_value()?;
120                        }
121                        "libraries" => {
122                            libraries = map.next_value()?;
123                        }
124                        "components" => {
125                            components = map.next_value()?;
126                        }
127                        "eventTrees" => {
128                            event_trees_map = map.next_value()?;
129                        }
130                        "eventTree" => {
131                            event_tree_legacy = Some(map.next_value()?);
132                        }
133                        "faultTrees" => {
134                            fault_trees = map.next_value()?;
135                        }
136                        k if k.starts_with("x-") => {
137                            let val: serde_yaml::Value = map.next_value()?;
138                            extensions.insert(k.to_string(), val);
139                        }
140                        unknown => {
141                            return Err(Error::custom(format!(
142                                "unrecognized field '{}' in ETDL document; extension fields must start with 'x-'",
143                                unknown
144                            )));
145                        }
146                    }
147                }
148
149                let etdl = etdl.ok_or_else(|| Error::missing_field("etdl"))?;
150                let info = info.ok_or_else(|| Error::missing_field("info"))?;
151
152                let event_trees = match (event_trees_map.is_empty(), event_tree_legacy) {
153                    (true, Some(tree)) => {
154                        let mut map = BTreeMap::new();
155                        map.insert("default".to_string(), tree);
156                        map
157                    }
158                    (true, None) => {
159                        return Err(Error::custom(
160                            "at least one of 'eventTrees' or 'eventTree' (deprecated) must be present",
161                        ));
162                    }
163                    (false, None) => event_trees_map,
164                    (false, Some(_)) => {
165                        return Err(Error::custom(
166                            "both 'eventTrees' and 'eventTree' (deprecated) provided; use only 'eventTrees'",
167                        ));
168                    }
169                };
170
171                Ok(EtlDocument {
172                    etdl,
173                    info,
174                    asyncapi_imports,
175                    supplements,
176                    libraries,
177                    components,
178                    event_trees,
179                    fault_trees,
180                    extensions,
181                })
182            }
183        }
184
185        deserializer.deserialize_map(DocVisitor)
186    }
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct Info {
191    pub title: String,
192    pub version: String,
193    #[serde(deserialize_with = "deserialize_domain")]
194    pub domain: String,
195    #[serde(default)]
196    pub description: Option<String>,
197}
198
199fn deserialize_domain<'de, D>(deserializer: D) -> Result<String, D::Error>
200where
201    D: Deserializer<'de>,
202{
203    let s = String::deserialize(deserializer)?;
204    if s.is_empty() || !s.chars().next().unwrap().is_ascii_alphabetic() {
205        return Err(serde::de::Error::custom(
206            "domain must match ^[A-Za-z][A-Za-z0-9]*$",
207        ));
208    }
209    if s.chars().any(|c| !c.is_ascii_alphanumeric()) {
210        return Err(serde::de::Error::custom(
211            "domain must match ^[A-Za-z][A-Za-z0-9]*$",
212        ));
213    }
214    Ok(s)
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize, Default)]
218pub struct Components {
219    #[serde(default)]
220    pub barriers: Option<BTreeMap<String, Barrier>>,
221    #[serde(default)]
222    pub operations: Option<BTreeMap<String, Operation>>,
223    #[serde(default)]
224    pub gates: Option<BTreeMap<String, Gate>>,
225    #[serde(default)]
226    pub basic_events: Option<BTreeMap<String, BasicEvent>>,
227    /// Inline Message Schema Objects (Section 5.4.1), resolved by a Message
228    /// Reference of the form `#/components/messages/<id>` (Section 5.3.4).
229    #[serde(default)]
230    pub messages: Option<BTreeMap<String, Message>>,
231}
232
233/// A Message Schema Object (Section 5.4.1): an inline, AsyncAPI 3.0 Message
234/// Object-shaped definition, used when a document has no `asyncapi_imports`
235/// (or a specific message isn't covered by one) and instead defines its
236/// message shape directly under `components.messages`.
237#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct Message {
239    #[serde(default)]
240    pub name: Option<String>,
241    /// A JSON Schema describing the message payload.
242    pub payload: serde_yaml::Value,
243    /// A JSON Schema describing the message headers.
244    #[serde(default)]
245    pub headers: Option<serde_yaml::Value>,
246}
247
248/// An ETDL **library document**: a reusable component catalog (the standard
249/// library, a domain library, or a user library), as opposed to an
250/// [`EtlDocument`] (a system: event trees, fault trees, an actual model).
251///
252/// A library has no event trees or fault trees of its own — it only
253/// *provides* named, reusable `components` that an importing [`EtlDocument`]
254/// can reference. It is parsed with the same YAML conventions and the exact
255/// same [`Components`]/[`BasicEvent`]/[`Gate`] types as an ordinary document
256/// (see `etdl_parser::parse_library_document`), just under a lighter
257/// top-level schema that does not require an event tree to be present.
258#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct LibraryDocument {
260    /// The ETDL language-version dialect this library's syntax targets.
261    /// Checked the same way as [`EtlDocument::etdl`].
262    pub etdl: String,
263    pub library: LibraryInfo,
264    #[serde(default)]
265    pub components: Components,
266}
267
268/// A library's own identity: name, version, and description. `version` is a
269/// distinct axis from `etdl` (the language dialect) and from any crate
270/// version — see `docs/reference/standard-library.md`.
271#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct LibraryInfo {
273    /// The library's dotted name, e.g. `std.events`. Must match the `name`
274    /// an importing document declares in `libraries:`.
275    pub name: String,
276    /// The library's own version, e.g. `"1.0"`.
277    pub version: String,
278    #[serde(default)]
279    pub description: Option<String>,
280    /// Other libraries this one depends on. Resolved transitively with
281    /// cycle detection (`stdlib::LibraryError::Cyclic`); kept simple
282    /// deliberately (no version solver, no diamond-dependency merge logic).
283    #[serde(default, rename = "dependsOn")]
284    pub depends_on: Vec<LibraryImport>,
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct EventTree {
289    #[serde(rename = "initiatingEvent")]
290    pub initiating_event: InitiatingEvent,
291    pub nodes: BTreeMap<NodeId, Node>,
292    #[serde(default)]
293    pub description: Option<String>,
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize)]
297pub struct InitiatingEvent {
298    pub id: String,
299    pub message: MessageRef,
300    pub next: NodeId,
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize)]
304#[serde(tag = "type")]
305pub enum Node {
306    #[serde(rename = "barrier")]
307    Barrier(Barrier),
308    #[serde(rename = "operation")]
309    Operation(Operation),
310    #[serde(rename = "consequence")]
311    Consequence(Consequence),
312}
313
314impl Node {
315    pub fn node_type(&self) -> &str {
316        match self {
317            Node::Barrier(_) => "barrier",
318            Node::Operation(_) => "operation",
319            Node::Consequence(_) => "consequence",
320        }
321    }
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct Barrier {
326    pub branches: Vec<Branch>,
327    #[serde(default)]
328    pub description: Option<String>,
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize)]
332pub struct Branch {
333    pub outcome: String,
334
335    #[serde(deserialize_with = "deserialize_condition")]
336    pub condition: Condition,
337
338    #[serde(default, deserialize_with = "deserialize_optional_f64")]
339    pub probability: Option<f64>,
340
341    #[serde(default, alias = "probabilityOfSuccess")]
342    pub probability_of_success: Option<f64>,
343
344    #[serde(default, alias = "probabilityOfFailure")]
345    pub probability_of_failure: Option<f64>,
346
347    #[serde(default, alias = "probabilitySource")]
348    pub probability_source: Option<InternalRef>,
349
350    pub next: NodeId,
351}
352
353fn deserialize_condition<'de, D>(deserializer: D) -> Result<Condition, D::Error>
354where
355    D: Deserializer<'de>,
356{
357    let s = String::deserialize(deserializer)?;
358    parse_condition(&s).map_err(serde::de::Error::custom)
359}
360
361fn deserialize_optional_f64<'de, D>(deserializer: D) -> Result<Option<f64>, D::Error>
362where
363    D: Deserializer<'de>,
364{
365    Option::<f64>::deserialize(deserializer)
366}
367
368impl Branch {
369    pub fn effective_probability(&self) -> Option<f64> {
370        self.probability
371            .or(self.probability_of_success)
372            .or(self.probability_of_failure)
373    }
374
375    pub fn has_probability_source(&self) -> bool {
376        self.probability_source.is_some()
377            || self.probability.is_some()
378            || self.probability_of_success.is_some()
379            || self.probability_of_failure.is_some()
380    }
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize)]
384pub struct Operation {
385    #[serde(default = "default_action")]
386    pub action: ActionKind,
387    pub handler: String,
388
389    #[serde(default)]
390    pub emits: Option<MessageRef>,
391
392    pub next: NodeId,
393
394    #[serde(default, alias = "onFailure")]
395    pub on_failure: Option<NodeId>,
396
397    #[serde(default, alias = "onFailureProbabilitySource")]
398    pub on_failure_probability_source: Option<InternalRef>,
399
400    #[serde(default, alias = "retryPolicy")]
401    pub retry_policy: Option<RetryPolicy>,
402
403    #[serde(default, alias = "timeoutMs")]
404    pub timeout_ms: Option<u64>,
405
406    #[serde(default)]
407    pub description: Option<String>,
408}
409
410fn default_action() -> ActionKind {
411    ActionKind::Execute
412}
413
414#[derive(Debug, Clone, Serialize, Deserialize)]
415pub enum ActionKind {
416    #[serde(rename = "execute")]
417    Execute,
418}
419
420#[derive(Debug, Clone, Serialize, Deserialize)]
421pub struct RetryPolicy {
422    #[serde(default = "default_max_attempts", alias = "maxAttempts")]
423    pub max_attempts: u32,
424
425    #[serde(default = "default_backoff_ms", alias = "backoffMs")]
426    pub backoff_ms: u64,
427
428    #[serde(default, alias = "backoffStrategy")]
429    pub backoff_strategy: Option<BackoffStrategy>,
430}
431
432fn default_max_attempts() -> u32 {
433    1
434}
435fn default_backoff_ms() -> u64 {
436    0
437}
438
439#[derive(Debug, Clone, Serialize, Deserialize, Default)]
440pub enum BackoffStrategy {
441    #[serde(rename = "fixed")]
442    #[default]
443    Fixed,
444    #[serde(rename = "exponential")]
445    Exponential,
446}
447
448#[derive(Debug, Clone, Serialize, Deserialize)]
449pub struct Consequence {
450    #[serde(rename = "operation")]
451    pub consequence_operation: ConsequenceOperation,
452    #[serde(default)]
453    pub channel: Option<ChannelRef>,
454    #[serde(default)]
455    pub message: Option<MessageRef>,
456    #[serde(default)]
457    pub description: Option<String>,
458}
459
460#[derive(Debug, Clone, Serialize, Deserialize)]
461pub enum ConsequenceOperation {
462    #[serde(rename = "send")]
463    Send,
464    #[serde(rename = "terminate")]
465    Terminate,
466}
467
468#[derive(Debug, Clone, Serialize, Deserialize)]
469pub struct FaultTree {
470    #[serde(rename = "topEvent")]
471    pub top_event: TopEvent,
472
473    #[serde(default)]
474    pub gates: Option<BTreeMap<GateId, Gate>>,
475
476    #[serde(rename = "basicEvents")]
477    pub basic_events: BTreeMap<String, BasicEvent>,
478
479    #[serde(default)]
480    pub transfers: Option<BTreeMap<String, TransferNode>>,
481
482    #[serde(default)]
483    pub description: Option<String>,
484}
485
486#[derive(Debug, Clone, Serialize, Deserialize)]
487pub struct TopEvent {
488    pub id: String,
489    pub description: String,
490    #[serde(default)]
491    pub message: Option<MessageRef>,
492    #[serde(rename = "rootCause")]
493    pub root_cause: FaultTreeNodeRef,
494}
495
496#[derive(Debug, Clone, Serialize, Deserialize)]
497pub struct Gate {
498    #[serde(rename = "type")]
499    pub gate_type: GateType,
500    pub inputs: Vec<FaultTreeNodeRef>,
501    #[serde(default)]
502    pub k: Option<u32>,
503    #[serde(default)]
504    pub description: Option<String>,
505    #[serde(default, alias = "inhibitCondition")]
506    pub inhibit_condition: Option<String>,
507}
508
509#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
510pub enum GateType {
511    #[serde(rename = "AND")]
512    And,
513    #[serde(rename = "OR")]
514    Or,
515    #[serde(rename = "NOT")]
516    Not,
517    #[serde(rename = "XOR")]
518    Xor,
519    #[serde(rename = "VOTING")]
520    Voting,
521    #[serde(rename = "INHIBIT")]
522    Inhibit,
523    #[serde(rename = "PRIORITY_AND")]
524    PriorityAnd,
525}
526
527#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
528pub enum BasicEventType {
529    #[serde(rename = "basic")]
530    Basic,
531    #[serde(rename = "house")]
532    House,
533    #[serde(rename = "undeveloped")]
534    Undeveloped,
535    #[serde(rename = "conditional")]
536    Conditional,
537}
538
539#[derive(Debug, Clone, Serialize, Deserialize)]
540pub struct TransferNode {
541    pub target: String,
542    #[serde(default)]
543    pub label: Option<String>,
544}
545
546#[derive(Debug, Clone, Serialize, Deserialize)]
547pub struct BasicEvent {
548    pub description: String,
549    #[serde(default)]
550    pub probability: Option<f64>,
551    #[serde(default, alias = "failureRate")]
552    pub failure_rate: Option<f64>,
553    #[serde(default, alias = "missionTime")]
554    pub mission_time: Option<f64>,
555    #[serde(default)]
556    pub undeveloped: Option<bool>,
557    #[serde(default, alias = "eventType")]
558    pub event_type: Option<BasicEventType>,
559    #[serde(default)]
560    pub message: Option<MessageRef>,
561    /// `x-*` extension fields (core Section 11), preserved as raw values.
562    #[serde(default, flatten)]
563    pub extensions: BTreeMap<String, serde_yaml::Value>,
564}
565
566#[derive(Debug, Clone)]
567pub struct ExternalRef {
568    pub alias: String,
569    pub pointer: String,
570}
571
572#[derive(Debug, Clone)]
573pub struct InternalRef {
574    pub pointer: String,
575}
576
577pub type FaultTreeNodeRef = String;
578
579impl Serialize for ExternalRef {
580    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
581    where
582        S: Serializer,
583    {
584        serializer.serialize_str(&format!("{}#{}", self.alias, self.pointer))
585    }
586}
587
588impl<'de> Deserialize<'de> for ExternalRef {
589    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
590    where
591        D: Deserializer<'de>,
592    {
593        let s = String::deserialize(deserializer)?;
594        parse_external_ref(&s).map_err(serde::de::Error::custom)
595    }
596}
597
598fn parse_external_ref(s: &str) -> Result<ExternalRef, String> {
599    if let Some(hash_pos) = s.find('#') {
600        let alias = &s[..hash_pos];
601        let pointer = &s[hash_pos..];
602        if alias.is_empty() {
603            if pointer.starts_with("#/") {
604                return Err(format!(
605                    "bare JSON Pointer '{}' without import alias; use InternalRef for same-document references",
606                    pointer
607                ));
608            }
609            return Err("empty alias in external reference".to_string());
610        }
611        if alias
612            .chars()
613            .any(|c| !c.is_ascii_alphanumeric() && c != '_')
614        {
615            return Err(format!("invalid import alias '{}'", alias));
616        }
617        Ok(ExternalRef {
618            alias: alias.to_string(),
619            pointer: pointer.to_string(),
620        })
621    } else {
622        Err(format!("no '#' found in external reference '{}'", s))
623    }
624}
625
626impl Serialize for InternalRef {
627    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
628    where
629        S: Serializer,
630    {
631        serializer.serialize_str(&self.pointer)
632    }
633}
634
635impl<'de> Deserialize<'de> for InternalRef {
636    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
637    where
638        D: Deserializer<'de>,
639    {
640        let s = String::deserialize(deserializer)?;
641        if !s.starts_with("#/") {
642            return Err(serde::de::Error::custom(format!(
643                "invalid internal reference '{}'; must start with '#/'",
644                s
645            )));
646        }
647        Ok(InternalRef { pointer: s })
648    }
649}
650
651impl ExternalRef {
652    pub fn as_string(&self) -> String {
653        format!("{}#{}", self.alias, self.pointer)
654    }
655}
656
657impl InternalRef {
658    pub fn as_string(&self) -> String {
659        self.pointer.clone()
660    }
661}
662
663#[derive(Debug, Clone)]
664pub enum ParsedReference {
665    External(ExternalRef),
666    Internal(InternalRef),
667}
668
669pub fn parse_reference(s: &str) -> Result<ParsedReference, String> {
670    if s.starts_with("#/") {
671        Ok(ParsedReference::Internal(InternalRef {
672            pointer: s.to_string(),
673        }))
674    } else if let Some(hash_pos) = s.find('#') {
675        let alias = &s[..hash_pos];
676        let pointer = &s[hash_pos..];
677        if alias.is_empty() || pointer.is_empty() || !pointer.starts_with('#') {
678            return Err(format!("invalid reference syntax: '{}'", s));
679        }
680        parse_external_ref(s).map(ParsedReference::External)
681    } else {
682        Err(format!(
683            "reference '{}' matches neither external nor internal format",
684            s
685        ))
686    }
687}
688
689/// A Message Reference (Section 5.3.4): either an External Reference into a
690/// loaded `asyncapi_imports` document, or an Internal Reference of the form
691/// `#/components/messages/<id>` resolving to an inline Message Schema
692/// Object (Section 5.4.1).
693#[derive(Debug, Clone)]
694pub enum MessageRef {
695    External(ExternalRef),
696    Internal(InternalRef),
697}
698
699impl Serialize for MessageRef {
700    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
701    where
702        S: Serializer,
703    {
704        match self {
705            MessageRef::External(r) => r.serialize(serializer),
706            MessageRef::Internal(r) => r.serialize(serializer),
707        }
708    }
709}
710
711impl<'de> Deserialize<'de> for MessageRef {
712    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
713    where
714        D: Deserializer<'de>,
715    {
716        let s = String::deserialize(deserializer)?;
717        match parse_reference(&s).map_err(serde::de::Error::custom)? {
718            ParsedReference::External(r) => Ok(MessageRef::External(r)),
719            ParsedReference::Internal(r) => Ok(MessageRef::Internal(r)),
720        }
721    }
722}
723
724impl MessageRef {
725    pub fn as_string(&self) -> String {
726        match self {
727            MessageRef::External(r) => r.as_string(),
728            MessageRef::Internal(r) => r.as_string(),
729        }
730    }
731}
732
733/// A Channel Reference (Section 5.3.5): an External Reference (required
734/// whenever the document declares any `asyncapi_imports`), or — only when
735/// the document has no `asyncapi_imports` at all — a bare channel-name
736/// string. Never an Internal Reference: channel addressing has no inline
737/// schema counterpart to `components.messages`.
738#[derive(Debug, Clone)]
739pub enum ChannelRef {
740    External(ExternalRef),
741    Bare(String),
742}
743
744impl Serialize for ChannelRef {
745    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
746    where
747        S: Serializer,
748    {
749        match self {
750            ChannelRef::External(r) => r.serialize(serializer),
751            ChannelRef::Bare(s) => serializer.serialize_str(s),
752        }
753    }
754}
755
756impl<'de> Deserialize<'de> for ChannelRef {
757    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
758    where
759        D: Deserializer<'de>,
760    {
761        let s = String::deserialize(deserializer)?;
762        if s.starts_with("#/") {
763            return Err(serde::de::Error::custom(format!(
764                "'{}' is not a valid Channel Reference: internal pointers are not supported for channel (Section 5.3.5)",
765                s
766            )));
767        }
768        if s.contains('#') {
769            parse_external_ref(&s)
770                .map(ChannelRef::External)
771                .map_err(serde::de::Error::custom)
772        } else {
773            Ok(ChannelRef::Bare(s))
774        }
775    }
776}
777
778impl ChannelRef {
779    pub fn as_string(&self) -> String {
780        match self {
781            ChannelRef::External(r) => r.as_string(),
782            ChannelRef::Bare(s) => s.clone(),
783        }
784    }
785}