1use concinnity_cook::authoring::registry::RegisteredType;
3use concinnity_cook::authoring::world::{find_world_jsonl, parse_world_jsonl, resolve_includes};
4
5fn registration_for(type_str: &str) -> Option<concinnity_cook::authoring::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::build_only::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 mut world = crate::build_world_from_str(content)?;
202 complete(&mut world)?;
203 let lines = manifest_lines(&world);
204
205 if lines.is_empty() {
206 println!("{} runs no systems.", json_path);
207 return Ok(());
208 }
209
210 println!("{} runs {} system(s), in order:", json_path, lines.len());
211 for line in &lines {
212 println!(" {}", line);
213 }
214 Ok(())
215}
216
217fn complete(world: &mut concinnity_engine::ecs::World) -> std::io::Result<()> {
221 let Some(complete) = concinnity_engine::ecs::SYSTEMS.complete_world else {
222 return Ok(());
223 };
224 complete(&mut world.context())
225 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))
226}
227
228fn manifest_lines(world: &concinnity_engine::ecs::World) -> Vec<String> {
233 let manifest = world.system_manifest(concinnity_engine::ecs::SYSTEMS);
234 let width = manifest.iter().map(|n| n.len()).max().unwrap_or(0);
235 manifest
236 .iter()
237 .map(|name| {
238 let reason = concinnity_engine::ecs::SYSTEMS
239 .entries
240 .iter()
241 .find(|e| e.name == *name)
242 .map(|e| e.present_when)
243 .unwrap_or("");
244 format!("{name:<width$} {reason}")
245 })
246 .collect()
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 fn write_world(content: &str) -> (tempfile::TempDir, String) {
254 let dir = tempfile::tempdir().unwrap();
255 let path = dir.path().join("world.jsonl");
256 std::fs::write(&path, content).unwrap();
257 (dir, path.to_string_lossy().into_owned())
258 }
259
260 #[test]
261 fn resolve_world_path_prefers_an_explicit_existing_path() {
262 let (_dir, path) = write_world("");
263 assert_eq!(resolve_world_path(Some(&path)).unwrap(), path);
264 }
265
266 #[test]
270 fn manifest_lines_report_the_world_schedule_with_reasons() {
271 let world = crate::build_world_from_str(
272 "{\"name\":\"gfx\",\"type\":\"GraphicsConfig\",\"args\":{}}\n\
273 {\"name\":\"cam\",\"type\":\"Camera3D\",\"args\":{\"controller\":{\"free_fly\":true}}}\n",
274 )
275 .unwrap();
276 let lines = manifest_lines(&world);
277 let joined = lines.join("\n");
278 assert!(joined.contains("GraphicsSystem"), "{joined}");
279 assert!(joined.contains("Camera3DSystem"), "{joined}");
280 assert!(joined.contains("GraphicsConfig"), "{joined}");
282 }
283
284 #[test]
287 fn the_manifest_reports_systems_the_engine_defaults_turn_on() {
288 let mut world = crate::build_world_from_str(
289 "{\"name\":\"gfx\",\"type\":\"GraphicsConfig\",\"args\":{}}\n",
290 )
291 .unwrap();
292 assert!(
293 !manifest_lines(&world).join("\n").contains("DebugHud"),
294 "the world declares no DebugHud of its own"
295 );
296 complete(&mut world).unwrap();
297 assert!(
298 manifest_lines(&world).join("\n").contains("DebugHud"),
299 "the injected debug HUD brings its system with it"
300 );
301 }
302
303 #[test]
305 fn list_with_systems_flag_is_ok() {
306 let (_dir, path) =
307 write_world("{\"name\":\"gfx\",\"type\":\"GraphicsConfig\",\"args\":{}}\n");
308 list(Some(&path), false, true).unwrap();
309 }
310
311 fn loaded_world_fixture() -> concinnity_cook::build_only::LoadedWorld {
312 concinnity_cook::build_only::LoadedWorld {
313 assets: Vec::new(),
314 injected: vec![concinnity_cook::build_only::InjectedAsset {
315 name: "debug_hud".to_string(),
316 asset_type: "DebugHud".to_string(),
317 args: serde_json::json!({}),
318 injected_by: "debug_hud",
319 }],
320 generated: vec![concinnity_cook::build_only::GeneratedAsset {
321 name: "bistro_mat_wood".to_string(),
322 asset_type: "Material".to_string(),
323 generated_by: "bistro".to_string(),
324 }],
325 shadowed: vec![concinnity_cook::build_only::ShadowedAsset {
326 name: "bistro_mat_glass".to_string(),
327 asset_type: "Material".to_string(),
328 generated_by: "bistro".to_string(),
329 args: serde_json::json!({}),
330 }],
331 authored: vec!["cam".to_string(), "bistro_mat_glass".to_string()],
332 }
333 }
334
335 #[test]
336 fn provenance_reports_authored_names() {
337 assert_eq!(provenance(&loaded_world_fixture(), "cam"), "authored");
338 }
339
340 #[test]
341 fn provenance_reports_the_injection_pass() {
342 assert_eq!(
343 provenance(&loaded_world_fixture(), "debug_hud"),
344 "injected:debug_hud"
345 );
346 }
347
348 #[test]
351 fn provenance_reports_the_generating_import() {
352 assert_eq!(
353 provenance(&loaded_world_fixture(), "bistro_mat_wood"),
354 "generated:bistro"
355 );
356 }
357
358 #[test]
361 fn provenance_reports_what_an_authored_copy_shadows() {
362 assert_eq!(
363 provenance(&loaded_world_fixture(), "bistro_mat_glass"),
364 "authored (shadows bistro)"
365 );
366 }
367
368 #[test]
369 fn provenance_falls_back_to_expanded() {
370 assert_eq!(provenance(&loaded_world_fixture(), "anything"), "expanded");
371 }
372
373 #[test]
374 fn list_of_an_empty_world_is_ok() {
375 let (_dir, path) = write_world("");
376 list(Some(&path), false, false).unwrap();
377 }
378
379 #[test]
380 fn list_prints_known_and_unknown_types() {
381 let (_dir, path) = write_world(concat!(
385 "{\"name\":\"gfx\",\"type\":\"GraphicsConfig\",\"args\":{}}\n",
386 "{\"name\":\"clip\",\"type\":\"AudioClip\",\"args\":{}}\n",
387 "{\"name\":\"odd\",\"type\":\"NotARealAssetType\",\"args\":{}}\n",
388 "{\"type\":\"GraphicsConfig\",\"args\":{}}\n",
389 ));
390 list(Some(&path), false, false).unwrap();
391 }
392
393 #[test]
394 fn registration_for_resolves_component_types() {
395 let r = registration_for("GraphicsConfig").unwrap();
396 assert_eq!(r.type_name, "GraphicsConfig");
397 }
398
399 #[test]
400 fn registration_for_resolves_resource_assets() {
401 for name in ["AudioClip", "Texture"] {
404 assert!(RegisteredType::parse(name).is_some_and(|t| t.is_resource()));
405 let r = registration_for(name).unwrap();
406 assert_eq!(
407 r.origin,
408 concinnity_cook::authoring::registry::AssetOrigin::External
409 );
410 assert_eq!(
411 r.payload,
412 concinnity_cook::authoring::registry::AssetPayload::Compiled
413 );
414 }
415 }
416
417 #[test]
418 fn registration_for_unknown_type_is_none() {
419 assert!(registration_for("NotARealAssetType").is_none());
420 }
421
422 #[test]
423 fn list_with_invalid_json_errors() {
424 let (_dir, path) = write_world("{ not json\n");
425 let err = list(Some(&path), false, false).unwrap_err();
426 assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
427 }
428
429 #[test]
430 fn list_expanded_runs_the_build_front_half() {
431 let (_dir, path) =
432 write_world("{\"name\":\"gfx\",\"type\":\"GraphicsConfig\",\"args\":{}}\n");
433 list(Some(&path), true, false).unwrap();
434 }
435
436 #[test]
437 fn list_expanded_rejects_an_unknown_asset_type() {
438 let (_dir, path) =
439 write_world("{\"name\":\"odd\",\"type\":\"NotARealAssetType\",\"args\":{}}\n");
440 assert!(list(Some(&path), true, false).is_err());
441 }
442
443 #[test]
447 fn list_of_an_unreadable_world_surfaces_the_read_error() {
448 let dir = tempfile::tempdir().unwrap();
449 let path = dir.path().to_string_lossy().into_owned();
450 assert!(list(Some(&path), false, false).is_err());
451 }
452
453 #[test]
456 fn list_expanded_of_an_empty_world_is_ok() {
457 let (_dir, path) = write_world("");
458 list(Some(&path), true, false).unwrap();
459 }
460
461 #[test]
463 fn list_systems_of_an_empty_world_is_ok() {
464 let (_dir, path) = write_world("");
465 list(Some(&path), false, true).unwrap();
466 }
467
468 #[test]
469 fn manifest_lines_of_an_empty_world_are_empty() {
470 let world = crate::build_world_from_str("").unwrap();
471 assert!(manifest_lines(&world).is_empty());
472 }
473}