Skip to main content

code_moniker_core/declare/
serialize.rs

1use serde_json::{Value, json};
2
3use super::{EdgeKind, Lang};
4use crate::core::code_graph::CodeGraph;
5use crate::core::kinds::{REF_CALLS, REF_DI_REGISTER, REF_DI_REQUIRE, REF_IMPORTS_MODULE};
6use crate::core::moniker::Moniker;
7use crate::core::uri::{UriConfig, to_uri};
8
9#[derive(Clone, Debug, Eq, PartialEq)]
10pub enum SerializeError {
11	RootHasNoLangSegment {
12		root: String,
13	},
14	UnknownLangSegment {
15		lang: String,
16	},
17	LangMismatch {
18		expected: &'static str,
19		actual: String,
20	},
21	UriRender {
22		reason: String,
23	},
24	Utf8 {
25		what: &'static str,
26	},
27}
28
29impl std::fmt::Display for SerializeError {
30	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31		match self {
32			Self::RootHasNoLangSegment { root } => write!(
33				f,
34				"graph root `{root}` has no `lang:` segment ; cannot infer the spec's `lang` field"
35			),
36			Self::UnknownLangSegment { lang } => write!(
37				f,
38				"graph root carries `lang:{lang}` which is not a recognised declarative profile"
39			),
40			Self::LangMismatch { expected, actual } => write!(
41				f,
42				"graph root carries `lang:{actual}` which does not match the typed extractor's `{expected}` (use the dynamic-dispatch entry point if you do not know the language ahead of time)"
43			),
44			Self::UriRender { reason } => write!(f, "moniker URI render error: {reason}"),
45			Self::Utf8 { what } => write!(f, "{what} contains non-UTF-8 bytes"),
46		}
47	}
48}
49
50impl std::error::Error for SerializeError {}
51
52pub fn graph_to_spec(graph: &CodeGraph) -> Result<Value, SerializeError> {
53	let root = graph.root();
54	let lang = lang_from_root(root)?;
55	let cfg = UriConfig::default();
56	let defs: Vec<&_> = graph.defs().collect();
57
58	let mut symbols: Vec<Value> = Vec::with_capacity(defs.len().saturating_sub(1));
59	for (i, d) in defs.iter().enumerate() {
60		if i == 0 {
61			continue;
62		}
63		let parent_moniker = defs
64			.get(d.parent.unwrap_or(0))
65			.map(|p| &p.moniker)
66			.unwrap_or(root);
67		let mut sym = serde_json::Map::with_capacity(5);
68		sym.insert(
69			"moniker".to_string(),
70			Value::String(render(&d.moniker, &cfg)?),
71		);
72		sym.insert(
73			"kind".to_string(),
74			Value::String(utf8(&d.kind, "def kind")?.to_string()),
75		);
76		sym.insert(
77			"parent".to_string(),
78			Value::String(render(parent_moniker, &cfg)?),
79		);
80		if !d.visibility.is_empty() {
81			sym.insert(
82				"visibility".to_string(),
83				Value::String(utf8(&d.visibility, "def visibility")?.to_string()),
84			);
85		}
86		if !d.signature.is_empty() {
87			sym.insert(
88				"signature".to_string(),
89				Value::String(utf8(&d.signature, "def signature")?.to_string()),
90			);
91		}
92		symbols.push(Value::Object(sym));
93	}
94
95	let mut edges: Vec<Value> = Vec::with_capacity(graph.ref_count());
96	for r in graph.refs() {
97		let Some(canonical) = lift_ref_kind(&r.kind) else {
98			continue;
99		};
100		let from_moniker = &defs
101			.get(r.source)
102			.ok_or_else(|| SerializeError::UriRender {
103				reason: format!("ref source index {} out of bounds", r.source),
104			})?
105			.moniker;
106		edges.push(json!({
107			"from": render(from_moniker, &cfg)?,
108			"kind": canonical.tag(),
109			"to":   render(&r.target, &cfg)?,
110		}));
111	}
112
113	Ok(json!({
114		"root":    render(root, &cfg)?,
115		"lang":    lang.tag(),
116		"symbols": symbols,
117		"edges":   edges,
118	}))
119}
120
121fn lang_from_root(root: &Moniker) -> Result<Lang, SerializeError> {
122	let cfg = UriConfig::default();
123	let view = root.as_view();
124	let lang_bytes = view
125		.lang_segment()
126		.ok_or_else(|| SerializeError::RootHasNoLangSegment {
127			root: render(root, &cfg).unwrap_or_else(|_| "<unrenderable>".to_string()),
128		})?;
129	let lang_str = std::str::from_utf8(lang_bytes).map_err(|_| SerializeError::Utf8 {
130		what: "lang segment",
131	})?;
132	Lang::from_tag(lang_str).ok_or_else(|| SerializeError::UnknownLangSegment {
133		lang: lang_str.to_string(),
134	})
135}
136
137fn lift_ref_kind(kind: &[u8]) -> Option<EdgeKind> {
138	match kind {
139		k if k == REF_IMPORTS_MODULE => Some(EdgeKind::DependsOn),
140		k if k == REF_CALLS => Some(EdgeKind::Calls),
141		k if k == REF_DI_REGISTER => Some(EdgeKind::InjectsProvide),
142		k if k == REF_DI_REQUIRE => Some(EdgeKind::InjectsRequire),
143		_ => None,
144	}
145}
146
147fn render(m: &Moniker, cfg: &UriConfig<'_>) -> Result<String, SerializeError> {
148	to_uri(m, cfg).map_err(|e| SerializeError::UriRender {
149		reason: e.to_string(),
150	})
151}
152
153fn utf8<'a>(bytes: &'a [u8], what: &'static str) -> Result<&'a str, SerializeError> {
154	std::str::from_utf8(bytes).map_err(|_| SerializeError::Utf8 { what })
155}
156
157#[cfg(test)]
158mod tests {
159	use super::*;
160	use crate::declare::{declare_from_json_value, parse_spec};
161	use serde_json::json;
162
163	fn round_trip(input: Value) -> Value {
164		let g = declare_from_json_value(&input).unwrap();
165		graph_to_spec(&g).unwrap()
166	}
167
168	#[test]
169	fn lang_field_is_inferred_from_root_lang_segment() {
170		let v = json!({
171			"root": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo",
172			"lang": "java",
173			"symbols": []
174		});
175		let out = round_trip(v);
176		assert_eq!(out.get("lang").unwrap().as_str().unwrap(), "java");
177	}
178
179	#[test]
180	fn root_field_is_preserved() {
181		let root = "code+moniker://app/srcset:main/lang:java/package:com/module:Foo";
182		let v = json!({
183			"root": root,
184			"lang": "java",
185			"symbols": []
186		});
187		let out = round_trip(v);
188		assert_eq!(out.get("root").unwrap().as_str().unwrap(), root);
189	}
190
191	#[test]
192	fn symbols_are_emitted_for_each_non_root_def() {
193		let v = json!({
194			"root": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo",
195			"lang": "java",
196			"symbols": [
197				{
198					"moniker": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo/class:Foo",
199					"kind": "class",
200					"parent": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo",
201					"visibility": "public"
202				},
203				{
204					"moniker": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo/class:Foo/method:bar()",
205					"kind": "method",
206					"parent": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo/class:Foo",
207					"visibility": "public",
208					"signature": "bar(): void"
209				}
210			]
211		});
212		let out = round_trip(v);
213		let symbols = out.get("symbols").unwrap().as_array().unwrap();
214		assert_eq!(symbols.len(), 2);
215	}
216
217	#[test]
218	fn edges_lift_canonical_kinds() {
219		let v = json!({
220			"root": "code+moniker://app/srcset:main/lang:rs/module:svc",
221			"lang": "rs",
222			"symbols": [{
223				"moniker": "code+moniker://app/srcset:main/lang:rs/module:svc/fn:f()",
224				"kind": "fn",
225				"parent": "code+moniker://app/srcset:main/lang:rs/module:svc"
226			}],
227			"edges": [
228				{ "from": "code+moniker://app/srcset:main/lang:rs/module:svc/fn:f()",
229				  "kind": "depends_on",
230				  "to":   "code+moniker://app/external_pkg:cargo/path:serde" },
231				{ "from": "code+moniker://app/srcset:main/lang:rs/module:svc/fn:f()",
232				  "kind": "calls",
233				  "to":   "code+moniker://app/srcset:main/lang:rs/module:other/fn:g()" },
234				{ "from": "code+moniker://app/srcset:main/lang:rs/module:svc/fn:f()",
235				  "kind": "injects:provide",
236				  "to":   "code+moniker://app/srcset:main/lang:rs/module:di/trait:Repo" },
237				{ "from": "code+moniker://app/srcset:main/lang:rs/module:svc/fn:f()",
238				  "kind": "injects:require",
239				  "to":   "code+moniker://app/srcset:main/lang:rs/module:di/trait:Bus" }
240			]
241		});
242		let out = round_trip(v);
243		let edges = out.get("edges").unwrap().as_array().unwrap();
244		assert_eq!(edges.len(), 4);
245		let kinds: Vec<&str> = edges
246			.iter()
247			.map(|e| e.get("kind").unwrap().as_str().unwrap())
248			.collect();
249		assert!(kinds.contains(&"depends_on"));
250		assert!(kinds.contains(&"calls"));
251		assert!(kinds.contains(&"injects:provide"));
252		assert!(kinds.contains(&"injects:require"));
253	}
254
255	#[test]
256	fn non_canonical_ref_kinds_are_dropped() {
257		use crate::core::code_graph::CodeGraph;
258		use crate::core::moniker::MonikerBuilder;
259		let root = MonikerBuilder::new()
260			.project(b"app")
261			.segment(b"srcset", b"main")
262			.segment(b"lang", b"rs")
263			.segment(b"module", b"svc")
264			.build();
265		let foo = MonikerBuilder::from_view(root.as_view())
266			.segment(b"fn", b"f()")
267			.build();
268		let mut g = CodeGraph::new(root.clone(), b"module");
269		g.add_def(foo.clone(), b"fn", &root, None).unwrap();
270		g.add_ref(
271			&foo,
272			MonikerBuilder::new()
273				.project(b"app")
274				.segment(b"srcset", b"main")
275				.segment(b"lang", b"rs")
276				.segment(b"module", b"svc")
277				.segment(b"struct", b"X")
278				.build(),
279			b"uses_type",
280			None,
281		)
282		.unwrap();
283
284		let out = graph_to_spec(&g).unwrap();
285		let edges = out.get("edges").unwrap().as_array().unwrap();
286		assert!(edges.is_empty(), "non-canonical refs should be dropped");
287	}
288
289	#[test]
290	fn errors_when_root_has_no_lang_segment() {
291		// Build a graph whose root is a project-regime moniker (no lang:).
292		use crate::core::code_graph::CodeGraph;
293		use crate::core::moniker::MonikerBuilder;
294		let root = MonikerBuilder::new()
295			.project(b"app")
296			.segment(b"srcset", b"main")
297			.build();
298		let g = CodeGraph::new(root, b"srcset");
299		let err = graph_to_spec(&g).unwrap_err();
300		assert!(matches!(err, SerializeError::RootHasNoLangSegment { .. }));
301	}
302
303	#[test]
304	fn round_trip_preserves_structure() {
305		let original = json!({
306			"root": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo",
307			"lang": "java",
308			"symbols": [
309				{
310					"moniker": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo/class:Foo",
311					"kind": "class",
312					"parent": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo",
313					"visibility": "public"
314				},
315				{
316					"moniker": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo/class:Foo/method:bar()",
317					"kind": "method",
318					"parent": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo/class:Foo",
319					"visibility": "public"
320				}
321			],
322			"edges": [
323				{
324					"from": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo/class:Foo/method:bar()",
325					"kind": "calls",
326					"to":   "code+moniker://app/srcset:main/lang:java/package:com/module:Other/class:Other/method:baz()"
327				}
328			]
329		});
330		let g1 = declare_from_json_value(&original).unwrap();
331		let spec1 = graph_to_spec(&g1).unwrap();
332		// Re-parse the output: it must still be a valid spec.
333		let _ = parse_spec(&spec1).unwrap();
334		let g2 = declare_from_json_value(&spec1).unwrap();
335		let spec2 = graph_to_spec(&g2).unwrap();
336		// Second round must equal first round (idempotent).
337		assert_eq!(spec1, spec2);
338	}
339
340	#[test]
341	fn declared_origin_preserved_after_round_trip() {
342		let v = json!({
343			"root": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo",
344			"lang": "java",
345			"symbols": [{
346				"moniker": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo/class:Foo",
347				"kind": "class",
348				"parent": "code+moniker://app/srcset:main/lang:java/package:com/module:Foo",
349				"visibility": "public"
350			}]
351		});
352		let g1 = declare_from_json_value(&v).unwrap();
353		let spec = graph_to_spec(&g1).unwrap();
354		let g2 = declare_from_json_value(&spec).unwrap();
355		// The class def in g2 must have origin=declared (because re-declared).
356		let class_def = g2.defs().nth(1).unwrap();
357		assert_eq!(
358			class_def.origin,
359			crate::core::kinds::ORIGIN_DECLARED.to_vec()
360		);
361	}
362}