1use std::sync::Arc;
10
11use sim_kernel::{Cx, DefaultFactory, Expr, NoopEvalPolicy, ShapeMatch, Symbol};
12
13use crate::kinds::{KIND_KEY, is_known_kind};
14
15#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct SceneError {
20 pub path: Vec<String>,
22 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 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
50pub use sim_value::build::map;
53
54pub 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
68pub 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
81pub 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}