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 load_from_content(
65        &mut self,
66        alias: &str,
67        content: &str,
68        is_json: bool,
69    ) -> Result<(), String> {
70        let root: Value = if is_json {
71            serde_json::from_str(content).map_err(|e| {
72                format!("invalid JSON in AsyncAPI doc '{}': {}", alias, e)
73            })?
74        } else {
75            serde_yaml::from_str(content).map_err(|e| {
76                format!("invalid YAML in AsyncAPI doc '{}': {}", alias, e)
77            })?
78        };
79
80        self.documents.insert(
81            alias.to_string(),
82            AsyncApiDocument {
83                _path: PathBuf::from(alias),
84                root,
85            },
86        );
87        Ok(())
88    }
89
90    pub fn resolve(&self, ext_ref: &ExternalRef) -> Result<&Value, String> {
91        let doc = self.documents.get(&ext_ref.alias).ok_or_else(|| {
92            format!(
93                "import alias '{}' not found in loaded AsyncAPI documents",
94                ext_ref.alias
95            )
96        })?;
97
98        let pointer = &ext_ref.pointer;
99        jsonptr::resolve_json_pointer(&doc.root, pointer).ok_or_else(|| {
100            format!(
101                "JSON Pointer '{}' does not resolve in AsyncAPI document '{}'",
102                pointer, ext_ref.alias
103            )
104        })
105    }
106
107    pub fn resolve_ref(&self, alias: &str, pointer: &str) -> Result<&Value, String> {
108        let doc = self.documents.get(alias).ok_or_else(|| {
109            format!(
110                "import alias '{}' not found in loaded AsyncAPI documents",
111                alias
112            )
113        })?;
114
115        jsonptr::resolve_json_pointer(&doc.root, pointer).ok_or_else(|| {
116            format!(
117                "JSON Pointer '{}' does not resolve in AsyncAPI document '{}'",
118                pointer, alias
119            )
120        })
121    }
122
123    pub fn get_schema_for_path(
124        &self,
125        ext_ref: &ExternalRef,
126        path_segments: &[crate::ecel::PathSegment],
127    ) -> Result<Option<Value>, String> {
128        let message_value = self.resolve(ext_ref)?;
129
130        let payload_schema = message_value
131            .get("payload")
132            .or_else(|| message_value.get("schema"));
133
134        let Some(schema) = payload_schema else {
135            return Ok(None);
136        };
137
138        let resolved = resolve_schema_path(schema, path_segments);
139        Ok(resolved)
140    }
141}
142
143fn resolve_schema_path(
144    schema: &Value,
145    segments: &[crate::ecel::PathSegment],
146) -> Option<Value> {
147    if segments.is_empty() {
148        return Some(schema.clone());
149    }
150
151    let first = &segments[0];
152
153    match first {
154        crate::ecel::PathSegment::Field(name) => {
155            if name == "message" && segments.len() > 1 {
156                return resolve_schema_path(schema, &segments[1..]);
157            }
158
159            let field_schema = resolve_field(schema, name)?;
160            if segments.len() == 1 {
161                Some(field_schema.clone())
162            } else {
163                resolve_schema_path(&field_schema, &segments[1..])
164            }
165        }
166        crate::ecel::PathSegment::Wildcard => {
167            let items_schema = resolve_array_items(schema)?;
168            if segments.len() == 1 {
169                Some(items_schema.clone())
170            } else {
171                resolve_schema_path(&items_schema, &segments[1..])
172            }
173        }
174        crate::ecel::PathSegment::Index(_) => {
175            let items_schema = resolve_array_items(schema)?;
176            if segments.len() == 1 {
177                Some(items_schema.clone())
178            } else {
179                resolve_schema_path(&items_schema, &segments[1..])
180            }
181        }
182        crate::ecel::PathSegment::QuotedKey(name) => {
183            let field_schema = resolve_field(schema, name)?;
184            if segments.len() == 1 {
185                Some(field_schema.clone())
186            } else {
187                resolve_schema_path(&field_schema, &segments[1..])
188            }
189        }
190    }
191}
192
193fn resolve_field(schema: &Value, name: &str) -> Option<Value> {
194    if let Some(properties) = schema.get("properties") {
195        if let Some(field) = properties.get(name) {
196            return Some(field.clone());
197        }
198    }
199
200    if let Some(obj) = schema.as_object() {
201        if let Some(field) = obj.get(name) {
202            return Some(field.clone());
203        }
204    }
205
206    None
207}
208
209fn resolve_array_items(schema: &Value) -> Option<Value> {
210    if let Some(items) = schema.get("items") {
211        return Some(items.clone());
212    }
213
214    if let Some(type_val) = schema.get("type") {
215        if type_val.as_str() == Some("array") {
216            if let Some(items) = schema.get("items") {
217                return Some(items.clone());
218            }
219        }
220    }
221
222    None
223}
224
225fn resolve_location(location: &str, base_dir: &Path) -> Result<PathBuf, String> {
226    if location.starts_with("http://") || location.starts_with("https://") {
227        return Err(format!(
228            "remote AsyncAPI imports not supported in this version: '{}'",
229            location
230        ));
231    }
232
233    let path = Path::new(location);
234    if path.is_absolute() {
235        Ok(path.to_path_buf())
236    } else {
237        Ok(base_dir.join(path))
238    }
239}