use super::object_schema;
use comnum_core::{Error, Result};
use serde::de::DeserializeOwned;
use serde_json::{Map, Value as Json, json};
use std::fmt::Write;
#[derive(Clone, Debug)]
pub enum ParamDefault {
Required,
Absent(&'static str),
Value(Json, String),
}
#[derive(Clone, Debug)]
pub struct ParamInfo {
pub name: &'static str,
pub default: ParamDefault,
pub unit: &'static str,
pub choices: &'static [&'static str],
pub gt: Option<f64>,
pub ge: Option<f64>,
pub le: Option<f64>,
pub lt: Option<f64>,
pub doc: &'static str,
pub schema: Json,
pub variants: Vec<(&'static str, Vec<ParamInfo>)>,
pub fields: Vec<ParamInfo>,
}
impl ParamInfo {
fn bounds(&self) -> Vec<String> {
[(">", self.gt), (">=", self.ge), ("<=", self.le), ("<", self.lt)]
.iter()
.filter_map(|(op, b)| b.map(|b| format!("{op} {b}")))
.collect()
}
pub fn describe(&self, indent: usize) -> String {
let mut doc = self.doc.to_string();
if !self.unit.is_empty() {
let _ = write!(doc, " [{}]", self.unit);
}
if !self.choices.is_empty() {
let _ = write!(doc, " ({})", self.choices.join(", "));
}
let bounds = self.bounds();
if !bounds.is_empty() {
let _ = write!(doc, " ({})", bounds.join(", "));
}
let pad = " ".repeat(indent);
let mut s = format!("{pad}{:<20} {:<14} {doc}\n", self.name, self.default_text());
for p in &self.fields {
s.push_str(&p.describe(indent + 4));
}
for (kind, params) in &self.variants {
let _ = writeln!(s, "{pad} {{type: {kind}}}");
for p in params {
s.push_str(&p.describe(indent + 4));
}
}
s
}
pub fn default_text(&self) -> String {
match &self.default {
ParamDefault::Required => "required".into(),
ParamDefault::Absent(t) => (*t).into(),
ParamDefault::Value(_, t) => t.clone(),
}
}
pub fn json_schema(&self) -> Json {
let mut s = match &self.schema {
Json::Object(o) => o.clone(),
_ => Map::new(),
};
let mut doc = self.doc.to_string();
if !self.unit.is_empty() {
s.insert("x-unit".into(), json!(self.unit));
let _ = write!(doc, " [{}]", self.unit);
}
s.insert("description".into(), json!(doc));
if !self.choices.is_empty() {
match s.get_mut("items") {
Some(Json::Object(items)) => {
items.insert("enum".into(), json!(self.choices));
}
_ => {
s.insert("enum".into(), json!(self.choices));
}
}
}
let bound = |b: f64| Json::from(b);
if let Some(b) = self.gt {
s.insert("exclusiveMinimum".into(), bound(b));
}
if let Some(b) = self.ge {
s.insert("minimum".into(), bound(b));
}
if let Some(b) = self.le {
s.insert("maximum".into(), bound(b));
}
if let Some(b) = self.lt {
s.insert("exclusiveMaximum".into(), bound(b));
}
match &self.default {
ParamDefault::Value(v, _) => {
s.insert("default".into(), v.clone());
}
ParamDefault::Absent(t) => {
s.insert("x-default".into(), json!(t));
}
ParamDefault::Required => {}
}
json!({"anyOf": [Json::Object(s), {"type": "string", "pattern": "^\\$", "description": "a grid variable"}]})
}
#[cfg(test)]
pub(crate) fn check(&self, kind: &str, v: &Json) -> Result<()> {
self.accept(kind, v.clone()).map(drop)
}
pub(crate) fn accept(&self, kind: &str, mut v: Json) -> Result<Json> {
if !self.choices.is_empty() {
let mut all = true;
let mut canonical = |x: &mut Json| {
if let Json::String(s) = x {
match choice(self.choices, s) {
Some(c) => *s = c.to_string(),
None => all = false,
}
}
};
match &mut v {
Json::Array(a) => a.iter_mut().for_each(&mut canonical),
x => canonical(x),
}
if !all {
return Err(Error::Config(format!(
"block {kind:?}: {} must be one of {:?}, got {v}",
self.name, self.choices
)));
}
}
let numbers: Vec<f64> = match &v {
Json::Number(n) => n.as_f64().into_iter().collect(),
Json::Array(a) => a.iter().filter_map(Json::as_f64).collect(),
_ => Vec::new(),
};
for x in numbers {
let fails = self.gt.is_some_and(|b| x <= b)
|| self.ge.is_some_and(|b| x < b)
|| self.le.is_some_and(|b| x > b)
|| self.lt.is_some_and(|b| x >= b);
if fails {
return Err(Error::Config(format!(
"block {kind:?}: {} must be {}, got {x}",
self.name,
self.bounds().join(" and ")
)));
}
}
Ok(v)
}
}
pub fn choice(choices: &[&'static str], s: &str) -> Option<&'static str> {
choices.iter().copied().find(|c| c.eq_ignore_ascii_case(s))
}
pub trait ParamType {
fn schema() -> Json;
fn variants() -> Vec<(&'static str, Vec<ParamInfo>)> {
Vec::new()
}
fn fields() -> Vec<ParamInfo> {
Vec::new()
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Mapping<T> {
pub value: Json,
_params: std::marker::PhantomData<fn() -> T>,
}
impl<'de, T> serde::Deserialize<'de> for Mapping<T> {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
Ok(Mapping { value: Json::deserialize(d)?, _params: std::marker::PhantomData })
}
}
impl<T: Params> Mapping<T> {
pub fn parse(&self, kind: &str) -> Result<T> {
T::parse(kind, self.value.clone())
}
}
impl<T: Params> ParamType for Mapping<T> {
fn schema() -> Json {
Json::Object(object_schema(&T::params(), &[]))
}
fn fields() -> Vec<ParamInfo> {
T::params()
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct NameOr<T>(pub Mapping<T>);
#[derive(Clone, Debug, PartialEq)]
pub enum Named<'a, T> {
Name(&'a str),
Params(T),
}
impl<'de, T> serde::Deserialize<'de> for NameOr<T> {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
Mapping::deserialize(d).map(NameOr)
}
}
impl<T: Params> NameOr<T> {
pub fn get(&self, kind: &str) -> Result<Named<'_, T>> {
match &self.0.value {
Json::String(s) => Ok(Named::Name(s)),
Json::Object(_) => self.0.parse(kind).map(Named::Params),
other => Err(Error::Config(format!("{kind}: a name or a mapping, got {other}"))),
}
}
pub fn json(&self) -> &Json {
&self.0.value
}
}
impl<T: Params> ParamType for NameOr<T> {
fn schema() -> Json {
json!({"anyOf": [{"type": "string"}, Mapping::<T>::schema()]})
}
fn fields() -> Vec<ParamInfo> {
T::params()
}
}
impl ParamType for f64 {
fn schema() -> Json {
json!({"type": "number"})
}
}
macro_rules! unsigned {
($($t:ty),*) => {$(
impl ParamType for $t {
fn schema() -> Json {
json!({"type": "integer", "minimum": 0})
}
}
)*};
}
unsigned!(usize, u64, u32, u8);
impl ParamType for i64 {
fn schema() -> Json {
json!({"type": "integer"})
}
}
impl ParamType for bool {
fn schema() -> Json {
json!({"type": "boolean"})
}
}
impl ParamType for String {
fn schema() -> Json {
json!({"type": "string"})
}
}
impl ParamType for Json {
fn schema() -> Json {
json!({})
}
}
impl<T: ParamType> ParamType for Option<T> {
fn schema() -> Json {
T::schema()
}
fn variants() -> Vec<(&'static str, Vec<ParamInfo>)> {
T::variants()
}
fn fields() -> Vec<ParamInfo> {
T::fields()
}
}
impl<T: ParamType, const N: usize> ParamType for [T; N] {
fn schema() -> Json {
json!({"type": "array", "items": T::schema(), "minItems": N, "maxItems": N})
}
}
impl<T: ParamType> ParamType for Vec<T> {
fn schema() -> Json {
json!({"type": "array", "items": T::schema()})
}
fn fields() -> Vec<ParamInfo> {
T::fields()
}
}
json_param! {
NumberOrList,
json!({"anyOf": [{"type": "number"}, {"type": "array", "items": {"type": "number"}}]})
}
pub trait Params: Sized {
fn params() -> Vec<ParamInfo>;
fn parse(kind: &str, v: Json) -> Result<Self> {
let params = Self::params();
let mut m = mapping(kind, v, ¶ms)?;
Self::take_from(kind, &mut m, ¶ms)
}
fn take_from(kind: &str, m: &mut Map<String, Json>, params: &[ParamInfo]) -> Result<Self>;
}
pub fn mapping(kind: &str, v: Json, params: &[ParamInfo]) -> Result<Map<String, Json>> {
let m = match v {
Json::Object(m) => m,
Json::Null => Map::new(),
other => return Err(Error::Config(format!("block {kind:?}: the parameters must be a mapping, got {other}"))),
};
if let Some(unknown) = m.keys().find(|k| !params.iter().any(|p| p.name == k.as_str())) {
let names: Vec<String> = params.iter().map(|p| format!("`{}`", p.name)).collect();
let expected = match names.len() {
0 => "there are no fields".to_string(),
1 => format!("expected {}", names[0]),
_ => format!("expected one of {}", names.join(", ")),
};
return Err(Error::Config(format!("block {kind:?}: unknown field `{unknown}`, {expected}")));
}
Ok(m)
}
pub fn take<T: DeserializeOwned>(kind: &str, m: &mut Map<String, Json>, params: &[ParamInfo], name: &str) -> Result<T> {
let Some(p) = params.iter().find(|p| p.name == name) else {
return Err(Error::Config(format!("block {kind:?}: undeclared parameter {name}")));
};
let v = match m.remove(name) {
Some(v) => p.accept(kind, v)?,
None => match &p.default {
ParamDefault::Required => return Err(Error::Config(format!("block {kind:?}: missing field `{name}`"))),
ParamDefault::Absent(_) => Json::Null,
ParamDefault::Value(v, _) => v.clone(),
},
};
serde_json::from_value(v).map_err(|e| Error::Config(format!("block {kind:?}: {name}: {e}")))
}
pub fn shown(tokens: &str) -> String {
tokens.strip_prefix('"').and_then(|t| t.strip_suffix('"')).unwrap_or(tokens).to_string()
}