Skip to main content

mint_cli/layout/
used_values.rs

1use serde_json::{Map, Number, Value};
2
3use crate::layout::error::LayoutError;
4use crate::layout::value::DataValue;
5
6/// Records resolved values for export.
7pub trait ValueSink {
8    /// Insert a value at the given path.
9    fn record_value(&mut self, path: &[String], value: Value) -> Result<(), LayoutError>;
10}
11
12/// Collects used values into a nested JSON object.
13#[derive(Debug, Default)]
14pub struct ValueCollector {
15    root: Map<String, Value>,
16}
17
18impl ValueCollector {
19    /// Create an empty collector.
20    pub fn new() -> Self {
21        Self { root: Map::new() }
22    }
23
24    /// Convert the collected values into a JSON object.
25    pub fn into_value(self) -> Value {
26        Value::Object(self.root)
27    }
28}
29
30impl ValueSink for ValueCollector {
31    fn record_value(&mut self, path: &[String], value: Value) -> Result<(), LayoutError> {
32        insert_value(&mut self.root, path, value)
33    }
34}
35
36/// No-op sink for builds that don't export JSON.
37pub struct NoopValueSink;
38
39impl ValueSink for NoopValueSink {
40    fn record_value(&mut self, _path: &[String], _value: Value) -> Result<(), LayoutError> {
41        Ok(())
42    }
43}
44
45pub fn data_value_to_json(value: &DataValue) -> Result<Value, LayoutError> {
46    match value {
47        DataValue::Bool(v) => Ok(Value::Number(Number::from(if *v { 1 } else { 0 }))),
48        DataValue::U64(v) => Ok(Value::Number(Number::from(*v))),
49        DataValue::I64(v) => Ok(Value::Number(Number::from(*v))),
50        DataValue::F64(v) => Number::from_f64(*v).map(Value::Number).ok_or_else(|| {
51            LayoutError::DataValueExportFailed(
52                "Non-finite float cannot be serialized to JSON.".to_string(),
53            )
54        }),
55        DataValue::Str(v) => Ok(Value::String(v.clone())),
56    }
57}
58
59pub fn i128_to_json(value: i128) -> Result<Value, LayoutError> {
60    if value >= 0 {
61        let unsigned = u64::try_from(value).map_err(|_| {
62            LayoutError::DataValueExportFailed("Value out of range for JSON number.".to_string())
63        })?;
64        Ok(Value::Number(Number::from(unsigned)))
65    } else {
66        let signed = i64::try_from(value).map_err(|_| {
67            LayoutError::DataValueExportFailed("Value out of range for JSON number.".to_string())
68        })?;
69        Ok(Value::Number(Number::from(signed)))
70    }
71}
72
73pub fn array_to_json(values: &[DataValue]) -> Result<Value, LayoutError> {
74    let mut out = Vec::with_capacity(values.len());
75    for value in values {
76        out.push(data_value_to_json(value)?);
77    }
78    Ok(Value::Array(out))
79}
80
81pub fn array_2d_to_json(values: &[Vec<DataValue>]) -> Result<Value, LayoutError> {
82    let mut out = Vec::with_capacity(values.len());
83    for row in values {
84        out.push(array_to_json(row)?);
85    }
86    Ok(Value::Array(out))
87}
88
89fn insert_value(
90    root: &mut Map<String, Value>,
91    path: &[String],
92    value: Value,
93) -> Result<(), LayoutError> {
94    if path.is_empty() {
95        return Err(LayoutError::DataValueExportFailed(
96            "Cannot record value with empty path.".to_string(),
97        ));
98    }
99
100    let key = &path[0];
101    if path.len() == 1 {
102        if root.contains_key(key) {
103            return Err(LayoutError::DataValueExportFailed(format!(
104                "Duplicate value path '{}'.",
105                path.join(".")
106            )));
107        }
108        root.insert(key.clone(), value);
109        return Ok(());
110    }
111
112    let entry = root
113        .entry(key.clone())
114        .or_insert_with(|| Value::Object(Map::new()));
115
116    match entry {
117        Value::Object(child) => insert_value(child, &path[1..], value),
118        _ => Err(LayoutError::DataValueExportFailed(format!(
119            "Path '{}' collides with existing value.",
120            path.join(".")
121        ))),
122    }
123}