Skip to main content

comnum_spec/blocks/schema/
param.rs

1//! The parameters of a block: their documentation ([`ParamInfo`]), the JSON Schema of
2//! their types ([`ParamType`]), the values given as a mapping ([`Mapping`], [`NameOr`])
3//! and the parser of a mapping of parameters ([`Params`]).
4
5use super::object_schema;
6use comnum_core::{Error, Result};
7use serde::de::DeserializeOwned;
8use serde_json::{Map, Value as Json, json};
9use std::fmt::Write;
10
11/// The default of a parameter.
12#[derive(Clone, Debug)]
13pub enum ParamDefault {
14    /// The parameter must be given.
15    Required,
16    /// An optional parameter left out; the text says what the block then does.
17    Absent(&'static str),
18    /// A value, with the text that shows it.
19    Value(Json, String),
20}
21
22/// One parameter of a block.
23#[derive(Clone, Debug)]
24pub struct ParamInfo {
25    /// Name.
26    pub name: &'static str,
27    /// Default.
28    pub default: ParamDefault,
29    /// Unit (`dB`, `Hz`, `ps/nm/km`…; empty for a number without unit or a non-number).
30    pub unit: &'static str,
31    /// The accepted values of a string (empty: any value of the type).
32    pub choices: &'static [&'static str],
33    /// Exclusive lower bound of a number.
34    pub gt: Option<f64>,
35    /// Inclusive lower bound of a number.
36    pub ge: Option<f64>,
37    /// Inclusive upper bound of a number.
38    pub le: Option<f64>,
39    /// Exclusive upper bound of a number.
40    pub lt: Option<f64>,
41    /// Meaning.
42    pub doc: &'static str,
43    /// The JSON Schema of the type of the value (`{"type": "number"}`…).
44    pub schema: Json,
45    /// The mappings the value may be, by their `type`, each with its parameters (the
46    /// codes of `fec_encode`, see [`ParamType::variants`]); empty for any other value.
47    pub variants: Vec<(&'static str, Vec<ParamInfo>)>,
48    /// The keys of a value that is a mapping of parameters ([`Mapping`], [`NameOr`],
49    /// see [`ParamType::fields`]); empty for any other value.
50    pub fields: Vec<ParamInfo>,
51}
52
53impl ParamInfo {
54    /// The bounds of a number, `> 0`, `<= 1`…
55    fn bounds(&self) -> Vec<String> {
56        [(">", self.gt), (">=", self.ge), ("<=", self.le), ("<", self.lt)]
57            .iter()
58            .filter_map(|(op, b)| b.map(|b| format!("{op} {b}")))
59            .collect()
60    }
61
62    /// The parameter as one line of text (name, default, meaning, unit, accepted values,
63    /// bounds), then one line per key of its mapping and per parameter of every variant
64    /// of its value, indented.
65    pub fn describe(&self, indent: usize) -> String {
66        let mut doc = self.doc.to_string();
67        if !self.unit.is_empty() {
68            let _ = write!(doc, " [{}]", self.unit);
69        }
70        if !self.choices.is_empty() {
71            let _ = write!(doc, " ({})", self.choices.join(", "));
72        }
73        let bounds = self.bounds();
74        if !bounds.is_empty() {
75            let _ = write!(doc, " ({})", bounds.join(", "));
76        }
77        let pad = " ".repeat(indent);
78        let mut s = format!("{pad}{:<20} {:<14} {doc}\n", self.name, self.default_text());
79        for p in &self.fields {
80            s.push_str(&p.describe(indent + 4));
81        }
82        for (kind, params) in &self.variants {
83            let _ = writeln!(s, "{pad}  {{type: {kind}}}");
84            for p in params {
85                s.push_str(&p.describe(indent + 4));
86            }
87        }
88        s
89    }
90
91    /// The default as text: `required`, the value, or what an absent value means.
92    pub fn default_text(&self) -> String {
93        match &self.default {
94            ParamDefault::Required => "required".into(),
95            ParamDefault::Absent(t) => (*t).into(),
96            ParamDefault::Value(_, t) => t.clone(),
97        }
98    }
99
100    /// The JSON Schema of the parameter: its type, unit, accepted values, bounds,
101    /// default and meaning (a string `$name` of a grid variable is accepted too).
102    pub fn json_schema(&self) -> Json {
103        let mut s = match &self.schema {
104            Json::Object(o) => o.clone(),
105            _ => Map::new(),
106        };
107        let mut doc = self.doc.to_string();
108        if !self.unit.is_empty() {
109            s.insert("x-unit".into(), json!(self.unit));
110            let _ = write!(doc, " [{}]", self.unit);
111        }
112        s.insert("description".into(), json!(doc));
113        if !self.choices.is_empty() {
114            // the accepted values of a list are those of its items
115            match s.get_mut("items") {
116                Some(Json::Object(items)) => {
117                    items.insert("enum".into(), json!(self.choices));
118                }
119                _ => {
120                    s.insert("enum".into(), json!(self.choices));
121                }
122            }
123        }
124        let bound = |b: f64| Json::from(b);
125        if let Some(b) = self.gt {
126            s.insert("exclusiveMinimum".into(), bound(b));
127        }
128        if let Some(b) = self.ge {
129            s.insert("minimum".into(), bound(b));
130        }
131        if let Some(b) = self.le {
132            s.insert("maximum".into(), bound(b));
133        }
134        if let Some(b) = self.lt {
135            s.insert("exclusiveMaximum".into(), bound(b));
136        }
137        match &self.default {
138            ParamDefault::Value(v, _) => {
139                s.insert("default".into(), v.clone());
140            }
141            ParamDefault::Absent(t) => {
142                s.insert("x-default".into(), json!(t));
143            }
144            ParamDefault::Required => {}
145        }
146        json!({"anyOf": [Json::Object(s), {"type": "string", "pattern": "^\\$", "description": "a grid variable"}]})
147    }
148
149    /// Check a given value against the accepted values and the bounds.
150    #[cfg(test)]
151    pub(crate) fn check(&self, kind: &str, v: &Json) -> Result<()> {
152        self.accept(kind, v.clone()).map(drop)
153    }
154
155    /// Check a given value against the accepted values and the bounds, and return it
156    /// with every string among the accepted values in its declared spelling: the
157    /// accepted values are compared without case ([`choice`]), and the parser of the
158    /// block then sees the spelling it declared.
159    pub(crate) fn accept(&self, kind: &str, mut v: Json) -> Result<Json> {
160        if !self.choices.is_empty() {
161            let mut all = true;
162            let mut canonical = |x: &mut Json| {
163                if let Json::String(s) = x {
164                    match choice(self.choices, s) {
165                        Some(c) => *s = c.to_string(),
166                        None => all = false,
167                    }
168                }
169            };
170            match &mut v {
171                Json::Array(a) => a.iter_mut().for_each(&mut canonical),
172                x => canonical(x),
173            }
174            if !all {
175                return Err(Error::Config(format!(
176                    "block {kind:?}: {} must be one of {:?}, got {v}",
177                    self.name, self.choices
178                )));
179            }
180        }
181        let numbers: Vec<f64> = match &v {
182            Json::Number(n) => n.as_f64().into_iter().collect(),
183            Json::Array(a) => a.iter().filter_map(Json::as_f64).collect(),
184            _ => Vec::new(),
185        };
186        for x in numbers {
187            let fails = self.gt.is_some_and(|b| x <= b)
188                || self.ge.is_some_and(|b| x < b)
189                || self.le.is_some_and(|b| x > b)
190                || self.lt.is_some_and(|b| x >= b);
191            if fails {
192                return Err(Error::Config(format!(
193                    "block {kind:?}: {} must be {}, got {x}",
194                    self.name,
195                    self.bounds().join(" and ")
196                )));
197            }
198        }
199        Ok(v)
200    }
201}
202
203/// The accepted value that `s` names, compared without ASCII case: the one rule of the
204/// accepted values of the blocks, for the check of a parameter and its parsing alike.
205///
206/// # Examples
207/// ```
208/// use comnum_spec::blocks::schema::choice;
209/// assert_eq!(choice(&["signal", "unit"], "Unit"), Some("unit"));
210/// assert_eq!(choice(&["signal", "unit"], "peak"), None);
211/// ```
212pub fn choice(choices: &[&'static str], s: &str) -> Option<&'static str> {
213    choices.iter().copied().find(|c| c.eq_ignore_ascii_case(s))
214}
215
216/// The JSON Schema of the type of a parameter.
217pub trait ParamType {
218    /// The schema (`{"type": "number"}`, `{"type": "array", "items": …}`…).
219    fn schema() -> Json;
220    /// The mappings a value of the type may be, by their `type`, each with its
221    /// parameters ([`ParamInfo::variants`]): none by default.
222    fn variants() -> Vec<(&'static str, Vec<ParamInfo>)> {
223        Vec::new()
224    }
225    /// The keys of a value of the type that is a mapping of parameters
226    /// ([`ParamInfo::fields`]): none by default.
227    fn fields() -> Vec<ParamInfo> {
228        Vec::new()
229    }
230}
231
232/// The value of a parameter that is a mapping of the parameters `T` (declared with
233/// [`block_params!`](crate::block_params)), kept as given until the block parses it
234/// ([`Mapping::parse`]; the fibre blocks merge it over the fibre of the specification
235/// first): its keys are documented with those of the block.
236#[derive(Clone, Debug, Default, PartialEq)]
237pub struct Mapping<T> {
238    /// The mapping as given.
239    pub value: Json,
240    _params: std::marker::PhantomData<fn() -> T>,
241}
242
243impl<'de, T> serde::Deserialize<'de> for Mapping<T> {
244    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
245        Ok(Mapping { value: Json::deserialize(d)?, _params: std::marker::PhantomData })
246    }
247}
248
249impl<T: Params> Mapping<T> {
250    /// Read the parameters of the mapping (`kind` names it in the errors).
251    ///
252    /// # Errors
253    /// As [`Params::parse`].
254    pub fn parse(&self, kind: &str) -> Result<T> {
255        T::parse(kind, self.value.clone())
256    }
257}
258
259impl<T: Params> ParamType for Mapping<T> {
260    fn schema() -> Json {
261        Json::Object(object_schema(&T::params(), &[]))
262    }
263
264    fn fields() -> Vec<ParamInfo> {
265        T::params()
266    }
267}
268
269/// The value of a parameter that is a name or a mapping of the parameters `T` (a
270/// constellation `16-QAM` or `{kind: qam, m: 16}`, the pilots `none` or `{pattern: comb,
271/// …}`), kept as given until the block reads it ([`NameOr::get`]).
272#[derive(Clone, Debug, Default, PartialEq)]
273pub struct NameOr<T>(pub Mapping<T>);
274
275/// What a [`NameOr`] is.
276#[derive(Clone, Debug, PartialEq)]
277pub enum Named<'a, T> {
278    /// A name.
279    Name(&'a str),
280    /// A mapping, its parameters read.
281    Params(T),
282}
283
284impl<'de, T> serde::Deserialize<'de> for NameOr<T> {
285    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
286        Mapping::deserialize(d).map(NameOr)
287    }
288}
289
290impl<T: Params> NameOr<T> {
291    /// The name, or the parameters of the mapping (`kind` names it in the errors).
292    ///
293    /// # Errors
294    /// [`Error::Config`] for a value that is neither a string nor a valid mapping.
295    pub fn get(&self, kind: &str) -> Result<Named<'_, T>> {
296        match &self.0.value {
297            Json::String(s) => Ok(Named::Name(s)),
298            Json::Object(_) => self.0.parse(kind).map(Named::Params),
299            other => Err(Error::Config(format!("{kind}: a name or a mapping, got {other}"))),
300        }
301    }
302
303    /// The value as given.
304    pub fn json(&self) -> &Json {
305        &self.0.value
306    }
307}
308
309impl<T: Params> ParamType for NameOr<T> {
310    fn schema() -> Json {
311        json!({"anyOf": [{"type": "string"}, Mapping::<T>::schema()]})
312    }
313
314    fn fields() -> Vec<ParamInfo> {
315        T::params()
316    }
317}
318
319impl ParamType for f64 {
320    fn schema() -> Json {
321        json!({"type": "number"})
322    }
323}
324
325macro_rules! unsigned {
326    ($($t:ty),*) => {$(
327        impl ParamType for $t {
328            fn schema() -> Json {
329                json!({"type": "integer", "minimum": 0})
330            }
331        }
332    )*};
333}
334unsigned!(usize, u64, u32, u8);
335
336impl ParamType for i64 {
337    fn schema() -> Json {
338        json!({"type": "integer"})
339    }
340}
341
342impl ParamType for bool {
343    fn schema() -> Json {
344        json!({"type": "boolean"})
345    }
346}
347
348impl ParamType for String {
349    fn schema() -> Json {
350        json!({"type": "string"})
351    }
352}
353
354/// Any JSON value: a constellation name or mapping, a code, a list of taps…
355impl ParamType for Json {
356    fn schema() -> Json {
357        json!({})
358    }
359}
360
361impl<T: ParamType> ParamType for Option<T> {
362    fn schema() -> Json {
363        T::schema()
364    }
365
366    fn variants() -> Vec<(&'static str, Vec<ParamInfo>)> {
367        T::variants()
368    }
369
370    fn fields() -> Vec<ParamInfo> {
371        T::fields()
372    }
373}
374
375impl<T: ParamType, const N: usize> ParamType for [T; N] {
376    fn schema() -> Json {
377        json!({"type": "array", "items": T::schema(), "minItems": N, "maxItems": N})
378    }
379}
380
381impl<T: ParamType> ParamType for Vec<T> {
382    fn schema() -> Json {
383        json!({"type": "array", "items": T::schema()})
384    }
385
386    fn fields() -> Vec<ParamInfo> {
387        T::fields()
388    }
389}
390
391json_param! {
392    /// A number, or a list of numbers (one per stage, per channel…).
393    NumberOrList,
394    json!({"anyOf": [{"type": "number"}, {"type": "array", "items": {"type": "number"}}]})
395}
396
397/// The parameters of a block, declared with `block_params!`.
398pub trait Params: Sized {
399    /// The documentation of every parameter.
400    fn params() -> Vec<ParamInfo>;
401    /// Read the parameters of a block of type `kind` (a JSON mapping).
402    ///
403    /// # Errors
404    /// [`Error::Config`] for an unknown parameter, a missing one, a value of the wrong
405    /// type, out of its bounds or not among its accepted values.
406    fn parse(kind: &str, v: Json) -> Result<Self> {
407        let params = Self::params();
408        let mut m = mapping(kind, v, &params)?;
409        Self::take_from(kind, &mut m, &params)
410    }
411    /// Take the parameters from the mapping `m` of a block whose parameters are `params`
412    /// (these among them): the parser of a group of parameters shared by several blocks
413    /// (a `[flatten]` field of `block_params!`), whose unknown names
414    /// [`mapping`] rejected against the parameters of the whole block.
415    ///
416    /// # Errors
417    /// As [`Params::parse`].
418    fn take_from(kind: &str, m: &mut Map<String, Json>, params: &[ParamInfo]) -> Result<Self>;
419}
420
421/// The mapping of the parameters of a block, without unknown names.
422///
423/// # Errors
424/// [`Error::Config`] if `v` is not a mapping or has a name that `params` does not
425/// declare (the message lists the accepted ones, as serde does).
426pub fn mapping(kind: &str, v: Json, params: &[ParamInfo]) -> Result<Map<String, Json>> {
427    let m = match v {
428        Json::Object(m) => m,
429        Json::Null => Map::new(),
430        other => return Err(Error::Config(format!("block {kind:?}: the parameters must be a mapping, got {other}"))),
431    };
432    if let Some(unknown) = m.keys().find(|k| !params.iter().any(|p| p.name == k.as_str())) {
433        let names: Vec<String> = params.iter().map(|p| format!("`{}`", p.name)).collect();
434        let expected = match names.len() {
435            0 => "there are no fields".to_string(),
436            1 => format!("expected {}", names[0]),
437            _ => format!("expected one of {}", names.join(", ")),
438        };
439        return Err(Error::Config(format!("block {kind:?}: unknown field `{unknown}`, {expected}")));
440    }
441    Ok(m)
442}
443
444/// Take the value of the parameter `name` from the mapping, or its default.
445///
446/// # Errors
447/// [`Error::Config`] for a missing required parameter, a value of the wrong type, out of
448/// its bounds or not among its accepted values.
449pub fn take<T: DeserializeOwned>(kind: &str, m: &mut Map<String, Json>, params: &[ParamInfo], name: &str) -> Result<T> {
450    let Some(p) = params.iter().find(|p| p.name == name) else {
451        return Err(Error::Config(format!("block {kind:?}: undeclared parameter {name}")));
452    };
453    let v = match m.remove(name) {
454        Some(v) => p.accept(kind, v)?,
455        None => match &p.default {
456            ParamDefault::Required => return Err(Error::Config(format!("block {kind:?}: missing field `{name}`"))),
457            ParamDefault::Absent(_) => Json::Null,
458            ParamDefault::Value(v, _) => v.clone(),
459        },
460    };
461    serde_json::from_value(v).map_err(|e| Error::Config(format!("block {kind:?}: {name}: {e}")))
462}
463
464/// The text of a default written in `block_params!`: a string literal without its
465/// quotes, anything else as written.
466pub fn shown(tokens: &str) -> String {
467    tokens.strip_prefix('"').and_then(|t| t.strip_suffix('"')).unwrap_or(tokens).to_string()
468}