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}