Skip to main content

etdl_parser/
asyncapi.rs

1use crate::ast::ExternalRef;
2use crate::jsonptr;
3use serde_json::Value;
4use std::collections::BTreeMap;
5use std::fs;
6use std::path::{Path, PathBuf};
7
8pub struct AsyncApiRegistry {
9    documents: BTreeMap<String, AsyncApiDocument>,
10}
11
12struct AsyncApiDocument {
13    _path: PathBuf,
14    root: Value,
15}
16
17impl AsyncApiRegistry {
18    pub fn new() -> Self {
19        AsyncApiRegistry {
20            documents: BTreeMap::new(),
21        }
22    }
23
24    pub fn load(
25        &mut self,
26        alias: &str,
27        location: &str,
28        base_dir: &Path,
29    ) -> Result<(), String> {
30        let resolved_path = resolve_location(location, base_dir)?;
31        let content =
32            fs::read_to_string(&resolved_path).map_err(|e| {
33                format!("cannot read AsyncAPI doc '{}': {}", resolved_path.display(), e)
34            })?;
35
36        let root: Value = if resolved_path.extension().map_or(false, |ext| ext == "json") {
37            serde_json::from_str(&content).map_err(|e| {
38                format!(
39                    "invalid JSON in AsyncAPI doc '{}': {}",
40                    resolved_path.display(),
41                    e
42                )
43            })?
44        } else {
45            serde_yaml::from_str(&content).map_err(|e| {
46                format!(
47                    "invalid YAML in AsyncAPI doc '{}': {}",
48                    resolved_path.display(),
49                    e
50                )
51            })?
52        };
53
54        self.documents.insert(
55            alias.to_string(),
56            AsyncApiDocument {
57                _path: resolved_path,
58                root,
59            },
60        );
61        Ok(())
62    }
63
64    pub fn resolve(&self, ext_ref: &ExternalRef) -> Result<&Value, String> {
65        let doc = self.documents.get(&ext_ref.alias).ok_or_else(|| {
66            format!(
67                "import alias '{}' not found in loaded AsyncAPI documents",
68                ext_ref.alias
69            )
70        })?;
71
72        let pointer = &ext_ref.pointer;
73        jsonptr::resolve_json_pointer(&doc.root, pointer).ok_or_else(|| {
74            format!(
75                "JSON Pointer '{}' does not resolve in AsyncAPI document '{}'",
76                pointer, ext_ref.alias
77            )
78        })
79    }
80
81    pub fn resolve_ref(&self, alias: &str, pointer: &str) -> Result<&Value, String> {
82        let doc = self.documents.get(alias).ok_or_else(|| {
83            format!(
84                "import alias '{}' not found in loaded AsyncAPI documents",
85                alias
86            )
87        })?;
88
89        jsonptr::resolve_json_pointer(&doc.root, pointer).ok_or_else(|| {
90            format!(
91                "JSON Pointer '{}' does not resolve in AsyncAPI document '{}'",
92                pointer, alias
93            )
94        })
95    }
96
97    pub fn get_schema_for_path(
98        &self,
99        ext_ref: &ExternalRef,
100        path_segments: &[crate::ecel::PathSegment],
101    ) -> Result<Option<Value>, String> {
102        let message_value = self.resolve(ext_ref)?;
103
104        let payload_schema = message_value
105            .get("payload")
106            .or_else(|| message_value.get("schema"));
107
108        let Some(schema) = payload_schema else {
109            return Ok(None);
110        };
111
112        let resolved = resolve_schema_path(schema, path_segments);
113        Ok(resolved)
114    }
115}
116
117fn resolve_schema_path(
118    schema: &Value,
119    segments: &[crate::ecel::PathSegment],
120) -> Option<Value> {
121    if segments.is_empty() {
122        return Some(schema.clone());
123    }
124
125    let first = &segments[0];
126
127    match first {
128        crate::ecel::PathSegment::Field(name) => {
129            if name == "message" && segments.len() > 1 {
130                return resolve_schema_path(schema, &segments[1..]);
131            }
132
133            let field_schema = resolve_field(schema, name)?;
134            if segments.len() == 1 {
135                Some(field_schema.clone())
136            } else {
137                resolve_schema_path(&field_schema, &segments[1..])
138            }
139        }
140        crate::ecel::PathSegment::Wildcard => {
141            let items_schema = resolve_array_items(schema)?;
142            if segments.len() == 1 {
143                Some(items_schema.clone())
144            } else {
145                resolve_schema_path(&items_schema, &segments[1..])
146            }
147        }
148        crate::ecel::PathSegment::Index(_) => {
149            let items_schema = resolve_array_items(schema)?;
150            if segments.len() == 1 {
151                Some(items_schema.clone())
152            } else {
153                resolve_schema_path(&items_schema, &segments[1..])
154            }
155        }
156        crate::ecel::PathSegment::QuotedKey(name) => {
157            let field_schema = resolve_field(schema, name)?;
158            if segments.len() == 1 {
159                Some(field_schema.clone())
160            } else {
161                resolve_schema_path(&field_schema, &segments[1..])
162            }
163        }
164    }
165}
166
167fn resolve_field(schema: &Value, name: &str) -> Option<Value> {
168    if let Some(properties) = schema.get("properties") {
169        if let Some(field) = properties.get(name) {
170            return Some(field.clone());
171        }
172    }
173
174    if let Some(obj) = schema.as_object() {
175        if let Some(field) = obj.get(name) {
176            return Some(field.clone());
177        }
178    }
179
180    None
181}
182
183fn resolve_array_items(schema: &Value) -> Option<Value> {
184    if let Some(items) = schema.get("items") {
185        return Some(items.clone());
186    }
187
188    if let Some(type_val) = schema.get("type") {
189        if type_val.as_str() == Some("array") {
190            if let Some(items) = schema.get("items") {
191                return Some(items.clone());
192            }
193        }
194    }
195
196    None
197}
198
199fn resolve_location(location: &str, base_dir: &Path) -> Result<PathBuf, String> {
200    if location.starts_with("http://") || location.starts_with("https://") {
201        return Err(format!(
202            "remote AsyncAPI imports not supported in this version: '{}'",
203            location
204        ));
205    }
206
207    let path = Path::new(location);
208    if path.is_absolute() {
209        Ok(path.to_path_buf())
210    } else {
211        Ok(base_dir.join(path))
212    }
213}