concinnity_world/template/mod.rs
1//! World templates: named bundles of asset specs the editor and `cn add --template`
2//! layer onto a world. Each template's assets are built from the typed
3//! `crate::spec::asset` builders, so a consumer gets structured specs (never a JSON
4//! string to parse).
5//! Every asset stays standalone (no required cross-references) and free of source-file
6//! dependencies, so a template applies cleanly to any fresh world.
7
8mod minimal_world;
9
10use crate::spec::AssetSpec;
11
12/// A named bundle of asset specs.
13pub struct WorldTemplate {
14 /// Stable machine name used on the command line and in the editor
15 /// (`cn add --template <name>`).
16 pub name: &'static str,
17 /// Human-facing label shown in the editor's templates dropdown.
18 pub title: &'static str,
19 /// One-line description of what the template layers onto a world.
20 pub description: &'static str,
21 // Builds the template's assets. A function (not a stored slice) because an
22 // `AssetSpec` owns heap data and cannot be a `const`.
23 build: fn() -> Vec<AssetSpec>,
24}
25
26impl WorldTemplate {
27 /// The template's assets as typed specs, in application order.
28 pub fn assets(&self) -> Vec<AssetSpec> {
29 (self.build)()
30 }
31}
32
33/// Every engine-owned world template, in display order.
34pub const TEMPLATES: &[WorldTemplate] = &[WorldTemplate {
35 name: "minimal-3d-world",
36 title: "Minimal 3D World",
37 description: "A lit room with a camera and sky.",
38 build: minimal_world::assets,
39}];
40
41/// Look up a world template by its machine `name`.
42pub fn by_name(name: &str) -> Option<&'static WorldTemplate> {
43 TEMPLATES.iter().find(|t| t.name == name)
44}
45
46#[cfg(test)]
47mod tests {
48 use super::*;
49
50 // Every template builds at least one asset, each a well-formed spec (a
51 // non-empty name and a non-empty args object). This is the structural contract
52 // the engine's asset construction relies on; type-level validity against the
53 // real asset schemas is checked where a std consumer can reach the registry
54 // (the `cn add` template test).
55 #[test]
56 fn every_template_builds_well_formed_specs() {
57 for t in TEMPLATES {
58 let specs = t.assets();
59 assert!(!specs.is_empty(), "template '{}' has no assets", t.name);
60 for s in &specs {
61 assert!(
62 !s.name.is_empty(),
63 "template '{}' has an unnamed asset",
64 t.name
65 );
66 assert!(
67 !s.fields.is_empty(),
68 "template '{}' asset '{}' sets no args",
69 t.name,
70 s.name
71 );
72 }
73 }
74 }
75
76 // Template names are unique (the lookup key) and resolvable.
77 #[test]
78 fn names_are_unique_and_resolvable() {
79 for (i, t) in TEMPLATES.iter().enumerate() {
80 assert_eq!(by_name(t.name).map(|r| r.name), Some(t.name));
81 for other in &TEMPLATES[i + 1..] {
82 assert_ne!(t.name, other.name, "duplicate template name '{}'", t.name);
83 }
84 }
85 assert!(by_name("does-not-exist").is_none());
86 }
87
88 // Asset names are unique within each template, so applying it never collides an
89 // entry with itself.
90 #[test]
91 fn asset_names_within_a_template_are_unique() {
92 for t in TEMPLATES {
93 let mut names: Vec<String> = t.assets().into_iter().map(|s| s.name).collect();
94 names.sort();
95 let before = names.len();
96 names.dedup();
97 assert_eq!(
98 before,
99 names.len(),
100 "template '{}' has duplicate asset names",
101 t.name
102 );
103 }
104 }
105}