Skip to main content

concinnity_cook/check/
mod.rs

1//! Semantic validation of an expanded world: per-asset arg checks, cross-asset
2//! reference checks, and world-shape rules (crate::check::shape). Structural
3//! validation (name/type present, known type, unique names) happens earlier in
4//! crate::authoring::world::load_world.
5//!
6//! Most checks here are pure JSON-shape validation. A few asset types validate
7//! by running their compiler (mesh generators, texture generators,
8//! cubemap/environment-map sources); those live in the four modules below and
9//! run in the same collection pass as the pure ones.
10
11pub(crate) mod animation_graph;
12pub(crate) mod asset_refs;
13pub(crate) mod audio;
14pub mod behavior;
15pub(crate) mod cross_reference;
16pub(crate) mod cubemap_texture;
17pub(crate) mod environment_map;
18pub mod fault;
19pub(crate) mod instanced_prop;
20pub(crate) mod mesh;
21pub(crate) mod physics;
22/// `Prop` argument checks.
23pub(crate) mod prop;
24/// `SdfVolume` argument checks.
25pub(crate) mod sdf_volume;
26/// `Shader` argument checks.
27pub(crate) mod shader;
28pub(crate) mod shape;
29pub(crate) mod texture;
30pub(crate) mod voxel_chunk;
31pub(crate) mod voxel_world;
32
33use crate::authoring::world::WorldJsonlAsset;
34
35/// Print each validation error in CLI form and collapse them into a single
36/// io::Error. Shared by the `cn test` command and the build orchestrator so a
37/// failed world surfaces every problem in one pass.
38pub fn report_validation_errors(errors: &[String]) -> std::io::Error {
39    for e in errors {
40        eprintln!("error:   {}", e);
41    }
42    eprintln!("\nvalidation failed ({} error(s))", errors.len());
43    std::io::Error::new(
44        std::io::ErrorKind::InvalidData,
45        format!("validation failed with {} error(s)", errors.len()),
46    )
47}
48
49// The pure per-asset checks: JSON-shape validation that runs no compiler.
50fn check_authored_asset(
51    type_norm: &str,
52    name: &str,
53    args: &serde_json::Value,
54) -> Result<(), String> {
55    match type_norm {
56        "animationgraph" => animation_graph::check(name, args),
57        "behavior" => behavior::check(name, args),
58        "variables" => behavior::check_variables(name, args),
59        "shader" => shader::check(name, args),
60        "prop" => prop::check(name, args),
61        "sdfvolume" | "sdf" => sdf_volume::check(name, args),
62        "voxelchunk" | "chunk" => voxel_chunk::check(name, args),
63        "voxelworld" => voxel_world::check(name, args),
64        "instancedprop" | "instanced" => instanced_prop::check(name, args),
65        "triggervolume" => physics::check(name, args),
66        "audioemitter" => audio::check_emitter(name, args),
67        "audiocue" => audio::check_cue(name, args),
68        "propbody" => audio::check_prop_body(name, args),
69        _ => Ok(()),
70    }
71}
72
73// The per-asset checks that validate by running the asset's compiler.
74fn check_compiled_asset(
75    type_norm: &str,
76    name: &str,
77    args: &serde_json::Value,
78) -> Result<(), String> {
79    match type_norm {
80        "texture" => texture::check(name, args),
81        "cubemaptexture" | "cubemap" => cubemap_texture::check(name, args),
82        "environmentmap" | "envmap" | "ibl" => environment_map::check(name, args),
83        "mesh" | "proceduralmesh" => mesh::check(name, args),
84        _ => Ok(()),
85    }
86}
87
88// The full per-asset check: the pure checks plus the compile-backed ones.
89pub(crate) fn check_asset(
90    type_norm: &str,
91    name: &str,
92    args: &serde_json::Value,
93) -> Result<(), String> {
94    check_authored_asset(type_norm, name, args)?;
95    check_compiled_asset(type_norm, name, args)
96}
97
98/// Run all semantic validation on a fully expanded world. Collects every
99/// problem found (per-asset arg errors, unresolved cross-references, and
100/// graphics-rule violations) so the caller can report them in a single pass.
101pub(crate) fn check_world(assets: &[WorldJsonlAsset]) -> Result<(), Vec<String>> {
102    let mut errors: Vec<String> = Vec::new();
103
104    // Names must still be unique after expansion and injection: a duplicate
105    // here means a generated or injected asset silently aliased another (the
106    // authored world's uniqueness was already checked before expansion).
107    let mut seen_names: std::collections::HashSet<&str> = Default::default();
108    for asset in assets {
109        if !seen_names.insert(asset.name.as_str()) {
110            errors.push(format!(
111                "duplicate name '{}' after build-time expansion: a generated or \
112                 injected asset collides with another; rename one of them",
113                asset.name
114            ));
115        }
116    }
117
118    // The world's declared variable table, if it declares one. Behaviors are
119    // checked against it rather than in isolation, so a `set` resolves to the
120    // variable's declared type and a misspelled name is caught here.
121    let declared_vars = assets
122        .iter()
123        .find(|a| a.asset_type.to_lowercase().replace('_', "") == "variables")
124        .map(|a| behavior::DeclaredVars::from_args(&a.args))
125        .unwrap_or_default();
126
127    for asset in assets {
128        let type_norm = asset.asset_type.to_lowercase().replace('_', "");
129        let checked = if type_norm == "behavior" {
130            behavior::check_with_vars(&asset.name, &asset.args, &declared_vars)
131        } else {
132            check_authored_asset(&type_norm, &asset.name, &asset.args)
133        };
134        if let Err(e) = checked {
135            errors.push(e);
136        }
137        if let Err(e) = check_compiled_asset(&type_norm, &asset.name, &asset.args) {
138            errors.push(e);
139        }
140    }
141
142    if let Err(ref_errors) = cross_reference::validate_cross_references(assets) {
143        errors.extend(ref_errors);
144    }
145
146    shape::check_shape(assets, &mut errors);
147
148    if errors.is_empty() {
149        Ok(())
150    } else {
151        Err(errors)
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    fn asset(name: &str, asset_type: &str, args: serde_json::Value) -> WorldJsonlAsset {
160        WorldJsonlAsset {
161            name: name.to_string(),
162            asset_type: asset_type.to_string(),
163            args,
164        }
165    }
166
167    #[test]
168    fn graphics_config_with_full_render_stack_passes_graphics_rules() {
169        let assets = vec![
170            asset("gfx", "GraphicsConfig", serde_json::json!({})),
171            asset("win", "Window", serde_json::json!({})),
172            asset(
173                "scene_shader",
174                "Shader",
175                serde_json::json!({"fragment": "x.slang"}),
176            ),
177        ];
178        assert!(check_world(&assets).is_ok());
179    }
180
181    #[test]
182    fn per_asset_and_cross_reference_errors_both_collected() {
183        // Prop with no mesh/model/prefab (per-asset error) plus a Material
184        // with a missing albedo texture (cross-reference error).
185        let assets = vec![
186            asset("bad_prop", "Prop", serde_json::json!({})),
187            asset("bad_mat", "Material", serde_json::json!({"albedo":"ghost"})),
188        ];
189        let errs = check_world(&assets).unwrap_err();
190        assert!(errs.iter().any(|e| e.contains("bad_prop")));
191        assert!(errs.iter().any(|e| e.contains("ghost")));
192    }
193
194    // The composed pass surfaces a compile-backed error (unknown texture
195    // generator) alongside a pure one (Prop with no source) -- both check sets
196    // run in one collection.
197    #[test]
198    fn composed_checks_collect_pure_and_compile_backed_errors() {
199        let assets = vec![
200            asset("bad_prop", "Prop", serde_json::json!({})),
201            asset(
202                "bad_tex",
203                "Texture",
204                serde_json::json!({"generator": "not_a_generator"}),
205            ),
206        ];
207        let errs = check_world(&assets).unwrap_err();
208        assert!(errs.iter().any(|e| e.contains("bad_prop")));
209        assert!(errs.iter().any(|e| e.contains("not_a_generator")));
210    }
211
212    // Every spelling of a compile-backed asset type reaches the same check.
213    #[test]
214    fn check_asset_routes_each_type_alias() {
215        for alias in ["cubemaptexture", "cubemap"] {
216            let args = serde_json::json!({"source": "studio.png"});
217            let err = check_asset(alias, "c", &args).unwrap_err();
218            assert!(err.contains("Radiance .hdr"), "{alias}: {err}");
219        }
220        for alias in ["environmentmap", "envmap", "ibl"] {
221            let args = serde_json::json!({"generator": "aurora"});
222            let err = check_asset(alias, "e", &args).unwrap_err();
223            assert!(
224                err.contains("unknown EnvironmentMap generator"),
225                "{alias}: {err}"
226            );
227        }
228    }
229
230    #[test]
231    fn check_asset_runs_both_check_sets() {
232        // A pure check arm.
233        assert!(check_asset("prop", "p", &serde_json::json!({})).is_err());
234        // A compile-backed arm.
235        assert!(
236            check_asset(
237                "texture",
238                "t",
239                &serde_json::json!({"generator": "not_a_generator"})
240            )
241            .is_err()
242        );
243        // A type neither set knows is fine.
244        assert!(check_asset("window", "w", &serde_json::json!({})).is_ok());
245    }
246}