Skip to main content

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}