Skip to main content

sim_lib_scene/
model.rs

1//! Scene value model: builders, accessors, and fail-closed validation.
2//!
3//! A Scene is a SIM value (an `Expr` tree) built from open maps tagged with a
4//! `kind` symbol. This module never introduces a parallel data model; it only
5//! provides ergonomic constructors over `Expr` and a validator that turns a
6//! malformed scene into a structured [`SceneError`] (a path plus a message)
7//! rather than a panic.
8
9use std::sync::Arc;
10
11use sim_kernel::{Cx, DefaultFactory, Expr, NoopEvalPolicy, ShapeMatch, Symbol};
12
13use crate::kinds::{KIND_KEY, is_known_kind};
14
15/// A structured scene validation diagnostic: where the problem is and what it
16/// is. `path` is a human-readable address into the scene tree (for example
17/// `nodes[0].kind`); `message` describes the violation.
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct SceneError {
20    /// Address into the scene tree, outermost segment first.
21    pub path: Vec<String>,
22    /// Human-readable description of the violation.
23    pub message: String,
24}
25
26impl SceneError {
27    fn at(path: &[String], message: impl Into<String>) -> Self {
28        Self {
29            path: path.to_vec(),
30            message: message.into(),
31        }
32    }
33
34    /// Render the path as a dotted/indexed address, or `<root>` when empty.
35    pub fn path_string(&self) -> String {
36        if self.path.is_empty() {
37            "<root>".to_owned()
38        } else {
39            self.path.join("")
40        }
41    }
42}
43
44impl core::fmt::Display for SceneError {
45    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
46        write!(f, "{}: {}", self.path_string(), self.message)
47    }
48}
49
50/// Build a plain data map from string-keyed entries (keys become `core`-less
51/// symbols). Use [`node`] to build a tagged scene node.
52pub use sim_value::build::map;
53
54/// Build a scene node: an `Expr::Map` whose first entry is `kind: scene/<name>`
55/// followed by `entries`.
56pub fn node(kind_name: &str, entries: Vec<(&str, Expr)>) -> Expr {
57    let mut pairs = Vec::with_capacity(entries.len() + 1);
58    pairs.push((
59        Expr::Symbol(Symbol::new(KIND_KEY)),
60        Expr::Symbol(Symbol::qualified(crate::kinds::SCENE_NAMESPACE, kind_name)),
61    ));
62    for (key, value) in entries {
63        pairs.push((Expr::Symbol(Symbol::new(key)), value));
64    }
65    Expr::Map(pairs)
66}
67
68/// If `expr` is a map tagged with a symbol `kind`, return that kind symbol.
69pub fn node_kind(expr: &Expr) -> Option<Symbol> {
70    sim_value::access::field_sym(expr, KIND_KEY)
71}
72
73fn kind_entry(map: &Expr) -> Option<&Expr> {
74    sim_value::access::field(map, KIND_KEY)
75}
76
77fn has_kind_key(map: &Expr) -> bool {
78    kind_entry(map).is_some()
79}
80
81/// Validate that `expr` is a well-formed scene, failing closed with a
82/// [`SceneError`] otherwise.
83///
84/// The root must be a scene node (a map tagged with a recognized `scene/<kind>`
85/// symbol). Nested maps that carry a `kind` key are validated as scene nodes
86/// too; maps without a `kind` key are treated as plain data and only recursed
87/// into. This keeps the metadata open (arbitrary data may ride along) while
88/// still rejecting a map that claims to be a scene node but is not one.
89pub fn validate_scene(expr: &Expr) -> Result<(), SceneError> {
90    let mut path = Vec::new();
91    validate_node(expr, &mut path)
92}
93
94fn validate_node(expr: &Expr, path: &mut Vec<String>) -> Result<(), SceneError> {
95    let shape_error = check_scene_shape(expr, path)?;
96    let Expr::Map(entries) = expr else {
97        return Err(SceneError::at(
98            path,
99            "expected a scene node map (an Expr::Map tagged with a kind)",
100        ));
101    };
102    match kind_entry(expr) {
103        None => {
104            return Err(SceneError::at(path, "scene node is missing a 'kind' tag"));
105        }
106        Some(Expr::Symbol(kind)) => {
107            if !is_known_kind(kind) {
108                return Err(SceneError::at(
109                    path,
110                    format!(
111                        "unrecognized scene kind '{kind}' -- if this is a plain data map, \
112                         rename its 'kind' field (scene node maps reserve 'kind')"
113                    ),
114                ));
115            }
116        }
117        Some(_) => {
118            return Err(SceneError::at(path, "scene node 'kind' must be a symbol"));
119        }
120    }
121    if let Some(message) = shape_error {
122        return Err(SceneError::at(path, message));
123    }
124    validate_children(entries, path)
125}
126
127fn check_scene_shape(expr: &Expr, path: &[String]) -> Result<Option<String>, SceneError> {
128    let mut cx = Cx::new(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
129    let matched = crate::shapes::scene_shape()
130        .check_expr(&mut cx, expr)
131        .map_err(|error| SceneError::at(path, format!("scene shape check failed: {error}")))?;
132    Ok((!matched.accepted)
133        .then(|| rejection_message(&matched, "value is not a recognized scene node")))
134}
135
136fn rejection_message(matched: &ShapeMatch, fallback: &str) -> String {
137    matched
138        .diagnostics
139        .first()
140        .map(|diagnostic| diagnostic.message.clone())
141        .unwrap_or_else(|| fallback.to_owned())
142}
143
144fn validate_children(entries: &[(Expr, Expr)], path: &mut Vec<String>) -> Result<(), SceneError> {
145    for (key, value) in entries {
146        let label = match key {
147            Expr::Symbol(symbol) => format!(".{}", symbol.as_qualified_str()),
148            other => format!(".{other:?}"),
149        };
150        path.push(label);
151        validate_data(value, path)?;
152        path.pop();
153    }
154    Ok(())
155}
156
157fn validate_data(expr: &Expr, path: &mut Vec<String>) -> Result<(), SceneError> {
158    match expr {
159        Expr::Map(_) if has_kind_key(expr) => validate_node(expr, path),
160        Expr::Map(entries) => validate_children(entries, path),
161        Expr::List(items) | Expr::Vector(items) | Expr::Set(items) => {
162            for (index, item) in items.iter().enumerate() {
163                path.push(format!("[{index}]"));
164                validate_data(item, path)?;
165                path.pop();
166            }
167            Ok(())
168        }
169        _ => Ok(()),
170    }
171}