#[derive(Clone, Debug, serde::Serialize)]
#[serde(tag = "kind", rename_all = "camelCase")]
pub enum ParamDef {
Float {
name: &'static str,
min: f32,
max: f32,
default: f32,
},
Int {
name: &'static str,
min: i32,
max: i32,
default: i32,
},
Bool {
name: &'static str,
default: bool,
},
String {
name: &'static str,
default: &'static str,
},
Curve {
name: &'static str,
default: &'static [[f32; 2]],
},
Enum {
name: &'static str,
options: &'static [&'static str],
default: i32,
},
FloatInput {
name: &'static str,
min: f32,
max: f32,
default: f32,
},
Icon {
name: &'static str,
options: &'static [(&'static str, &'static str)],
default: &'static str,
},
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum ParamValue {
Bool(bool),
Int(i32),
Float(f32),
String(String),
Curve(Vec<[f32; 2]>),
}
pub fn param_values_from_json(obj: &serde_json::Value, defs: &[ParamDef]) -> Vec<ParamValue> {
let map = match obj.as_object() {
Some(m) => m,
None => return defs.iter().map(|d| d.default_value()).collect(),
};
defs.iter()
.map(|def| match def {
ParamDef::Float { name, default, .. } => {
let v = map
.get(*name)
.and_then(|v| v.as_f64())
.unwrap_or(*default as f64) as f32;
ParamValue::Float(v)
}
ParamDef::Int { name, default, .. } => {
let v = map
.get(*name)
.and_then(|v| v.as_f64())
.unwrap_or(*default as f64) as i32;
ParamValue::Int(v)
}
ParamDef::Bool { name, default } => {
let v = map.get(*name).and_then(|v| v.as_bool()).unwrap_or(*default);
ParamValue::Bool(v)
}
ParamDef::String { name, default } => {
let v = map
.get(*name)
.and_then(|v| v.as_str())
.unwrap_or(default)
.to_string();
ParamValue::String(v)
}
ParamDef::Curve { name, default } => {
let points = map
.get(*name)
.and_then(|v| serde_json::from_value::<Vec<[f32; 2]>>(v.clone()).ok())
.unwrap_or_else(|| default.to_vec());
ParamValue::Curve(points)
}
ParamDef::Enum { name, default, .. } => {
let v = map
.get(*name)
.and_then(|v| v.as_f64())
.unwrap_or(*default as f64) as i32;
ParamValue::Int(v)
}
ParamDef::FloatInput { name, default, .. } => {
let v = map
.get(*name)
.and_then(|v| v.as_f64())
.unwrap_or(*default as f64) as f32;
ParamValue::Float(v)
}
ParamDef::Icon { name, default, .. } => {
let v = map
.get(*name)
.and_then(|v| v.as_str())
.unwrap_or(default)
.to_string();
ParamValue::String(v)
}
})
.collect()
}
impl ParamDef {
pub fn default_value(&self) -> ParamValue {
match self {
ParamDef::Float { default, .. } => ParamValue::Float(*default),
ParamDef::Int { default, .. } => ParamValue::Int(*default),
ParamDef::Bool { default, .. } => ParamValue::Bool(*default),
ParamDef::String { default, .. } => ParamValue::String(default.to_string()),
ParamDef::Curve { default, .. } => ParamValue::Curve(default.to_vec()),
ParamDef::Enum { default, .. } => ParamValue::Int(*default),
ParamDef::FloatInput { default, .. } => ParamValue::Float(*default),
ParamDef::Icon { default, .. } => ParamValue::String(default.to_string()),
}
}
pub fn name(&self) -> &'static str {
match self {
ParamDef::Float { name, .. }
| ParamDef::FloatInput { name, .. }
| ParamDef::Int { name, .. }
| ParamDef::Bool { name, .. }
| ParamDef::String { name, .. }
| ParamDef::Curve { name, .. }
| ParamDef::Enum { name, .. }
| ParamDef::Icon { name, .. } => name,
}
}
pub fn coerce_portable(&self, v: PortableValue) -> Result<ParamValue, ParamTypeMismatch> {
let actual = v.kind_label();
let mismatch = |expected: &'static str| Err(ParamTypeMismatch { expected, actual });
match self {
ParamDef::Bool { .. } => match v {
PortableValue::Bool(b) => Ok(ParamValue::Bool(b)),
_ => mismatch("bool"),
},
ParamDef::Int { .. } | ParamDef::Enum { .. } => match v {
PortableValue::Int(i) => Ok(ParamValue::Int(i as i32)),
_ => mismatch("integer"),
},
ParamDef::Float { .. } | ParamDef::FloatInput { .. } => match v {
PortableValue::Float(f) => Ok(ParamValue::Float(f as f32)),
PortableValue::Int(i) => Ok(ParamValue::Float(i as f32)),
_ => mismatch("number"),
},
ParamDef::String { .. } | ParamDef::Icon { .. } => match v {
PortableValue::String(s) => Ok(ParamValue::String(s)),
_ => mismatch("string"),
},
ParamDef::Curve { .. } => match v {
PortableValue::Curve(c) => Ok(ParamValue::Curve(c)),
_ => mismatch("curve (list of [x, y] pairs)"),
},
}
}
}
#[derive(Debug)]
pub struct ParamTypeMismatch {
pub expected: &'static str,
pub actual: &'static str,
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum PortableValue {
Bool(bool),
Int(i64),
Float(f64),
String(String),
Curve(Vec<[f32; 2]>),
}
impl PortableValue {
pub fn from_param(p: &ParamValue) -> Self {
match p {
ParamValue::Bool(b) => Self::Bool(*b),
ParamValue::Int(i) => Self::Int(*i as i64),
ParamValue::Float(f) => Self::Float(*f as f64),
ParamValue::String(s) => Self::String(s.clone()),
ParamValue::Curve(c) => Self::Curve(c.clone()),
}
}
fn kind_label(&self) -> &'static str {
match self {
Self::Bool(_) => "bool",
Self::Int(_) => "integer",
Self::Float(_) => "float",
Self::String(_) => "string",
Self::Curve(_) => "curve",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn paramvalue_round_trips_preserve_variant() {
for v in [
ParamValue::Bool(true),
ParamValue::Bool(false),
ParamValue::Int(0),
ParamValue::Int(1),
ParamValue::Int(-3),
ParamValue::Float(0.0),
ParamValue::Float(1.0),
ParamValue::Float(1.5),
ParamValue::Float(-2.25),
ParamValue::String("hello".into()),
ParamValue::Curve(vec![[0.0, 0.0], [1.0, 1.0]]),
] {
let json = serde_json::to_string(&v).unwrap();
let back: ParamValue = serde_json::from_str(&json).unwrap();
let ok = match (&v, &back) {
(ParamValue::Bool(a), ParamValue::Bool(b)) => a == b,
(ParamValue::Int(a), ParamValue::Int(b)) => a == b,
(ParamValue::Float(a), ParamValue::Float(b)) => a == b,
(ParamValue::String(a), ParamValue::String(b)) => a == b,
(ParamValue::Curve(a), ParamValue::Curve(b)) => a == b,
_ => false,
};
assert!(ok, "round-trip changed variant: {v:?} → {json} → {back:?}");
}
}
}