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