1use serde_json::{Map, Value};
2
3use super::{DeclEdge, DeclSymbol, DeclareError, DeclareSpec, EdgeKind, Lang};
4use crate::core::moniker::Moniker;
5
6pub fn parse_spec(value: &Value) -> Result<DeclareSpec, DeclareError> {
7 let obj = value.as_object().ok_or(DeclareError::NotAnObject("spec"))?;
8
9 let lang_str = req_str(obj, "$", "lang")?;
10 let lang =
11 Lang::from_tag(lang_str).ok_or_else(|| DeclareError::UnknownLang(lang_str.to_string()))?;
12
13 let root_str = req_str(obj, "$", "root")?;
14 let root = parse_moniker_uri(root_str, "$.root")?;
15
16 let symbols_val = obj.get("symbols").ok_or(DeclareError::MissingField {
17 path: "$".to_string(),
18 field: "symbols",
19 })?;
20 let symbols_arr = symbols_val.as_array().ok_or(DeclareError::InvalidType {
21 path: "$.symbols".to_string(),
22 expected: "array",
23 })?;
24 let symbols: Vec<DeclSymbol> = symbols_arr
25 .iter()
26 .enumerate()
27 .map(|(i, v)| parse_symbol(v, &format!("$.symbols[{i}]"), lang))
28 .collect::<Result<_, _>>()?;
29
30 let edges = match obj.get("edges") {
31 None | Some(Value::Null) => Vec::new(),
32 Some(v) => {
33 let arr = v.as_array().ok_or(DeclareError::InvalidType {
34 path: "$.edges".to_string(),
35 expected: "array",
36 })?;
37 arr.iter()
38 .enumerate()
39 .map(|(i, ev)| parse_edge(ev, &format!("$.edges[{i}]")))
40 .collect::<Result<_, _>>()?
41 }
42 };
43
44 Ok(DeclareSpec {
45 root,
46 lang,
47 symbols,
48 edges,
49 })
50}
51
52fn parse_symbol(value: &Value, path: &str, lang: Lang) -> Result<DeclSymbol, DeclareError> {
53 let obj = value.as_object().ok_or(DeclareError::InvalidType {
54 path: path.to_string(),
55 expected: "object",
56 })?;
57
58 let moniker_str = req_str(obj, path, "moniker")?;
59 let moniker = parse_moniker_uri(moniker_str, &format!("{path}.moniker"))?;
60
61 let kind = req_str(obj, path, "kind")?.to_string();
62 if !crate::lang::kinds::INTERNAL_KINDS.contains(&kind.as_str())
63 && !lang.allowed_kinds().contains(&kind.as_str())
64 {
65 return Err(DeclareError::KindNotInProfile {
66 lang: lang.tag(),
67 kind,
68 });
69 }
70
71 let parent_str = req_str(obj, path, "parent")?;
72 let parent = parse_moniker_uri(parent_str, &format!("{path}.parent"))?;
73
74 let visibility = match obj.get("visibility") {
75 None | Some(Value::Null) => None,
76 Some(v) => {
77 let s = v.as_str().ok_or(DeclareError::InvalidType {
78 path: format!("{path}.visibility"),
79 expected: "string",
80 })?;
81 if !lang.ignores_visibility() && !lang.allowed_visibilities().contains(&s) {
82 return Err(DeclareError::VisibilityNotInProfile {
83 lang: lang.tag(),
84 visibility: s.to_string(),
85 });
86 }
87 Some(s.to_string())
88 }
89 };
90
91 let signature = match obj.get("signature") {
92 None | Some(Value::Null) => None,
93 Some(v) => Some(
94 v.as_str()
95 .ok_or(DeclareError::InvalidType {
96 path: format!("{path}.signature"),
97 expected: "string",
98 })?
99 .to_string(),
100 ),
101 };
102
103 Ok(DeclSymbol {
104 moniker,
105 kind,
106 parent,
107 visibility,
108 signature,
109 })
110}
111
112fn parse_edge(value: &Value, path: &str) -> Result<DeclEdge, DeclareError> {
113 let obj = value.as_object().ok_or(DeclareError::InvalidType {
114 path: path.to_string(),
115 expected: "object",
116 })?;
117
118 let from_str = req_str(obj, path, "from")?;
119 let from = parse_moniker_uri(from_str, &format!("{path}.from"))?;
120
121 let kind_str = req_str(obj, path, "kind")?;
122 let kind = EdgeKind::from_tag(kind_str)
123 .ok_or_else(|| DeclareError::UnknownEdgeKind(kind_str.to_string()))?;
124
125 let to_str = req_str(obj, path, "to")?;
126 let to = parse_moniker_uri(to_str, &format!("{path}.to"))?;
127
128 Ok(DeclEdge { from, kind, to })
129}
130
131fn req_str<'a>(
132 obj: &'a Map<String, Value>,
133 path: &str,
134 field: &'static str,
135) -> Result<&'a str, DeclareError> {
136 let v = obj.get(field).ok_or(DeclareError::MissingField {
137 path: path.to_string(),
138 field,
139 })?;
140 v.as_str().ok_or(DeclareError::InvalidType {
141 path: format!("{path}.{field}"),
142 expected: "string",
143 })
144}
145
146fn parse_moniker_uri(uri: &str, path: &str) -> Result<Moniker, DeclareError> {
147 if !uri.contains("://") {
148 return Err(DeclareError::InvalidMoniker {
149 path: path.to_string(),
150 value: uri.to_string(),
151 reason: "URI must contain `://`".to_string(),
152 });
153 }
154 super::parse_moniker_uri(uri).map_err(|e| DeclareError::InvalidMoniker {
155 path: path.to_string(),
156 value: uri.to_string(),
157 reason: e.to_string(),
158 })
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use serde_json::json;
165
166 fn minimal_spec() -> Value {
167 json!({
168 "root": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo",
169 "lang": "java",
170 "symbols": [
171 {
172 "moniker": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo/class:Foo",
173 "kind": "class",
174 "parent": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo",
175 "visibility": "public"
176 }
177 ]
178 })
179 }
180
181 #[test]
182 fn parses_minimal_java_spec() {
183 let s = parse_spec(&minimal_spec()).unwrap();
184 assert_eq!(s.lang, Lang::Java);
185 assert_eq!(s.symbols.len(), 1);
186 assert_eq!(s.symbols[0].kind, "class");
187 assert!(s.edges.is_empty());
188 }
189
190 #[test]
191 fn rejects_missing_root() {
192 let mut v = minimal_spec();
193 v.as_object_mut().unwrap().remove("root");
194 let err = parse_spec(&v).unwrap_err();
195 assert!(matches!(
196 err,
197 DeclareError::MissingField { field, .. } if field == "root"
198 ));
199 }
200
201 #[test]
202 fn rejects_missing_lang() {
203 let mut v = minimal_spec();
204 v.as_object_mut().unwrap().remove("lang");
205 let err = parse_spec(&v).unwrap_err();
206 assert!(matches!(
207 err,
208 DeclareError::MissingField { field, .. } if field == "lang"
209 ));
210 }
211
212 #[test]
213 fn rejects_unknown_lang() {
214 let v = json!({
215 "root": "code+moniker://app/foo:bar",
216 "lang": "cobol",
217 "symbols": []
218 });
219 let err = parse_spec(&v).unwrap_err();
220 assert!(matches!(err, DeclareError::UnknownLang(s) if s == "cobol"));
221 }
222
223 #[test]
224 fn accepts_internal_kinds_comment_local_param_module() {
225 let v = json!({
226 "root": "code+moniker://app/lang:rs/module:foo",
227 "lang": "rs",
228 "symbols": [
229 { "moniker": "code+moniker://app/lang:rs/module:foo/comment:128",
230 "kind": "comment",
231 "parent": "code+moniker://app/lang:rs/module:foo" },
232 { "moniker": "code+moniker://app/lang:rs/module:foo/fn:run()",
233 "kind": "fn",
234 "parent": "code+moniker://app/lang:rs/module:foo" },
235 { "moniker": "code+moniker://app/lang:rs/module:foo/fn:run()/local:x",
236 "kind": "local",
237 "parent": "code+moniker://app/lang:rs/module:foo/fn:run()" },
238 { "moniker": "code+moniker://app/lang:rs/module:foo/fn:run()/param:y",
239 "kind": "param",
240 "parent": "code+moniker://app/lang:rs/module:foo/fn:run()" }
241 ]
242 });
243 let spec = parse_spec(&v).expect("internal kinds must round-trip through declare");
244 assert_eq!(spec.symbols.len(), 4);
245 }
246
247 #[test]
248 fn rejects_kind_outside_profile() {
249 let v = json!({
250 "root": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo",
251 "lang": "java",
252 "symbols": [{
253 "moniker": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo/trait:Foo",
254 "kind": "trait",
255 "parent": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo"
256 }]
257 });
258 let err = parse_spec(&v).unwrap_err();
259 assert!(matches!(err, DeclareError::KindNotInProfile { ref kind, .. } if kind == "trait"));
260 }
261
262 #[test]
263 fn rejects_visibility_outside_profile() {
264 let v = json!({
265 "root": "code+moniker://app/srcset:main/lang:ts/dir:src/module:foo",
266 "lang": "ts",
267 "symbols": [{
268 "moniker": "code+moniker://app/srcset:main/lang:ts/dir:src/module:foo/class:Bar",
269 "kind": "class",
270 "parent": "code+moniker://app/srcset:main/lang:ts/dir:src/module:foo",
271 "visibility": "package"
272 }]
273 });
274 let err = parse_spec(&v).unwrap_err();
275 assert!(matches!(
276 err,
277 DeclareError::VisibilityNotInProfile { ref visibility, .. } if visibility == "package"
278 ));
279 }
280
281 #[test]
282 fn ts_accepts_module_visibility() {
283 let v = json!({
284 "root": "code+moniker://app/srcset:main/lang:ts/dir:src/module:foo",
285 "lang": "ts",
286 "symbols": [{
287 "moniker": "code+moniker://app/srcset:main/lang:ts/dir:src/module:foo/class:Bar",
288 "kind": "class",
289 "parent": "code+moniker://app/srcset:main/lang:ts/dir:src/module:foo",
290 "visibility": "module"
291 }]
292 });
293 assert!(parse_spec(&v).is_ok());
294 }
295
296 #[test]
297 fn python_accepts_module_visibility() {
298 let v = json!({
299 "root": "code+moniker://app/srcset:main/lang:python/package:acme/module:util",
300 "lang": "python",
301 "symbols": [{
302 "moniker": "code+moniker://app/srcset:main/lang:python/package:acme/module:util/class:Helper",
303 "kind": "class",
304 "parent": "code+moniker://app/srcset:main/lang:python/package:acme/module:util",
305 "visibility": "module"
306 }]
307 });
308 assert!(parse_spec(&v).is_ok());
309 }
310
311 #[test]
312 fn go_accepts_module_visibility_replaces_package() {
313 let v = json!({
314 "root": "code+moniker://app/srcset:main/lang:go/package:foo/module:svc",
315 "lang": "go",
316 "symbols": [{
317 "moniker": "code+moniker://app/srcset:main/lang:go/package:foo/module:svc/func:helper()",
318 "kind": "func",
319 "parent": "code+moniker://app/srcset:main/lang:go/package:foo/module:svc",
320 "visibility": "module"
321 }]
322 });
323 assert!(parse_spec(&v).is_ok());
324 }
325
326 #[test]
327 fn go_rejects_legacy_package_visibility() {
328 let v = json!({
329 "root": "code+moniker://app/srcset:main/lang:go/package:foo/module:svc",
330 "lang": "go",
331 "symbols": [{
332 "moniker": "code+moniker://app/srcset:main/lang:go/package:foo/module:svc/func:helper()",
333 "kind": "func",
334 "parent": "code+moniker://app/srcset:main/lang:go/package:foo/module:svc",
335 "visibility": "package"
336 }]
337 });
338 let err = parse_spec(&v).unwrap_err();
339 assert!(matches!(
340 err,
341 DeclareError::VisibilityNotInProfile { ref visibility, .. } if visibility == "package"
342 ));
343 }
344
345 #[test]
346 fn sql_ignores_visibility_field() {
347 let v = json!({
348 "root": "code+moniker://app/srcset:db/lang:sql/schema:public",
349 "lang": "sql",
350 "symbols": [{
351 "moniker": "code+moniker://app/srcset:db/lang:sql/schema:public/function:do_thing(uuid)",
352 "kind": "function",
353 "parent": "code+moniker://app/srcset:db/lang:sql/schema:public",
354 "visibility": "anything"
355 }]
356 });
357 assert!(parse_spec(&v).is_ok());
358 }
359
360 #[test]
361 fn rejects_unknown_edge_kind() {
362 let v = json!({
363 "root": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo",
364 "lang": "java",
365 "symbols": [],
366 "edges": [{
367 "from": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo",
368 "kind": "extends",
369 "to": "code+moniker://app/srcset:main/lang:java/package:com/module:Bar"
370 }]
371 });
372 let err = parse_spec(&v).unwrap_err();
373 assert!(matches!(err, DeclareError::UnknownEdgeKind(s) if s == "extends"));
374 }
375
376 #[test]
377 fn parses_all_four_canonical_edge_kinds() {
378 let v = json!({
379 "root": "code+moniker://app/srcset:main/lang:rs/module:foo",
380 "lang": "rs",
381 "symbols": [{
382 "moniker": "code+moniker://app/srcset:main/lang:rs/module:foo/fn:f()",
383 "kind": "fn",
384 "parent": "code+moniker://app/srcset:main/lang:rs/module:foo"
385 }],
386 "edges": [
387 { "from": "code+moniker://app/srcset:main/lang:rs/module:foo/fn:f()",
388 "kind": "depends_on",
389 "to": "code+moniker://app/external_pkg:cargo/path:serde" },
390 { "from": "code+moniker://app/srcset:main/lang:rs/module:foo/fn:f()",
391 "kind": "calls",
392 "to": "code+moniker://app/srcset:main/lang:rs/module:foo/fn:g()" },
393 { "from": "code+moniker://app/srcset:main/lang:rs/module:foo/fn:f()",
394 "kind": "injects:provide",
395 "to": "code+moniker://app/srcset:main/lang:rs/module:bar/trait:T" },
396 { "from": "code+moniker://app/srcset:main/lang:rs/module:foo/fn:f()",
397 "kind": "injects:require",
398 "to": "code+moniker://app/srcset:main/lang:rs/module:bar/trait:U" }
399 ]
400 });
401 let s = parse_spec(&v).unwrap();
402 assert_eq!(s.edges.len(), 4);
403 assert_eq!(s.edges[0].kind, EdgeKind::DependsOn);
404 assert_eq!(s.edges[1].kind, EdgeKind::Calls);
405 assert_eq!(s.edges[2].kind, EdgeKind::InjectsProvide);
406 assert_eq!(s.edges[3].kind, EdgeKind::InjectsRequire);
407 }
408
409 #[test]
410 fn rejects_invalid_moniker_uri() {
411 let v = json!({
412 "root": "not-a-uri",
413 "lang": "java",
414 "symbols": []
415 });
416 let err = parse_spec(&v).unwrap_err();
417 assert!(matches!(err, DeclareError::InvalidMoniker { .. }));
418 }
419
420 #[test]
421 fn missing_edges_treated_as_empty() {
422 let s = parse_spec(&minimal_spec()).unwrap();
423 assert!(s.edges.is_empty());
424 }
425
426 #[test]
427 fn null_edges_treated_as_empty() {
428 let mut v = minimal_spec();
429 v.as_object_mut()
430 .unwrap()
431 .insert("edges".to_string(), Value::Null);
432 let s = parse_spec(&v).unwrap();
433 assert!(s.edges.is_empty());
434 }
435}