Skip to main content

concinnity_world/spec/
mod.rs

1//! Typed asset specs: the struct form of a world.jsonl line.
2//!
3//! An `AssetSpec` is a `{name, type, args}` entry described as data rather than a
4//! JSON string. `args` is an ordered list of `(key, ArgValue)` pairs, where
5//! `ArgValue` is a small JSON-shaped value tree. Builders in `asset` assemble
6//! these; `json` converts one into the engine's `serde_json::Value` so a consumer
7//! never parses a JSON string. This is the substrate both the asset builders and
8//! the world templates in `crate::template` are expressed in.
9
10pub mod asset;
11
12mod json;
13
14pub use json::{arg_value_to_json, spec_args, spec_to_value};
15
16/// A JSON-shaped value: exactly the shapes an asset's `args` object can hold. Kept
17/// deliberately small so it maps one-to-one onto the engine's accept path
18/// (`serde_json::from_value` with `#[serde(default)]`), where an omitted field
19/// falls back to its default.
20#[derive(Clone, Debug, PartialEq)]
21pub enum ArgValue {
22    /// A JSON null.
23    Null,
24    /// A boolean.
25    Bool(bool),
26    /// An integer.
27    Int(i64),
28    /// A floating-point number.
29    Float(f64),
30    /// A string.
31    Str(String),
32    /// An array of values.
33    Array(Vec<ArgValue>),
34    /// An object, in insertion order (nested args reuse the same ordered shape).
35    Object(Vec<(String, ArgValue)>),
36}
37
38impl ArgValue {
39    /// A numeric array from float components (colours, positions, sizes).
40    pub fn floats(vals: &[f32]) -> ArgValue {
41        ArgValue::Array(vals.iter().map(|&v| ArgValue::Float(v as f64)).collect())
42    }
43}
44
45impl From<bool> for ArgValue {
46    fn from(v: bool) -> Self {
47        ArgValue::Bool(v)
48    }
49}
50impl From<i64> for ArgValue {
51    fn from(v: i64) -> Self {
52        ArgValue::Int(v)
53    }
54}
55impl From<u32> for ArgValue {
56    fn from(v: u32) -> Self {
57        ArgValue::Int(v as i64)
58    }
59}
60impl From<usize> for ArgValue {
61    fn from(v: usize) -> Self {
62        ArgValue::Int(v as i64)
63    }
64}
65impl From<f32> for ArgValue {
66    fn from(v: f32) -> Self {
67        ArgValue::Float(v as f64)
68    }
69}
70impl From<f64> for ArgValue {
71    fn from(v: f64) -> Self {
72        ArgValue::Float(v)
73    }
74}
75impl From<&str> for ArgValue {
76    fn from(v: &str) -> Self {
77        ArgValue::Str(String::from(v))
78    }
79}
80impl From<String> for ArgValue {
81    fn from(v: String) -> Self {
82        ArgValue::Str(v)
83    }
84}
85impl<const N: usize> From<[f32; N]> for ArgValue {
86    fn from(v: [f32; N]) -> Self {
87        ArgValue::floats(&v)
88    }
89}
90
91/// A named asset entry: the `{name, type, args}` of one world.jsonl line, as data.
92#[derive(Clone, Debug, PartialEq)]
93pub struct AssetSpec {
94    /// The readable asset name (the world-line key). Ignored when a spec is
95    /// materialized straight into a live component, which carries no name field.
96    pub name: String,
97    /// The registered asset type string (`"Sprite"`, `"DirectionalLight"`, ...).
98    pub asset_type: &'static str,
99    /// The `args` object, in insertion order.
100    pub fields: Vec<(String, ArgValue)>,
101}
102
103impl AssetSpec {
104    /// An entry of `asset_type` named `name`, with no args set yet (each unset
105    /// field takes the type's serde default when materialized).
106    pub fn new(name: impl Into<String>, asset_type: &'static str) -> Self {
107        AssetSpec {
108            name: name.into(),
109            asset_type,
110            fields: Vec::new(),
111        }
112    }
113
114    /// Set one arg, chainable. A later `set` of the same key appends a second
115    /// entry; builders set each key once.
116    pub fn set(mut self, key: impl Into<String>, value: impl Into<ArgValue>) -> Self {
117        self.fields.push((key.into(), value.into()));
118        self
119    }
120
121    /// The `args` object as a single `ArgValue`.
122    pub fn args(&self) -> ArgValue {
123        ArgValue::Object(self.fields.clone())
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn set_records_fields_in_order() {
133        let spec = AssetSpec::new("lamp", "PointLight")
134            .set("intensity", 8.0f32)
135            .set("range", 20.0f32);
136        assert_eq!(spec.name, "lamp");
137        assert_eq!(spec.asset_type, "PointLight");
138        assert_eq!(
139            spec.fields
140                .iter()
141                .map(|(k, _)| k.as_str())
142                .collect::<Vec<_>>(),
143            vec!["intensity", "range"]
144        );
145    }
146
147    #[test]
148    fn conversions_pick_the_expected_variant() {
149        assert_eq!(ArgValue::from(true), ArgValue::Bool(true));
150        assert_eq!(ArgValue::from(48u32), ArgValue::Int(48));
151        assert_eq!(ArgValue::from(1.5f32), ArgValue::Float(1.5));
152        assert_eq!(ArgValue::from("sky"), ArgValue::Str("sky".to_string()));
153        assert_eq!(
154            ArgValue::from([1.0f32, 0.0, 0.0]),
155            ArgValue::Array(vec![
156                ArgValue::Float(1.0),
157                ArgValue::Float(0.0),
158                ArgValue::Float(0.0),
159            ])
160        );
161    }
162
163    #[test]
164    fn args_wraps_fields_as_an_object() {
165        let spec = AssetSpec::new("s", "Sprite").set("visible", false);
166        assert_eq!(
167            spec.args(),
168            ArgValue::Object(vec![("visible".to_string(), ArgValue::Bool(false))])
169        );
170    }
171}