1use concinnity_cook::world::{find_world_jsonl, parse_world_jsonl, resolve_includes};
3use concinnity_world::registry::RegisteredType;
4
5fn registration_for(type_str: &str) -> Option<concinnity_world::registry::Registration> {
7 RegisteredType::parse(type_str).map(RegisteredType::registration)
8}
9
10pub(crate) fn resolve_world_path(json_path: Option<&str>) -> std::io::Result<String> {
13 match json_path {
14 Some(p) if std::path::Path::new(p).exists() => Ok(p.to_string()),
15 _ => find_world_jsonl(None),
16 }
17}
18
19pub(crate) fn provenance(loaded: &concinnity_cook::world::LoadedWorld, name: &str) -> String {
24 loaded.provenance(name).to_string()
25}
26
27pub fn list(json_path: Option<&str>, expanded: bool, systems: bool) -> std::io::Result<()> {
30 let json_path = resolve_world_path(json_path)?;
31
32 let content = std::fs::read_to_string(&json_path).map_err(|e| {
33 tracing::error!("Could not read {}: {}", json_path, e);
34 e
35 })?;
36
37 if systems {
38 return list_systems(&content, &json_path);
39 }
40 if expanded {
41 return list_expanded(&content, &json_path);
42 }
43
44 let assets = parse_world_jsonl(&content).map_err(|e| {
45 std::io::Error::new(
46 std::io::ErrorKind::InvalidData,
47 format!("Failed to parse {}: {}", json_path, e),
48 )
49 })?;
50
51 let raw = resolve_includes(assets)?;
52
53 if raw.is_empty() {
54 println!("{} has no assets.", json_path);
55 return Ok(());
56 }
57
58 struct Row {
60 name: String,
61 type_str: String,
62 origin: String,
63 payload: String,
64 }
65
66 let rows: Vec<Row> = raw
67 .iter()
68 .map(|v| {
69 let name = v
70 .get("name")
71 .and_then(|n| n.as_str())
72 .unwrap_or("(unnamed)")
73 .to_string();
74 let type_str = v
75 .get("type")
76 .and_then(|t| t.as_str())
77 .unwrap_or("?")
78 .to_string();
79
80 let (origin, payload) = if let Some(r) = registration_for(&type_str) {
81 (format!("{:?}", r.origin), format!("{:?}", r.payload))
82 } else {
83 ("?".to_string(), "?".to_string())
84 };
85
86 Row {
87 name,
88 type_str,
89 origin,
90 payload,
91 }
92 })
93 .collect();
94
95 let w_name = rows.iter().map(|r| r.name.len()).max().unwrap_or(4).max(4);
96 let w_type = rows
97 .iter()
98 .map(|r| r.type_str.len())
99 .max()
100 .unwrap_or(4)
101 .max(4);
102 let w_origin = rows
103 .iter()
104 .map(|r| r.origin.len())
105 .max()
106 .unwrap_or(6)
107 .max(6);
108
109 println!(
110 "{:<w_name$} {:<w_type$} {:<w_origin$} PAYLOAD",
111 "NAME",
112 "TYPE",
113 "ORIGIN",
114 w_name = w_name,
115 w_type = w_type,
116 w_origin = w_origin,
117 );
118 println!("{}", "-".repeat(w_name + w_type + w_origin + 16));
119
120 for r in &rows {
121 println!(
122 "{:<w_name$} {:<w_type$} {:<w_origin$} {}",
123 r.name,
124 r.type_str,
125 r.origin,
126 r.payload,
127 w_name = w_name,
128 w_type = w_type,
129 w_origin = w_origin,
130 );
131 }
132
133 println!("\n{} asset(s) in {}", rows.len(), json_path);
134 Ok(())
135}
136
137fn list_expanded(content: &str, json_path: &str) -> std::io::Result<()> {
141 let loaded =
142 concinnity_cook::prepare_world(content, concinnity_cook::paths::assets_dir().as_deref())
143 .map_err(|errs| concinnity_cook::check::report_validation_errors(&errs))?;
144
145 if loaded.assets.is_empty() {
146 println!("{} expands to no assets.", json_path);
147 return Ok(());
148 }
149
150 let rows: Vec<(String, String, String)> = loaded
151 .assets
152 .iter()
153 .map(|a| {
154 (
155 a.name.clone(),
156 a.asset_type.clone(),
157 provenance(&loaded, &a.name),
158 )
159 })
160 .collect();
161
162 let w_name = rows.iter().map(|r| r.0.len()).max().unwrap_or(4).max(4);
163 let w_type = rows.iter().map(|r| r.1.len()).max().unwrap_or(4).max(4);
164
165 println!(
166 "{:<w_name$} {:<w_type$} PROVENANCE",
167 "NAME",
168 "TYPE",
169 w_name = w_name,
170 w_type = w_type,
171 );
172 println!("{}", "-".repeat(w_name + w_type + 14));
173
174 for (name, type_str, prov) in &rows {
175 println!(
176 "{:<w_name$} {:<w_type$} {}",
177 name,
178 type_str,
179 prov,
180 w_name = w_name,
181 w_type = w_type,
182 );
183 }
184
185 let injected = loaded.injected.len();
186 println!(
187 "\n{} asset(s) after expansion ({} injected) in {}",
188 rows.len(),
189 injected,
190 json_path
191 );
192 println!("Use `cn explain <name>` to print an entry for overriding.");
193 Ok(())
194}
195
196fn list_systems(content: &str, json_path: &str) -> std::io::Result<()> {
201 let world = crate::build_world_from_str(content)?;
202 let lines = manifest_lines(&world);
203
204 if lines.is_empty() {
205 println!("{} runs no systems.", json_path);
206 return Ok(());
207 }
208
209 println!("{} runs {} system(s), in order:", json_path, lines.len());
210 for line in &lines {
211 println!(" {}", line);
212 }
213 Ok(())
214}
215
216fn manifest_lines(world: &concinnity_engine::ecs::World) -> Vec<String> {
221 let manifest = world.system_manifest(concinnity_engine::ecs::SYSTEMS);
222 let width = manifest.iter().map(|n| n.len()).max().unwrap_or(0);
223 manifest
224 .iter()
225 .map(|name| {
226 let reason = concinnity_engine::ecs::SYSTEMS
227 .entries
228 .iter()
229 .find(|e| e.name == *name)
230 .map(|e| e.present_when)
231 .unwrap_or("");
232 format!("{name:<width$} {reason}")
233 })
234 .collect()
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 fn write_world(content: &str) -> (tempfile::TempDir, String) {
242 let dir = tempfile::tempdir().unwrap();
243 let path = dir.path().join("world.jsonl");
244 std::fs::write(&path, content).unwrap();
245 (dir, path.to_string_lossy().into_owned())
246 }
247
248 #[test]
249 fn resolve_world_path_prefers_an_explicit_existing_path() {
250 let (_dir, path) = write_world("");
251 assert_eq!(resolve_world_path(Some(&path)).unwrap(), path);
252 }
253
254 #[test]
258 fn manifest_lines_report_the_world_schedule_with_reasons() {
259 let world = crate::build_world_from_str(
260 "{\"name\":\"gfx\",\"type\":\"GraphicsConfig\",\"args\":{}}\n\
261 {\"name\":\"cam\",\"type\":\"Camera3D\",\"args\":{\"controller\":{\"free_fly\":true}}}\n",
262 )
263 .unwrap();
264 let lines = manifest_lines(&world);
265 let joined = lines.join("\n");
266 assert!(joined.contains("GraphicsSystem"), "{joined}");
267 assert!(joined.contains("Camera3DSystem"), "{joined}");
268 assert!(joined.contains("GraphicsConfig"), "{joined}");
270 }
271
272 #[test]
274 fn list_with_systems_flag_is_ok() {
275 let (_dir, path) =
276 write_world("{\"name\":\"gfx\",\"type\":\"GraphicsConfig\",\"args\":{}}\n");
277 list(Some(&path), false, true).unwrap();
278 }
279
280 fn loaded_world_fixture() -> concinnity_cook::world::LoadedWorld {
281 concinnity_cook::world::LoadedWorld {
282 assets: Vec::new(),
283 injected: vec![concinnity_cook::world::InjectedAsset {
284 name: "debug_hud".to_string(),
285 asset_type: "DebugHud".to_string(),
286 args: serde_json::json!({}),
287 injected_by: "debug_hud",
288 }],
289 generated: vec![concinnity_cook::world::GeneratedAsset {
290 name: "bistro_mat_wood".to_string(),
291 asset_type: "Material".to_string(),
292 generated_by: "bistro".to_string(),
293 }],
294 shadowed: vec![concinnity_cook::world::ShadowedAsset {
295 name: "bistro_mat_glass".to_string(),
296 asset_type: "Material".to_string(),
297 generated_by: "bistro".to_string(),
298 args: serde_json::json!({}),
299 }],
300 authored: vec!["cam".to_string(), "bistro_mat_glass".to_string()],
301 }
302 }
303
304 #[test]
305 fn provenance_reports_authored_names() {
306 assert_eq!(provenance(&loaded_world_fixture(), "cam"), "authored");
307 }
308
309 #[test]
310 fn provenance_reports_the_injection_pass() {
311 assert_eq!(
312 provenance(&loaded_world_fixture(), "debug_hud"),
313 "injected:debug_hud"
314 );
315 }
316
317 #[test]
320 fn provenance_reports_the_generating_import() {
321 assert_eq!(
322 provenance(&loaded_world_fixture(), "bistro_mat_wood"),
323 "generated:bistro"
324 );
325 }
326
327 #[test]
330 fn provenance_reports_what_an_authored_copy_shadows() {
331 assert_eq!(
332 provenance(&loaded_world_fixture(), "bistro_mat_glass"),
333 "authored (shadows bistro)"
334 );
335 }
336
337 #[test]
338 fn provenance_falls_back_to_expanded() {
339 assert_eq!(provenance(&loaded_world_fixture(), "anything"), "expanded");
340 }
341
342 #[test]
343 fn list_of_an_empty_world_is_ok() {
344 let (_dir, path) = write_world("");
345 list(Some(&path), false, false).unwrap();
346 }
347
348 #[test]
349 fn list_prints_known_and_unknown_types() {
350 let (_dir, path) = write_world(concat!(
354 "{\"name\":\"gfx\",\"type\":\"GraphicsConfig\",\"args\":{}}\n",
355 "{\"name\":\"clip\",\"type\":\"AudioClip\",\"args\":{}}\n",
356 "{\"name\":\"odd\",\"type\":\"NotARealAssetType\",\"args\":{}}\n",
357 "{\"type\":\"GraphicsConfig\",\"args\":{}}\n",
358 ));
359 list(Some(&path), false, false).unwrap();
360 }
361
362 #[test]
363 fn registration_for_resolves_component_types() {
364 let r = registration_for("GraphicsConfig").unwrap();
365 assert_eq!(r.type_name, "GraphicsConfig");
366 }
367
368 #[test]
369 fn registration_for_resolves_resource_assets() {
370 for name in ["AudioClip", "Texture"] {
373 assert!(RegisteredType::parse(name).is_some_and(|t| t.is_resource()));
374 let r = registration_for(name).unwrap();
375 assert_eq!(r.origin, concinnity_world::registry::AssetOrigin::External);
376 assert_eq!(
377 r.payload,
378 concinnity_world::registry::AssetPayload::Compiled
379 );
380 }
381 }
382
383 #[test]
384 fn registration_for_unknown_type_is_none() {
385 assert!(registration_for("NotARealAssetType").is_none());
386 }
387
388 #[test]
389 fn list_with_invalid_json_errors() {
390 let (_dir, path) = write_world("{ not json\n");
391 let err = list(Some(&path), false, false).unwrap_err();
392 assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
393 }
394
395 #[test]
396 fn list_expanded_runs_the_build_front_half() {
397 let (_dir, path) =
398 write_world("{\"name\":\"gfx\",\"type\":\"GraphicsConfig\",\"args\":{}}\n");
399 list(Some(&path), true, false).unwrap();
400 }
401
402 #[test]
403 fn list_expanded_rejects_an_unknown_asset_type() {
404 let (_dir, path) =
405 write_world("{\"name\":\"odd\",\"type\":\"NotARealAssetType\",\"args\":{}}\n");
406 assert!(list(Some(&path), true, false).is_err());
407 }
408
409 #[test]
413 fn list_of_an_unreadable_world_surfaces_the_read_error() {
414 let dir = tempfile::tempdir().unwrap();
415 let path = dir.path().to_string_lossy().into_owned();
416 assert!(list(Some(&path), false, false).is_err());
417 }
418
419 #[test]
422 fn list_expanded_of_an_empty_world_is_ok() {
423 let (_dir, path) = write_world("");
424 list(Some(&path), true, false).unwrap();
425 }
426
427 #[test]
429 fn list_systems_of_an_empty_world_is_ok() {
430 let (_dir, path) = write_world("");
431 list(Some(&path), false, true).unwrap();
432 }
433
434 #[test]
435 fn manifest_lines_of_an_empty_world_are_empty() {
436 let world = crate::build_world_from_str("").unwrap();
437 assert!(manifest_lines(&world).is_empty());
438 }
439}