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}