Skip to main content

code_moniker_core/declare/
parse.rs

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}