1use comnum_core::{Error, Result};
5use comnum_sim::grid::{Condition, Value};
6use serde_json::{Map, Number, Value as Json};
7
8pub(crate) fn to_json(v: &Value) -> Result<Json> {
10 Ok(match v {
11 Value::F64(x) => Json::Number(
12 Number::from_f64(*x).ok_or_else(|| Error::Config(format!("the value {x} cannot be substituted")))?,
13 ),
14 Value::I64(x) => Json::Number((*x).into()),
15 Value::Str(s) => Json::String(s.clone()),
16 Value::Bool(b) => Json::Bool(*b),
17 })
18}
19
20pub(crate) fn from_json(key: &str, v: &Json) -> Result<Value> {
22 match v {
23 Json::Number(n) => Ok(match n.as_i64() {
24 Some(i) => Value::I64(i),
25 None => Value::F64(n.as_f64().unwrap_or(f64::NAN)),
26 }),
27 Json::String(s) => Ok(Value::Str(s.clone())),
28 Json::Bool(b) => Ok(Value::Bool(*b)),
29 other => {
30 Err(Error::Config(format!("grid value of {key:?} must be a number, a string or a boolean, got {other}")))
31 }
32 }
33}
34
35fn is_name(s: &str) -> bool {
37 let mut chars = s.chars();
38 matches!(chars.next(), Some(c) if c.is_ascii_alphabetic() || c == '_')
39 && chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.')
40}
41
42fn lookup<'a>(cond: &'a Condition, name: &str) -> Result<&'a Value> {
43 cond.get(name).ok_or_else(|| {
44 let keys: Vec<&str> = cond.keys().collect();
45 Error::Config(format!("unknown variable ${name}: the grid defines {keys:?}"))
46 })
47}
48
49pub fn variable(s: &str) -> Option<&str> {
61 let name = s.strip_prefix('$')?;
62 let name = name.strip_prefix('{').and_then(|n| n.strip_suffix('}')).unwrap_or(name);
63 is_name(name).then_some(name)
64}
65
66pub fn substitute(tree: &Json, cond: &Condition) -> Result<Json> {
88 Ok(match tree {
89 Json::String(s) => substitute_str(s, cond)?,
90 Json::Array(a) => Json::Array(a.iter().map(|v| substitute(v, cond)).collect::<Result<_>>()?),
91 Json::Object(o) => {
92 let mut m = Map::new();
93 for (k, v) in o {
94 m.insert(k.clone(), substitute(v, cond)?);
95 }
96 Json::Object(m)
97 }
98 other => other.clone(),
99 })
100}
101
102fn substitute_str(s: &str, cond: &Condition) -> Result<Json> {
103 if let Some(name) = variable(s) {
104 return to_json(lookup(cond, name)?);
105 }
106 if !s.contains('$') {
107 return Ok(Json::String(s.to_string()));
108 }
109 let mut out = String::new();
110 let mut rest = s;
111 while let Some(i) = rest.find('$') {
112 out.push_str(&rest[..i]);
113 let tail = &rest[i + 1..];
114 if let Some(t) = tail.strip_prefix('$') {
115 out.push('$');
116 rest = t;
117 } else if let Some(t) = tail.strip_prefix('{') {
118 let end = t.find('}').ok_or_else(|| Error::Config(format!("unclosed ${{ in {s:?}")))?;
119 out.push_str(&lookup(cond, &t[..end])?.to_string());
120 rest = &t[end + 1..];
121 } else {
122 out.push('$');
123 rest = tail;
124 }
125 }
126 out.push_str(rest);
127 Ok(Json::String(out))
128}
129
130pub(crate) fn split_block(v: &Json) -> Result<(String, Option<String>, Json)> {
132 match v {
133 Json::String(s) => Ok((s.clone(), None, Json::Object(Map::new()))),
134 Json::Object(o) => {
135 let mut o = o.clone();
136 let kind = match o.remove("type") {
137 Some(Json::String(t)) => t,
138 Some(other) => return Err(Error::Config(format!("block type must be a string, got {other}"))),
139 None => return Err(Error::Config(format!("block without a type: {v}"))),
140 };
141 let label = match o.remove("label") {
142 Some(Json::String(l)) => Some(l),
143 None => None,
144 Some(other) => return Err(Error::Config(format!("block label must be a string, got {other}"))),
145 };
146 Ok((kind, label, Json::Object(o)))
147 }
148 other => Err(Error::Config(format!("a block is a name or a mapping with a type, got {other}"))),
149 }
150}
151
152pub fn parse<T: serde::de::DeserializeOwned>(kind: &str, v: Json) -> Result<T> {
157 serde_json::from_value(v).map_err(|e| Error::Config(format!("block {kind:?}: {e}")))
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use serde_json::json;
164
165 #[test]
166 fn variables_keep_their_type_or_interpolate() {
167 let c = Condition::new().with("snr_db", 12.5).with("m", 16usize).with("kind", "qam");
168 let t = json!({"a": "$snr_db", "b": ["${m}", "x"], "c": "out/${kind}_${m}.csv", "d": "$$5", "e": 3});
169 let r = substitute(&t, &c).unwrap();
170 assert_eq!(r, json!({"a": 12.5, "b": [16, "x"], "c": "out/qam_16.csv", "d": "$5", "e": 3}));
171 assert!(substitute(&json!("$missing"), &c).unwrap_err().to_string().contains("missing"));
172 }
173
174 #[test]
175 fn blocks_split_into_type_label_and_parameters() {
176 let (k, l, p) = split_block(&json!({"type": "awgn", "label": "rx", "snr_db": 3})).unwrap();
177 assert_eq!((k.as_str(), l.as_deref(), p), ("awgn", Some("rx"), json!({"snr_db": 3})));
178 assert_eq!(split_block(&json!("bps")).unwrap().0, "bps");
179 assert!(split_block(&json!({"snr_db": 3})).is_err());
180 }
181}