use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QuartoSchema {
pub version: String,
pub roots: Roots,
pub defs: BTreeMap<String, SchemaNode>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Roots {
pub frontmatter: String,
pub project: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cell_knitr: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cell_jupyter: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "t", rename_all = "lowercase")]
pub enum SchemaNode {
Any,
String,
Number,
Boolean,
Null,
Enum {
values: Vec<serde_json::Value>,
},
Array {
#[serde(default, skip_serializing_if = "Option::is_none")]
items: Option<Box<SchemaNode>>,
},
Object {
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
properties: BTreeMap<String, SchemaNode>,
#[serde(default, skip_serializing_if = "is_false")]
closed: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pattern: Vec<PatternProp>,
},
AnyOf {
of: Vec<SchemaNode>,
},
AllOf {
of: Vec<SchemaNode>,
},
Ref {
id: String,
},
}
impl<'de> Deserialize<'de> for SchemaNode {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
use serde::de::Error;
#[derive(Deserialize)]
#[serde(rename_all = "lowercase")]
enum Kind {
Any,
String,
Number,
Boolean,
Null,
Enum,
Array,
Object,
AnyOf,
AllOf,
Ref,
}
#[derive(Deserialize)]
struct Fields {
t: Kind,
values: Option<Vec<serde_json::Value>>,
items: Option<Box<SchemaNode>>,
#[serde(default)]
properties: BTreeMap<String, SchemaNode>,
#[serde(default)]
closed: bool,
#[serde(default)]
pattern: Vec<PatternProp>,
of: Option<Vec<SchemaNode>>,
id: Option<String>,
}
let fields = Fields::deserialize(deserializer)?;
Ok(match fields.t {
Kind::Any => Self::Any,
Kind::String => Self::String,
Kind::Number => Self::Number,
Kind::Boolean => Self::Boolean,
Kind::Null => Self::Null,
Kind::Enum => Self::Enum {
values: fields
.values
.ok_or_else(|| D::Error::missing_field("values"))?,
},
Kind::Array => Self::Array {
items: fields.items,
},
Kind::Object => Self::Object {
properties: fields.properties,
closed: fields.closed,
pattern: fields.pattern,
},
Kind::AnyOf => Self::AnyOf {
of: fields.of.ok_or_else(|| D::Error::missing_field("of"))?,
},
Kind::AllOf => Self::AllOf {
of: fields.of.ok_or_else(|| D::Error::missing_field("of"))?,
},
Kind::Ref => Self::Ref {
id: fields.id.ok_or_else(|| D::Error::missing_field("id"))?,
},
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PatternProp {
pub re: String,
pub schema: Box<SchemaNode>,
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn is_false(b: &bool) -> bool {
!*b
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn schema_nodes_round_trip_independently_of_field_order() {
let nodes = [
json!({"t": "any"}),
json!({"t": "string"}),
json!({"t": "number"}),
json!({"t": "boolean"}),
json!({"t": "null"}),
json!({"values": ["a", 1, true, null], "t": "enum"}),
json!({"items": {"t": "string"}, "t": "array"}),
json!({"t": "array"}),
json!({"t": "object"}),
json!({"properties": {"a": {"t": "boolean"}}, "closed": true,
"pattern": [{"re": "^x", "schema": {"t": "number"}}], "t": "object"}),
json!({"of": [{"t": "string"}, {"t": "null"}], "t": "anyof"}),
json!({"of": [{"t": "object"}], "t": "allof"}),
json!({"id": "front-matter", "t": "ref"}),
];
for value in nodes {
let node: SchemaNode = serde_json::from_str(&value.to_string()).unwrap();
assert_eq!(serde_json::to_value(node).unwrap(), value);
}
}
#[test]
fn schema_nodes_reject_missing_required_fields() {
for input in [
"{}",
r#"{"t":"unknown"}"#,
r#"{"t":"enum"}"#,
r#"{"t":"anyof"}"#,
r#"{"t":"allof"}"#,
r#"{"t":"ref"}"#,
] {
assert!(
serde_json::from_str::<SchemaNode>(input).is_err(),
"{input}"
);
}
}
}