1use std::collections::BTreeMap;
27use std::collections::BTreeSet;
28
29use lattice_config::{ConfigSchema, ConfigValue, ScalarKind, SchemaField};
30
31use crate::config_host::bindings::lattice::plugin_host::config as wit;
32
33#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum ArenaError {
37 OutOfRange { index: u32, len: usize },
39 Cycle { index: u32 },
44}
45
46impl std::fmt::Display for ArenaError {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 match self {
49 ArenaError::OutOfRange { index, len } => {
50 write!(f, "node index {index} is out of range (arena has {len})")
51 }
52 ArenaError::Cycle { index } => {
53 write!(f, "node index {index} is a cycle — a schema is a tree")
54 }
55 }
56 }
57}
58
59impl std::error::Error for ArenaError {}
60
61fn scalar_kind_from_wit(ty: wit::OptionType) -> ScalarKind {
62 match ty {
63 wit::OptionType::Boolean => ScalarKind::Bool,
64 wit::OptionType::Integer => ScalarKind::Int,
65 wit::OptionType::String => ScalarKind::Str,
66 }
67}
68
69fn scalar_kind_to_wit(kind: ScalarKind) -> wit::OptionType {
70 match kind {
71 ScalarKind::Bool => wit::OptionType::Boolean,
72 ScalarKind::Int => wit::OptionType::Integer,
73 ScalarKind::Str => wit::OptionType::String,
74 }
75}
76
77fn checked(index: u32, len: usize) -> Result<usize, ArenaError> {
79 let i = index as usize;
80 if i < len {
81 Ok(i)
82 } else {
83 Err(ArenaError::OutOfRange { index, len })
84 }
85}
86
87pub fn schema_from_wit(arena: &wit::ConfigSchema) -> Result<ConfigSchema, ArenaError> {
89 let mut on_path = BTreeSet::new();
90 schema_node(arena, arena.root, &mut on_path)
91}
92
93fn schema_node(
94 arena: &wit::ConfigSchema,
95 index: u32,
96 on_path: &mut BTreeSet<u32>,
97) -> Result<ConfigSchema, ArenaError> {
98 let i = checked(index, arena.nodes.len())?;
99 if !on_path.insert(index) {
104 return Err(ArenaError::Cycle { index });
105 }
106 let out = match &arena.nodes[i] {
107 wit::SchemaNode::Scalar(ty) => ConfigSchema::Scalar(scalar_kind_from_wit(*ty)),
108 wit::SchemaNode::EnumOf(forms) => ConfigSchema::Enum(forms.clone()),
109 wit::SchemaNode::ListOf(child) => ConfigSchema::list(schema_node(arena, *child, on_path)?),
110 wit::SchemaNode::Record(fields) => {
111 let mut out = Vec::with_capacity(fields.len());
112 for f in fields {
113 out.push(SchemaField {
114 name: f.name.clone(),
115 schema: schema_node(arena, f.schema, on_path)?,
116 required: f.required,
117 doc: f.doc.clone(),
118 });
119 }
120 ConfigSchema::Record(out)
121 }
122 };
123 on_path.remove(&index);
124 Ok(out)
125}
126
127pub fn value_from_wit(arena: &wit::ConfigValue) -> Result<ConfigValue, ArenaError> {
129 let mut on_path = BTreeSet::new();
130 value_node(arena, arena.root, &mut on_path)
131}
132
133fn value_node(
134 arena: &wit::ConfigValue,
135 index: u32,
136 on_path: &mut BTreeSet<u32>,
137) -> Result<ConfigValue, ArenaError> {
138 let i = checked(index, arena.nodes.len())?;
139 if !on_path.insert(index) {
140 return Err(ArenaError::Cycle { index });
141 }
142 let out = match &arena.nodes[i] {
143 wit::ValueNode::Bool(b) => ConfigValue::Bool(*b),
144 wit::ValueNode::Int(n) => ConfigValue::Int(*n),
145 wit::ValueNode::String(s) => ConfigValue::Str(s.clone()),
146 wit::ValueNode::List(children) => {
147 let mut out = Vec::with_capacity(children.len());
148 for c in children {
149 out.push(value_node(arena, *c, on_path)?);
150 }
151 ConfigValue::List(out)
152 }
153 wit::ValueNode::Record(fields) => {
154 let mut map = BTreeMap::new();
155 for (name, child) in fields {
156 map.insert(name.clone(), value_node(arena, *child, on_path)?);
157 }
158 ConfigValue::Record(map)
159 }
160 };
161 on_path.remove(&index);
162 Ok(out)
163}
164
165pub fn value_to_wit(value: &ConfigValue) -> wit::ConfigValue {
172 let mut nodes = Vec::new();
173 let root = push_value(&mut nodes, value);
174 wit::ConfigValue { nodes, root }
175}
176
177fn push_value(nodes: &mut Vec<wit::ValueNode>, value: &ConfigValue) -> u32 {
178 let node = match value {
181 ConfigValue::Bool(b) => wit::ValueNode::Bool(*b),
182 ConfigValue::Int(n) => wit::ValueNode::Int(*n),
183 ConfigValue::Str(s) => wit::ValueNode::String(s.clone()),
184 ConfigValue::List(items) => {
185 let children: Vec<u32> = items.iter().map(|v| push_value(nodes, v)).collect();
186 wit::ValueNode::List(children)
187 }
188 ConfigValue::Record(map) => {
189 let fields: Vec<(String, u32)> = map
190 .iter()
191 .map(|(k, v)| (k.clone(), push_value(nodes, v)))
192 .collect();
193 wit::ValueNode::Record(fields)
194 }
195 };
196 nodes.push(node);
197 (nodes.len() - 1) as u32
198}
199
200pub fn schema_to_wit(schema: &ConfigSchema) -> wit::ConfigSchema {
203 let mut nodes = Vec::new();
204 let root = push_schema(&mut nodes, schema);
205 wit::ConfigSchema { nodes, root }
206}
207
208fn push_schema(nodes: &mut Vec<wit::SchemaNode>, schema: &ConfigSchema) -> u32 {
209 let node = match schema {
210 ConfigSchema::Scalar(k) => wit::SchemaNode::Scalar(scalar_kind_to_wit(*k)),
211 ConfigSchema::Enum(forms) => wit::SchemaNode::EnumOf(forms.clone()),
212 ConfigSchema::List(inner) => wit::SchemaNode::ListOf(push_schema(nodes, inner)),
213 ConfigSchema::Record(fields) => {
214 let out: Vec<wit::SchemaField> = fields
215 .iter()
216 .map(|f| wit::SchemaField {
217 name: f.name.clone(),
218 schema: push_schema(nodes, &f.schema),
219 required: f.required,
220 doc: f.doc.clone(),
221 })
222 .collect();
223 wit::SchemaNode::Record(out)
224 }
225 };
226 nodes.push(node);
227 (nodes.len() - 1) as u32
228}
229
230#[cfg(test)]
231mod tests {
232 #![allow(clippy::unwrap_used, clippy::panic)]
233 use super::*;
234
235 fn templates() -> ConfigSchema {
236 ConfigSchema::list(ConfigSchema::record([
237 SchemaField::new("key", ConfigSchema::string(), "the key to press"),
238 SchemaField::new(
239 "target",
240 ConfigSchema::record([SchemaField::new(
241 "file",
242 ConfigSchema::string(),
243 "where it lands",
244 )]),
245 "where the capture goes",
246 ),
247 SchemaField::new("body", ConfigSchema::string(), "body").optional(),
248 ]))
249 }
250
251 #[test]
252 fn a_nested_schema_survives_the_arena_round_trip() {
253 let schema = templates();
256 let round = schema_from_wit(&schema_to_wit(&schema)).unwrap();
257 assert_eq!(round, schema);
258 }
259
260 #[test]
261 fn a_nested_value_survives_the_arena_round_trip() {
262 let value = ConfigValue::List(vec![ConfigValue::record([
263 ("key".to_string(), ConfigValue::Str("t".into())),
264 (
265 "target".to_string(),
266 ConfigValue::record([("file".to_string(), ConfigValue::Str("a.org".into()))]),
267 ),
268 ])]);
269 let round = value_from_wit(&value_to_wit(&value)).unwrap();
270 assert_eq!(round, value);
271 }
272
273 #[test]
274 fn every_scalar_kind_crosses_as_itself() {
275 for v in [
279 ConfigValue::Bool(true),
280 ConfigValue::Bool(false),
281 ConfigValue::Int(0),
282 ConfigValue::Int(-7),
283 ConfigValue::Int(i64::MAX),
284 ConfigValue::Str(String::new()),
285 ConfigValue::Str("x".into()),
286 ] {
287 assert_eq!(value_from_wit(&value_to_wit(&v)).unwrap(), v);
288 }
289 for s in [
290 ConfigSchema::bool(),
291 ConfigSchema::int(),
292 ConfigSchema::string(),
293 ConfigSchema::Enum(vec!["a".into(), "b".into()]),
294 ] {
295 assert_eq!(schema_from_wit(&schema_to_wit(&s)).unwrap(), s);
296 }
297 }
298
299 #[test]
300 fn an_out_of_range_index_is_refused_rather_than_indexed() {
301 let arena = wit::ConfigValue {
302 nodes: vec![wit::ValueNode::List(vec![9])],
303 root: 0,
304 };
305 assert_eq!(
306 value_from_wit(&arena),
307 Err(ArenaError::OutOfRange { index: 9, len: 1 })
308 );
309 let empty = wit::ConfigValue {
312 nodes: vec![],
313 root: 0,
314 };
315 assert!(matches!(
316 value_from_wit(&empty),
317 Err(ArenaError::OutOfRange { .. })
318 ));
319 }
320
321 #[test]
322 fn a_cycle_is_refused_rather_than_followed() {
323 let arena = wit::ConfigValue {
328 nodes: vec![wit::ValueNode::List(vec![1]), wit::ValueNode::List(vec![0])],
329 root: 0,
330 };
331 assert_eq!(value_from_wit(&arena), Err(ArenaError::Cycle { index: 0 }));
332
333 let schema = wit::ConfigSchema {
334 nodes: vec![wit::SchemaNode::ListOf(0)],
335 root: 0,
336 };
337 assert_eq!(
338 schema_from_wit(&schema),
339 Err(ArenaError::Cycle { index: 0 })
340 );
341 }
342
343 #[test]
344 fn sharing_a_node_from_two_places_is_not_a_cycle() {
345 let arena = wit::ConfigSchema {
350 nodes: vec![
351 wit::SchemaNode::Scalar(wit::OptionType::String),
352 wit::SchemaNode::Record(vec![
353 wit::SchemaField {
354 name: "a".into(),
355 schema: 0,
356 required: true,
357 doc: String::new(),
358 },
359 wit::SchemaField {
360 name: "b".into(),
361 schema: 0,
362 required: true,
363 doc: String::new(),
364 },
365 ]),
366 ],
367 root: 1,
368 };
369 let got = schema_from_wit(&arena).unwrap();
370 assert_eq!(
371 got,
372 ConfigSchema::record([
373 SchemaField::new("a", ConfigSchema::string(), ""),
374 SchemaField::new("b", ConfigSchema::string(), ""),
375 ])
376 );
377 }
378
379 #[test]
380 fn field_metadata_crosses_intact() {
381 let schema = templates();
386 let round = schema_from_wit(&schema_to_wit(&schema)).unwrap();
387 let ConfigSchema::List(inner) = &round else {
388 panic!("expected a list");
389 };
390 let ConfigSchema::Record(fields) = inner.as_ref() else {
391 panic!("expected a record");
392 };
393 let body = fields.iter().find(|f| f.name == "body").unwrap();
394 assert!(!body.required, "the optional field stayed optional");
395 let key = fields.iter().find(|f| f.name == "key").unwrap();
396 assert!(key.required);
397 assert_eq!(key.doc, "the key to press", "per-field docs cross");
398 }
399}