aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.toml5
-rw-r--r--src/lib.rs264
-rw-r--r--src/schema_schema.rs794
-rw-r--r--tests/kdl-schema.kdl373
-rw-r--r--tests/schema-via-knuffel.rs63
5 files changed, 1493 insertions, 6 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 67c95dc..3e6ff67 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,3 +6,8 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
+knuffel = { version = "1.1.0", optional = true }
+lazy_static = "1.4.0"
+
+[dev-dependencies]
+miette = { version = "3.3.0", features = ["fancy"] }
diff --git a/src/lib.rs b/src/lib.rs
index 1b4a90c..7f0966b 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,8 +1,260 @@
-#[cfg(test)]
-mod tests {
- #[test]
- fn it_works() {
- let result = 2 + 2;
- assert_eq!(result, 4);
+#[cfg(feature = "knuffel")]
+use knuffel::Decode;
+
+pub trait BuildFromRef {
+ fn ref_to(query: impl Into<String>) -> Self;
+}
+
+#[derive(Debug, PartialEq, Eq, Default)]
+#[cfg_attr(feature = "knuffel", derive(Decode))]
+pub struct Schema {
+ #[cfg_attr(feature = "knuffel", knuffel(child))]
+ pub document: Document,
+}
+
+#[derive(Debug, PartialEq, Eq, Default)]
+#[cfg_attr(feature = "knuffel", derive(Decode))]
+pub struct Document {
+ #[cfg_attr(feature = "knuffel", knuffel(child))]
+ pub info: Info,
+ #[cfg_attr(feature = "knuffel", knuffel(children(name = "node")))]
+ pub nodes: Vec<Node>,
+}
+
+pub use info::Info;
+
+pub mod info {
+ use super::*;
+
+ #[derive(Debug, PartialEq, Eq, Default)]
+ #[cfg_attr(feature = "knuffel", derive(Decode))]
+ pub struct Info {
+ #[cfg_attr(feature = "knuffel", knuffel(children(name = "title")))]
+ pub title: Vec<TextValue>,
+ #[cfg_attr(feature = "knuffel", knuffel(children(name = "description")))]
+ pub description: Vec<TextValue>,
+ #[cfg_attr(feature = "knuffel", knuffel(children(name = "author")))]
+ pub authors: Vec<Person>,
+ #[cfg_attr(feature = "knuffel", knuffel(children(name = "contributor")))]
+ pub contributors: Vec<Person>,
+ #[cfg_attr(feature = "knuffel", knuffel(children(name = "link")))]
+ pub links: Vec<Link>,
+ #[cfg_attr(feature = "knuffel", knuffel(children(name = "license")))]
+ pub licenses: Vec<License>,
+ #[cfg_attr(feature = "knuffel", knuffel(child))]
+ pub published: Option<Date>,
+ #[cfg_attr(feature = "knuffel", knuffel(child))]
+ pub modified: Option<Date>,
+ }
+
+ #[derive(Debug, PartialEq, Eq)]
+ #[cfg_attr(feature = "knuffel", derive(Decode))]
+ pub struct TextValue {
+ #[cfg_attr(feature = "knuffel", knuffel(argument))]
+ pub text: String,
+ #[cfg_attr(feature = "knuffel", knuffel(property))]
+ pub lang: Option<String>,
+ }
+
+ #[derive(Debug, PartialEq, Eq)]
+ #[cfg_attr(feature = "knuffel", derive(Decode))]
+ pub struct Person {
+ #[cfg_attr(feature = "knuffel", knuffel(argument))]
+ pub name: String,
+ #[cfg_attr(feature = "knuffel", knuffel(property))]
+ pub orcid: Option<String>,
+ #[cfg_attr(feature = "knuffel", knuffel(children(name = "link")))]
+ pub links: Vec<Link>,
+ }
+
+ #[derive(Debug, PartialEq, Eq)]
+ #[cfg_attr(feature = "knuffel", derive(Decode))]
+ pub struct Link {
+ #[cfg_attr(feature = "knuffel", knuffel(argument))]
+ pub iri: String,
+ #[cfg_attr(feature = "knuffel", knuffel(property))]
+ pub rel: Option<String>,
+ #[cfg_attr(feature = "knuffel", knuffel(property))]
+ pub lang: Option<String>,
+ }
+
+ #[derive(Debug, PartialEq, Eq)]
+ #[cfg_attr(feature = "knuffel", derive(Decode))]
+ pub struct License {
+ #[cfg_attr(feature = "knuffel", knuffel(argument))]
+ pub name: String,
+ #[cfg_attr(feature = "knuffel", knuffel(property))]
+ pub spdx: Option<String>,
+ #[cfg_attr(feature = "knuffel", knuffel(children(name = "link")))]
+ pub link: Vec<Link>,
+ }
+
+ #[derive(Debug, PartialEq, Eq)]
+ #[cfg_attr(feature = "knuffel", derive(Decode))]
+ pub struct Date {
+ #[cfg_attr(feature = "knuffel", knuffel(argument))]
+ pub date: String,
+ #[cfg_attr(feature = "knuffel", knuffel(property))]
+ pub time: Option<String>,
+ }
+}
+
+pub use node::Node;
+
+pub mod node {
+ use super::*;
+
+ #[derive(Debug, PartialEq, Eq, Default)]
+ #[cfg_attr(feature = "knuffel", derive(Decode))]
+ pub struct Node {
+ #[cfg_attr(feature = "knuffel", knuffel(argument))]
+ pub name: Option<String>,
+ #[cfg_attr(feature = "knuffel", knuffel(property))]
+ pub id: Option<String>,
+ #[cfg_attr(feature = "knuffel", knuffel(property))]
+ pub description: Option<String>,
+ #[cfg_attr(feature = "knuffel", knuffel(property))]
+ pub ref_: Option<String>,
+ #[cfg_attr(feature = "knuffel", knuffel(child, unwrap(argument)))]
+ pub min: Option<usize>,
+ #[cfg_attr(feature = "knuffel", knuffel(child, unwrap(argument)))]
+ pub max: Option<usize>,
+ #[cfg_attr(feature = "knuffel", knuffel(children(name = "prop")))]
+ pub props: Vec<Prop>,
+ #[cfg_attr(feature = "knuffel", knuffel(children(name = "value")))]
+ pub values: Vec<Value>,
+ #[cfg_attr(feature = "knuffel", knuffel(children(name = "children")))]
+ pub children: Vec<Children>,
+ }
+
+ impl BuildFromRef for Node {
+ fn ref_to(query: impl Into<String>) -> Self {
+ Self {
+ ref_: Some(query.into()),
+ ..Self::default()
+ }
+ }
+ }
+
+ #[derive(Debug, PartialEq, Eq, Default)]
+ #[cfg_attr(feature = "knuffel", derive(Decode))]
+ pub struct Prop {
+ #[cfg_attr(feature = "knuffel", knuffel(argument))]
+ pub key: Option<String>,
+ #[cfg_attr(feature = "knuffel", knuffel(property))]
+ pub id: Option<String>,
+ #[cfg_attr(feature = "knuffel", knuffel(property))]
+ pub description: Option<String>,
+ #[cfg_attr(feature = "knuffel", knuffel(property))]
+ pub ref_: Option<String>,
+ #[cfg_attr(feature = "knuffel", knuffel(child))]
+ pub required: bool,
+ #[cfg_attr(feature = "knuffel", knuffel(children))]
+ pub validations: Vec<Validation>,
+ }
+
+ impl BuildFromRef for Prop {
+ fn ref_to(query: impl Into<String>) -> Self {
+ Self {
+ ref_: Some(query.into()),
+ ..Self::default()
+ }
+ }
+ }
+
+ #[derive(Debug, PartialEq, Eq, Default)]
+ #[cfg_attr(feature = "knuffel", derive(Decode))]
+ pub struct Value {
+ #[cfg_attr(feature = "knuffel", knuffel(property))]
+ pub id: Option<String>,
+ #[cfg_attr(feature = "knuffel", knuffel(property))]
+ pub description: Option<String>,
+ #[cfg_attr(feature = "knuffel", knuffel(property))]
+ pub ref_: Option<String>,
+ #[cfg_attr(feature = "knuffel", knuffel(child, unwrap(argument)))]
+ pub min: Option<usize>,
+ #[cfg_attr(feature = "knuffel", knuffel(child, unwrap(argument)))]
+ pub max: Option<usize>,
+ #[cfg_attr(feature = "knuffel", knuffel(children))]
+ pub validations: Vec<Validation>,
+ }
+
+ impl BuildFromRef for Value {
+ fn ref_to(query: impl Into<String>) -> Self {
+ Self {
+ ref_: Some(query.into()),
+ ..Self::default()
+ }
+ }
+ }
+
+ #[derive(Debug, PartialEq, Eq, Default)]
+ #[cfg_attr(feature = "knuffel", derive(Decode))]
+ pub struct Children {
+ #[cfg_attr(feature = "knuffel", knuffel(property))]
+ pub id: Option<String>,
+ #[cfg_attr(feature = "knuffel", knuffel(property))]
+ pub description: Option<String>,
+ #[cfg_attr(feature = "knuffel", knuffel(property))]
+ pub ref_: Option<String>,
+ #[cfg_attr(feature = "knuffel", knuffel(children(name = "node")))]
+ pub nodes: Vec<Node>,
+ }
+
+ impl BuildFromRef for Children {
+ fn ref_to(query: impl Into<String>) -> Self {
+ Self {
+ ref_: Some(query.into()),
+ ..Self::default()
+ }
+ }
+ }
+
+ pub use validation::Validation;
+
+ pub mod validation {
+ #[cfg(feature = "knuffel")]
+ use knuffel::{Decode, DecodeScalar};
+
+ #[derive(Debug, PartialEq, Eq)]
+ #[cfg_attr(feature = "knuffel", derive(Decode))]
+ pub enum Validation {
+ Type(#[cfg_attr(feature = "knuffel", knuffel(argument))] String),
+ Enum(#[cfg_attr(feature = "knuffel", knuffel(arguments))] Vec<String>),
+ Pattern(#[cfg_attr(feature = "knuffel", knuffel(argument))] String),
+ Format(#[cfg_attr(feature = "knuffel", knuffel(arguments))] Vec<Format>),
+ }
+
+ #[derive(Debug, PartialEq, Eq)]
+ #[cfg_attr(feature = "knuffel", derive(DecodeScalar))]
+ pub enum Format {
+ DateTime,
+ Date,
+ Time,
+ Duration,
+ Decimal,
+ Currency,
+ Country2,
+ Country3,
+ CountrySubdivision,
+ Email,
+ IdnEmail,
+ Hostname,
+ IdnHostname,
+ Ipv4,
+ Ipv6,
+ Url,
+ UrlReference,
+ Irl,
+ IrlReference,
+ UrlTemplate,
+ Uuid,
+ Regex,
+ Base64,
+ KdlQuery,
+ }
}
}
+
+mod schema_schema;
+pub use schema_schema::SCHEMA_SCHEMA;
diff --git a/src/schema_schema.rs b/src/schema_schema.rs
new file mode 100644
index 0000000..d548f5a
--- /dev/null
+++ b/src/schema_schema.rs
@@ -0,0 +1,794 @@
+use lazy_static::lazy_static;
+
+use super::node::validation::*;
+use super::node::*;
+use super::*;
+
+lazy_static! {
+ pub static ref SCHEMA_SCHEMA: Schema = make_schema_schema();
+}
+
+fn s<T: From<String>>(text: &str) -> T {
+ T::from(text.to_string())
+}
+
+fn ref_to_id<T: BuildFromRef>(id: &str) -> T {
+ T::ref_to(format!(r#"[id="{}"]"#, id))
+}
+
+fn make_schema_schema() -> Schema {
+ Schema {
+ document: Document {
+ info: make_schema_info(),
+ nodes: vec![make_document_node()],
+ },
+ }
+}
+
+fn make_document_node() -> Node {
+ Node {
+ name: s("document"),
+ min: Some(1),
+ max: Some(1),
+ children: vec![Children {
+ id: s("node-children"),
+ nodes: vec![
+ Node {
+ name: s("node-names"),
+ id: s("node-names-node"),
+ description: s(
+ "Validations to apply specifically to arbitrary node names",
+ ),
+ children: vec![ref_to_id("validations")],
+ ..Node::default()
+ },
+ Node {
+ name: s("other-nodes-allowed"),
+ id: s("other-nodes-allowed-node"),
+ description: s("Whether to allow child nodes other than the ones explicitly listed. Defaults to 'false'."),
+ max: Some(1),
+ values: vec![Value {
+ min: Some(1),
+ max: Some(1),
+ validations: vec![Validation::Type(s("boolean"))],
+ ..Value::default()
+ }],
+ ..Node::default()
+ },
+ Node {
+ name: s("tag-names"),
+ description: s("Validations to apply specifically to arbitrary type tag names"),
+ children: vec![ref_to_id("validations")],
+ ..Node::default()
+ },
+ Node {
+ name: s("other-tags-allowed"),
+ description: s("Whether to allow child node tags other than the ones explicitly listed. Defaults to 'false'."),
+ max: Some(1),
+ values: vec![Value {
+ min: Some(1),
+ max: Some(1),
+ validations: vec![Validation::Type(s("boolean"))],
+ ..Value::default()
+ }],
+ ..Node::default()
+ },
+ make_info_node(),
+ Node {
+ name: s("tag"),
+ id: s("tag-node"),
+ description: s("A tag belonging to a child node of `document` or another node."),
+ values: vec![Value {
+ description: s("The name of the tag. If a tag name is not supplied, the node rules apply to _all_ nodes belonging to the parent."),
+ max: Some(1),
+ validations: vec![Validation::Type(s("string"))],
+ ..Value::default()
+ }],
+ props: vec![
+ Prop {
+ key: s("description"),
+ description: s("A description of this node's purpose."),
+ validations: vec![Validation::Type(s("string"))],
+ ..Prop::default()
+ },
+ Prop {
+ key: s("id"),
+ description: s("A globally-unique ID for this node."),
+ validations: vec![Validation::Type(s("string"))],
+ ..Prop::default()
+ },
+ Prop {
+ key: s("ref"),
+ description: s("A globally unique reference to another node."),
+ validations: vec![
+ Validation::Type(s("string")),
+ Validation::Format(vec![Format::KdlQuery]),
+ ],
+ ..Prop::default()
+ }
+ ],
+ children: vec![Children {
+ nodes: vec![
+ ref_to_id("node-names-node"),
+ ref_to_id("other-nodes-allowed-node"),
+ ref_to_id("node-node"),
+ ],
+ ..Children::default()
+ }],
+ ..Node::default()
+ },
+ make_node_node(),
+ Node {
+ name: s("definitions"),
+ description: s("Definitions to reference in parts of the top-level nodes"),
+ children: vec![Children {
+ nodes: vec![
+ ref_to_id("node-node"),
+ ref_to_id("value-node"),
+ ref_to_id("prop-node"),
+ ref_to_id("children-node"),
+ ref_to_id("tag-node"),
+ ],
+ ..Children::default()
+ }],
+ ..Node::default()
+ }
+ ],
+ ..Children::default()
+ }],
+ ..Node::default()
+ }
+}
+
+fn make_node_node() -> Node {
+ Node {
+ name: s("node"),
+ id: s("node-node"),
+ description: s("A child node belonging either to `document` or to another `node`. Nodes may be anonymous."),
+ values: vec![Value {
+ description: s("The name of the node. If a node name is not supplied, the node rules apply to _all_ nodes belonging to the parent."),
+ max: Some(1),
+ validations: vec![Validation::Type(s("string"))],
+ ..Value::default()
+ }],
+ props: vec![
+ Prop {
+ key: s("description"),
+ description: s("A description of this node's purpose."),
+ validations: vec![Validation::Type(s("string"))],
+ ..Prop::default()
+ },
+ Prop {
+ key: s("id"),
+ description: s("A globally-unique ID for this node."),
+ validations: vec![Validation::Type(s("string"))],
+ ..Prop::default()
+ },
+ Prop {
+ key: s("ref"),
+ description: s("A globally unique reference to another node."),
+ validations: vec![
+ Validation::Type(s("string")),
+ Validation::Format(vec![Format::KdlQuery]),
+ ],
+ ..Prop::default()
+ }
+ ],
+ children: vec![Children {
+ nodes: vec![
+ Node {
+ name: s("prop-names"),
+ description: s("Validations to apply specifically to arbitrary property names"),
+ children: vec![ref_to_id("validations")],
+ ..Node::default()
+ },
+ Node {
+ name: s("other-props-allowed"),
+ description: s("Whether to allow properties other than the ones explicitly listed. Defaults to 'false'."),
+ max: Some(1),
+ values: vec![Value {
+ min: Some(1),
+ max: Some(1),
+ validations: vec![Validation::Type(s("boolean"))],
+ ..Value::default()
+ }],
+ ..Node::default()
+ },
+ Node {
+ name: s("min"),
+ description: s("minimum number of instances of this node in its parent's children."),
+ max: Some(1),
+ values: vec![Value {
+ min: Some(1),
+ max: Some(1),
+ validations: vec![Validation::Type(s("number"))],
+ ..Value::default()
+ }],
+ ..Node::default()
+ },
+ Node {
+ name: s("max"),
+ description: s("maximum number of instances of this node in its parent's children."),
+ max: Some(1),
+ values: vec![Value {
+ min: Some(1),
+ max: Some(1),
+ validations: vec![Validation::Type(s("number"))],
+ ..Value::default()
+ }],
+ ..Node::default()
+ },
+ ref_to_id("value-tag-node"),
+ Node {
+ name: s("prop"),
+ id: s("prop-node"),
+ description: s("A node property key/value pair."),
+ values: vec![Value {
+ description: s("The property key."),
+ validations: vec![Validation::Type(s("string"))],
+ ..Value::default()
+ }],
+ props: vec![
+ Prop {
+ key: s("id"),
+ description: s("A globally-unique ID of this property."),
+ validations: vec![Validation::Type(s("string"))],
+ ..Prop::default()
+ },
+ Prop {
+ key: s("ref"),
+ description: s("A globally unique reference to another property node."),
+ validations: vec![
+ Validation::Type(s("string")),
+ Validation::Format(vec![Format::KdlQuery]),
+ ],
+ ..Prop::default()
+ },
+ Prop {
+ key: s("description"),
+ description: s("A description of this property's purpose."),
+ validations: vec![Validation::Type(s("string"))],
+ ..Prop::default()
+ },
+ ],
+ children: vec![
+ Children {
+ description: s("Property-specific validations."),
+ nodes: vec![Node {
+ name: s("required"),
+ description: s("Whether this property is required if its parent is present."),
+ max: Some(1),
+ values: vec![Value {
+ min: Some(1),
+ max: Some(1),
+ validations: vec![Validation::Type(s("boolean"))],
+ ..Value::default()
+ }],
+ ..Node::default()
+ }],
+ ..Children::default()
+ },
+ make_validations_children()
+ ],
+ ..Node::default()
+ },
+ Node {
+ name: s("value"),
+ id: s("value-node"),
+ description: s("one or more direct node values"),
+ props: vec![
+ Prop {
+ key: s("id"),
+ description: s("A globally-unique ID of this value."),
+ validations: vec![Validation::Type(s("string"))],
+ ..Prop::default()
+ },
+ Prop {
+ key: s("ref"),
+ description: s("A globally unique reference to another value node."),
+ validations: vec![
+ Validation::Type(s("string")),
+ Validation::Format(vec![Format::KdlQuery]),
+ ],
+ ..Prop::default()
+ },
+ Prop {
+ key: s("description"),
+ description: s("A description of this property's purpose."), // TODO report bug in original document
+ validations: vec![Validation::Type(s("string"))],
+ ..Prop::default()
+ },
+ ],
+ children: vec![
+ ref_to_id("validations"),
+ Children {
+ description: s("Node value-specific validations"),
+ nodes: vec![
+ Node {
+ name: s("min"),
+ description: s("minimum number of values for this node."),
+ max: Some(1),
+ values: vec![Value {
+ min: Some(1),
+ max: Some(1),
+ validations: vec![Validation::Type(s("number"))],
+ ..Value::default()
+ }],
+ ..Node::default()
+ },
+ Node {
+ name: s("max"),
+ description: s("maximum number of values for this node."),
+ max: Some(1),
+ values: vec![Value {
+ min: Some(1),
+ max: Some(1),
+ validations: vec![Validation::Type(s("number"))],
+ ..Value::default()
+ }],
+ ..Node::default()
+ },
+ ],
+ ..Children::default()
+ }
+ ],
+ ..Node::default()
+ },
+ Node {
+ name: s("children"),
+ id: s("children-node"),
+ props: vec![
+ Prop {
+ key: s("id"),
+ description: s("A globally-unique ID of this children node."),
+ validations: vec![Validation::Type(s("string"))],
+ ..Prop::default()
+ },
+ Prop {
+ key: s("ref"),
+ description: s("A globally unique reference to another children node."),
+ validations: vec![
+ Validation::Type(s("string")),
+ Validation::Format(vec![Format::KdlQuery]),
+ ],
+ ..Prop::default()
+ },
+ Prop {
+ key: s("description"),
+ description: s("A description of this these children's purpose."), // TODO report bug in original document
+ validations: vec![Validation::Type(s("string"))],
+ ..Prop::default()
+ },
+ ],
+ children: vec![ref_to_id("node-children")],
+ ..Node::default()
+ }
+ ],
+ ..Children::default()
+ }],
+ ..Node::default()
+ }
+}
+
+fn make_validations_children() -> Children {
+ Children {
+ id: s("validations"),
+ description: s("General value validations."),
+ nodes: vec![
+ Node {
+ name: s("tag"),
+ id: s("value-tag-node"),
+ description: s("The tags associated with this value"),
+ max: Some(1),
+ children: vec![ref_to_id("validations")],
+ ..Node::default()
+ },
+ Node {
+ name: s("type"),
+ description: s("The type for this prop's value."),
+ max: Some(1),
+ values: vec![Value {
+ min: Some(1),
+ validations: vec![Validation::Type(s("string"))],
+ ..Value::default()
+ }],
+ ..Node::default()
+ },
+ Node {
+ name: s("enum"),
+ description: s("An enumeration of possible values"),
+ max: Some(1),
+ values: vec![Value {
+ description: s("Enumeration choices"),
+ min: Some(1),
+ ..Value::default()
+ }],
+ ..Node::default()
+ },
+ Node {
+ name: s("pattern"),
+ description: s("PCRE (Regex) pattern or patterns to test prop values against."),
+ values: vec![Value {
+ min: Some(1),
+ validations: vec![Validation::Type(s("string"))],
+ ..Value::default()
+ }],
+ ..Node::default()
+ },
+ Node {
+ name: s("min-length"),
+ description: s("Minimum length of prop value, if it's a string."),
+ max: Some(1),
+ values: vec![Value {
+ min: Some(1),
+ validations: vec![Validation::Type(s("number"))],
+ ..Value::default()
+ }],
+ ..Node::default()
+ },
+ Node {
+ name: s("max-length"),
+ description: s("Maximum length of prop value, if it's a string."),
+ max: Some(1),
+ values: vec![Value {
+ min: Some(1),
+ validations: vec![Validation::Type(s("number"))],
+ ..Value::default()
+ }],
+ ..Node::default()
+ },
+ Node {
+ name: s("format"),
+ description: s("Intended data format."),
+ max: Some(1),
+ values: vec![Value {
+ min: Some(1),
+ validations: vec![
+ Validation::Type(s("string")),
+ Validation::Enum(vec![
+ s("date-time"),
+ s("date"),
+ s("time"),
+ s("duration"),
+ s("decimal"),
+ s("currency"),
+ s("country-2"),
+ s("country-3"),
+ s("country-subdivision"),
+ s("email"),
+ s("idn-email"),
+ s("hostname"),
+ s("idn-hostname"),
+ s("ipv4"),
+ s("ipv6"),
+ s("url"),
+ s("url-reference"),
+ s("irl"),
+ s("irl-reference"),
+ s("url-template"),
+ s("regex"),
+ s("uuid"),
+ s("kdl-query"),
+ s("i8"),
+ s("i16"),
+ s("i32"),
+ s("i64"),
+ s("u8"),
+ s("u16"),
+ s("u32"),
+ s("u64"),
+ s("isize"),
+ s("usize"),
+ s("f32"),
+ s("f64"),
+ s("decimal64"),
+ s("decimal128"),
+ ])
+ ],
+ ..Value::default()
+ }],
+ ..Node::default()
+ },
+ Node {
+ name: s("%"),
+ description: s("Only used for numeric values. Constrains them to be multiples of the given number(s)"),
+ max: Some(1),
+ values: vec![Value {
+ min: Some(1),
+ validations: vec![Validation::Type(s("number"))],
+ ..Value::default()
+ }],
+ ..Node::default()
+ },
+ Node {
+ name: s(">"),
+ description: s("Only used for numeric values. Constrains them to be greater than the given number(s)"),
+ max: Some(1),
+ values: vec![Value {
+ min: Some(1),
+ max: Some(1),
+ validations: vec![Validation::Type(s("number"))],
+ ..Value::default()
+ }],
+ ..Node::default()
+ },
+ Node {
+ name: s(">="),
+ description: s("Only used for numeric values. Constrains them to be greater than or equal to the given number(s)"),
+ max: Some(1),
+ values: vec![Value {
+ min: Some(1),
+ max: Some(1),
+ validations: vec![Validation::Type(s("number"))],
+ ..Value::default()
+ }],
+ ..Node::default()
+ },
+ Node {
+ name: s("<"),
+ description: s("Only used for numeric values. Constrains them to be less than the given number(s)"),
+ max: Some(1),
+ values: vec![Value {
+ min: Some(1),
+ max: Some(1),
+ validations: vec![Validation::Type(s("number"))],
+ ..Value::default()
+ }],
+ ..Node::default()
+ },
+ Node {
+ name: s("<="),
+ description: s("Only used for numeric values. Constrains them to be less than or equal to the given number(s)"),
+ max: Some(1),
+ values: vec![Value {
+ min: Some(1),
+ max: Some(1),
+ validations: vec![Validation::Type(s("number"))],
+ ..Value::default()
+ }],
+ ..Node::default()
+ },
+ ],
+ ..Children::default()
+ }
+}
+
+fn make_info_node() -> Node {
+ Node {
+ name: s("info"),
+ description: s("A child node that describes the schema itself."),
+ children: vec![Children {
+ nodes: vec![
+ Node {
+ name: s("title"),
+ description: s("The title of the schema or the format it describes"),
+ values: vec![Value {
+ description: s("The title text"),
+ min: Some(1),
+ max: Some(1),
+ validations: vec![Validation::Type(s("string"))],
+ ..Value::default()
+ }],
+ props: vec![Prop {
+ key: s("lang"),
+ id: s("info-lang"),
+ description: s("The language of the text"),
+ validations: vec![Validation::Type(s("string"))],
+ ..Prop::default()
+ }],
+ ..Node::default()
+ },
+ Node {
+ name: s("description"),
+ description: s("A description of the schema or the format it describes"),
+ values: vec![Value {
+ description: s("The description text"),
+ min: Some(1),
+ max: Some(1),
+ validations: vec![Validation::Type(s("string"))],
+ ..Value::default()
+ }],
+ props: vec![ref_to_id("info-lang")],
+ ..Node::default()
+ },
+ Node {
+ name: s("author"),
+ description: s("Author of the schema"),
+ values: vec![Value {
+ id: s("info-person-name"),
+ description: s("Person name"),
+ min: Some(1),
+ max: Some(1),
+ validations: vec![Validation::Type(s("string"))],
+ ..Value::default()
+ }],
+ props: vec![Prop {
+ key: s("orcid"),
+ id: s("info-orcid"),
+ description: s("The ORCID of the person"),
+ validations: vec![
+ Validation::Type(s("string")),
+ Validation::Pattern(s(r"\d{4}-\d{4}-\d{4}-\d{4}")),
+ ],
+ ..Prop::default()
+ }],
+ children: vec![Children {
+ nodes: vec![ref_to_id("info-link")],
+ ..Children::default()
+ }],
+ ..Node::default()
+ },
+ Node {
+ name: s("contributor"),
+ description: s("Contributor to the schema"),
+ values: vec![ref_to_id("info-person-name")],
+ props: vec![ref_to_id("info-orcid")],
+ ..Node::default()
+ },
+ Node {
+ name: s("link"),
+ id: s("info-link"),
+ description: s("Links to itself, and to sources describing it"),
+ values: vec![Value {
+ description: s("A URL that the link points to"),
+ min: Some(1),
+ max: Some(1),
+ validations: vec![
+ Validation::Type(s("string")),
+ Validation::Format(vec![Format::Url, Format::Irl]),
+ ],
+ ..Value::default()
+ }],
+ props: vec![
+ Prop {
+ key: s("rel"),
+ description: s("The relation between the current entity and the URL"),
+ validations: vec![
+ Validation::Type(s("string")),
+ Validation::Enum(vec![s("self"), s("documentation")]),
+ ],
+ ..Prop::default()
+ },
+ ref_to_id("info-lang"),
+ ],
+ ..Node::default()
+ },
+ Node {
+ name: s("license"),
+ description: s("The license(s) that the schema is licensed under"),
+ values: vec![Value {
+ description: s("Name of the used license"),
+ min: Some(1),
+ max: Some(1),
+ validations: vec![Validation::Type(s("string"))],
+ ..Value::default()
+ }],
+ props: vec![Prop {
+ key: s("spdx"),
+ description: s("An SPDX license identifier"),
+ validations: vec![Validation::Type(s("string"))],
+ ..Prop::default()
+ }],
+ children: vec![Children {
+ nodes: vec![ref_to_id("info-link")],
+ ..Children::default()
+ }],
+ ..Node::default()
+ },
+ Node {
+ name: s("published"),
+ description: s("When the schema was published"),
+ values: vec![Value {
+ description: s("Publication date"),
+ min: Some(1),
+ max: Some(1),
+ validations: vec![
+ Validation::Type(s("string")),
+ Validation::Format(vec![Format::Date]),
+ ],
+ ..Value::default()
+ }],
+ props: vec![Prop {
+ key: s("time"),
+ id: s("info-time"),
+ description: s("A time to accompany the date"),
+ validations: vec![
+ Validation::Type(s("string")),
+ Validation::Format(vec![Format::Time]),
+ ],
+ ..Prop::default()
+ }],
+ ..Node::default()
+ },
+ Node {
+ name: s("modified"),
+ description: s("When the schema was last modified"),
+ values: vec![Value {
+ description: s("Modification date"),
+ min: Some(1),
+ max: Some(1),
+ validations: vec![
+ Validation::Type(s("string")),
+ Validation::Format(vec![Format::Date]),
+ ],
+ ..Value::default()
+ }],
+ props: vec![ref_to_id("info-time")],
+ ..Node::default()
+ },
+ Node {
+ name: s("version"),
+ description: s("The version number of this version of the schema"),
+ values: vec![Value {
+ description: s("Semver version number"),
+ min: Some(1),
+ max: Some(1),
+ validations: vec![
+ Validation::Type(s("string")),
+ Validation::Pattern(s(
+ r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$",
+ )),
+ ],
+ ..Value::default()
+ }],
+ ..Node::default()
+ },
+ ],
+ ..Children::default()
+ }],
+ ..Node::default()
+ }
+}
+
+fn make_schema_info() -> Info {
+ Info {
+ title: vec![info::TextValue {
+ text: s("KDL Schema"),
+ lang: s("en"),
+ }],
+ description: vec![info::TextValue {
+ text: s("KDL Schema KDL schema in KDL"),
+ lang: s("en"),
+ }],
+ authors: vec![info::Person {
+ name: s("Kat Marchán"),
+ orcid: None,
+ links: vec![info::Link {
+ iri: s("https://github.com/zkat"),
+ rel: s("self"),
+ lang: None,
+ }],
+ }],
+ contributors: vec![info::Person {
+ name: s("Lars Willighagen"),
+ orcid: None,
+ links: vec![info::Link {
+ iri: s("https://github.com/larsgw"),
+ rel: s("self"),
+ lang: None,
+ }],
+ }],
+ links: vec![info::Link {
+ iri: s("https://github.com/zkat/kdl"),
+ rel: s("documentation"),
+ lang: None,
+ }],
+ licenses: vec![info::License {
+ name: s("Creative Commons Attribution-ShareAlike 4.0 International License"),
+ spdx: s("CC-BY-SA-4.0"),
+ link: vec![info::Link {
+ iri: s("https://creativecommons.org/licenses/by-sa/4.0/"),
+ rel: None,
+ lang: s("en"),
+ }],
+ }],
+ published: Some(info::Date {
+ date: s("2021-08-31"),
+ time: None,
+ }),
+ modified: Some(info::Date {
+ date: s("2021-09-01"),
+ time: None,
+ }),
+ }
+}
diff --git a/tests/kdl-schema.kdl b/tests/kdl-schema.kdl
new file mode 100644
index 0000000..4315199
--- /dev/null
+++ b/tests/kdl-schema.kdl
@@ -0,0 +1,373 @@
+document {
+ info {
+ title "KDL Schema" lang="en"
+ description "KDL Schema KDL schema in KDL" lang="en"
+ author "Kat Marchán" {
+ link "https://github.com/zkat" rel="self"
+ }
+ contributor "Lars Willighagen" {
+ link "https://github.com/larsgw" rel="self"
+ }
+ link "https://github.com/zkat/kdl" rel="documentation"
+ license "Creative Commons Attribution-ShareAlike 4.0 International License" spdx="CC-BY-SA-4.0" {
+ link "https://creativecommons.org/licenses/by-sa/4.0/" lang="en"
+ }
+ published "2021-08-31"
+ modified "2021-09-01"
+ }
+ node "document" {
+ min 1
+ max 1
+ children id="node-children" {
+ node "node-names" id="node-names-node" description="Validations to apply specifically to arbitrary node names" {
+ children ref=r#"[id="validations"]"#
+ }
+ node "other-nodes-allowed" id="other-nodes-allowed-node" description="Whether to allow child nodes other than the ones explicitly listed. Defaults to 'false'." {
+ max 1
+ value {
+ min 1
+ max 1
+ type "boolean"
+ }
+ }
+ node "tag-names" description="Validations to apply specifically to arbitrary type tag names" {
+ children ref=r#"[id="validations"]"#
+ }
+ node "other-tags-allowed" description="Whether to allow child node tags other than the ones explicitly listed. Defaults to 'false'." {
+ max 1
+ value {
+ min 1
+ max 1
+ type "boolean"
+ }
+ }
+ node "info" description="A child node that describes the schema itself." {
+ children {
+ node "title" description="The title of the schema or the format it describes" {
+ value description="The title text" {
+ type "string"
+ min 1
+ max 1
+ }
+ prop "lang" id="info-lang" description="The language of the text" {
+ type "string"
+ }
+ }
+ node "description" description="A description of the schema or the format it describes" {
+ value description="The description text" {
+ type "string"
+ min 1
+ max 1
+ }
+ prop ref=r#"[id="info-lang"]"#
+ }
+ node "author" description="Author of the schema" {
+ value id="info-person-name" description="Person name" {
+ type "string"
+ min 1
+ max 1
+ }
+ prop "orcid" id="info-orcid" description="The ORCID of the person" {
+ type "string"
+ pattern r"\d{4}-\d{4}-\d{4}-\d{4}"
+ }
+ children {
+ node ref=r#"[id="info-link"]"#
+ }
+ }
+ node "contributor" description="Contributor to the schema" {
+ value ref=r#"[id="info-person-name"]"#
+ prop ref=r#"[id="info-orcid"]"#
+ }
+ node "link" id="info-link" description="Links to itself, and to sources describing it" {
+ value description="A URL that the link points to" {
+ type "string"
+ format "url" "irl"
+ min 1
+ max 1
+ }
+ prop "rel" description="The relation between the current entity and the URL" {
+ type "string"
+ enum "self" "documentation"
+ }
+ prop ref=r#"[id="info-lang"]"#
+ }
+ node "license" description="The license(s) that the schema is licensed under" {
+ value description="Name of the used license" {
+ type "string"
+ min 1
+ max 1
+ }
+ prop "spdx" description="An SPDX license identifier" {
+ type "string"
+ }
+ children {
+ node ref=r#"[id="info-link"]"#
+ }
+ }
+ node "published" description="When the schema was published" {
+ value description="Publication date" {
+ type "string"
+ format "date"
+ min 1
+ max 1
+ }
+ prop "time" id="info-time" description="A time to accompany the date" {
+ type "string"
+ format "time"
+ }
+ }
+ node "modified" description="When the schema was last modified" {
+ value description="Modification date" {
+ type "string"
+ format "date"
+ min 1
+ max 1
+ }
+ prop ref=r#"[id="info-time"]"#
+ }
+ node "version" description="The version number of this version of the schema" {
+ value description="Semver version number" {
+ type "string"
+ pattern r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"
+ min 1
+ max 1
+ }
+ }
+ }
+ }
+ node "tag" id="tag-node" description="A tag belonging to a child node of `document` or another node." {
+ value description="The name of the tag. If a tag name is not supplied, the node rules apply to _all_ nodes belonging to the parent." {
+ type "string"
+ max 1
+ }
+ prop "description" description="A description of this node's purpose." {
+ type "string"
+ }
+ prop "id" description="A globally-unique ID for this node." {
+ type "string"
+ }
+ prop "ref" description="A globally unique reference to another node." {
+ type "string"
+ format "kdl-query"
+ }
+ children {
+ node ref=r#"[id="node-names-node"]"#
+ node ref=r#"[id="other-nodes-allowed-node"]"#
+ node ref=r#"[id="node-node"]"#
+ }
+ }
+ node "node" id="node-node" description="A child node belonging either to `document` or to another `node`. Nodes may be anonymous." {
+ value description="The name of the node. If a node name is not supplied, the node rules apply to _all_ nodes belonging to the parent." {
+ type "string"
+ max 1
+ }
+ prop "description" description="A description of this node's purpose." {
+ type "string"
+ }
+ prop "id" description="A globally-unique ID for this node." {
+ type "string"
+ }
+ prop "ref" description="A globally unique reference to another node." {
+ type "string"
+ format "kdl-query"
+ }
+ children {
+ node "prop-names" description="Validations to apply specifically to arbitrary property names" {
+ children ref=r#"[id="validations"]"#
+ }
+ node "other-props-allowed" description="Whether to allow properties other than the ones explicitly listed. Defaults to 'false'." {
+ max 1
+ value {
+ min 1
+ max 1
+ type "boolean"
+ }
+ }
+ node "min" description="minimum number of instances of this node in its parent's children." {
+ max 1
+ value {
+ min 1
+ max 1
+ type "number"
+ }
+ }
+ node "max" description="maximum number of instances of this node in its parent's children." {
+ max 1
+ value {
+ min 1
+ max 1
+ type "number"
+ }
+ }
+ node ref=r#"[id="value-tag-node"]"#
+ node "prop" id="prop-node" description="A node property key/value pair." {
+ value description="The property key." {
+ type "string"
+ }
+ prop "id" description="A globally-unique ID of this property." {
+ type "string"
+ }
+ prop "ref" description="A globally unique reference to another property node." {
+ type "string"
+ format "kdl-query"
+ }
+ prop "description" description="A description of this property's purpose." {
+ type "string"
+ }
+ children description="Property-specific validations." {
+ node "required" description="Whether this property is required if its parent is present." {
+ max 1
+ value {
+ min 1
+ max 1
+ type "boolean"
+ }
+ }
+ }
+ children id="validations" description="General value validations." {
+ node "tag" id="value-tag-node" description="The tags associated with this value" {
+ max 1
+ children ref=r#"[id="validations"]"#
+ }
+ node "type" description="The type for this prop's value." {
+ max 1
+ value {
+ min 1
+ type "string"
+ }
+ }
+ node "enum" description="An enumeration of possible values" {
+ max 1
+ value description="Enumeration choices" {
+ min 1
+ }
+ }
+ node "pattern" description="PCRE (Regex) pattern or patterns to test prop values against." {
+ value {
+ min 1
+ type "string"
+ }
+ }
+ node "min-length" description="Minimum length of prop value, if it's a string." {
+ max 1
+ value {
+ min 1
+ type "number"
+ }
+ }
+ node "max-length" description="Maximum length of prop value, if it's a string." {
+ max 1
+ value {
+ min 1
+ type "number"
+ }
+ }
+ node "format" description="Intended data format." {
+ max 1
+ value {
+ min 1
+ type "string"
+ // https://json-schema.org/understanding-json-schema/reference/string.html#format
+ enum "date-time" "date" "time" "duration" "decimal" "currency" "country-2" "country-3" "country-subdivision" "email" "idn-email" "hostname" "idn-hostname" "ipv4" "ipv6" "url" "url-reference" "irl" "irl-reference" "url-template" "regex" "uuid" "kdl-query" "i8" "i16" "i32" "i64" "u8" "u16" "u32" "u64" "isize" "usize" "f32" "f64" "decimal64" "decimal128"
+ }
+ }
+ node "%" description="Only used for numeric values. Constrains them to be multiples of the given number(s)" {
+ max 1
+ value {
+ min 1
+ type "number"
+ }
+ }
+ node ">" description="Only used for numeric values. Constrains them to be greater than the given number(s)" {
+ max 1
+ value {
+ min 1
+ max 1
+ type "number"
+ }
+ }
+ node ">=" description="Only used for numeric values. Constrains them to be greater than or equal to the given number(s)" {
+ max 1
+ value {
+ min 1
+ max 1
+ type "number"
+ }
+ }
+ node "<" description="Only used for numeric values. Constrains them to be less than the given number(s)" {
+ max 1
+ value {
+ min 1
+ max 1
+ type "number"
+ }
+ }
+ node "<=" description="Only used for numeric values. Constrains them to be less than or equal to the given number(s)" {
+ max 1
+ value {
+ min 1
+ max 1
+ type "number"
+ }
+ }
+ }
+ }
+ node "value" id="value-node" description="one or more direct node values" {
+ prop "id" description="A globally-unique ID of this value." {
+ type "string"
+ }
+ prop "ref" description="A globally unique reference to another value node." {
+ type "string"
+ format "kdl-query"
+ }
+ prop "description" description="A description of this property's purpose." {
+ type "string"
+ }
+ children ref=r#"[id="validations"]"#
+ children description="Node value-specific validations" {
+ node "min" description="minimum number of values for this node." {
+ max 1
+ value {
+ min 1
+ max 1
+ type "number"
+ }
+ }
+ node "max" description="maximum number of values for this node." {
+ max 1
+ value {
+ min 1
+ max 1
+ type "number"
+ }
+ }
+ }
+ }
+ node "children" id="children-node" {
+ prop "id" description="A globally-unique ID of this children node." {
+ type "string"
+ }
+ prop "ref" description="A globally unique reference to another children node." {
+ type "string"
+ format "kdl-query"
+ }
+ prop "description" description="A description of this these children's purpose." {
+ type "string"
+ }
+ children ref=r#"[id="node-children"]"#
+ }
+ }
+ }
+ node "definitions" description="Definitions to reference in parts of the top-level nodes" {
+ children {
+ node ref=r#"[id="node-node"]"#
+ node ref=r#"[id="value-node"]"#
+ node ref=r#"[id="prop-node"]"#
+ node ref=r#"[id="children-node"]"#
+ node ref=r#"[id="tag-node"]"#
+ }
+ }
+ }
+ }
+}
diff --git a/tests/schema-via-knuffel.rs b/tests/schema-via-knuffel.rs
new file mode 100644
index 0000000..b0d1a1c
--- /dev/null
+++ b/tests/schema-via-knuffel.rs
@@ -0,0 +1,63 @@
+#![cfg(feature = "knuffel")]
+
+use std::fmt::Debug;
+
+use kdl_schema::*;
+
+trait DeepAssertEq
+where
+ Self: PartialEq + Debug,
+{
+ fn deep_assert_eq(left: &Self, right: &Self) {
+ assert_eq!(left, right);
+ }
+}
+
+impl DeepAssertEq for Schema {
+ fn deep_assert_eq(left: &Self, right: &Self) {
+ DeepAssertEq::deep_assert_eq(&left.document, &right.document)
+ }
+}
+
+impl DeepAssertEq for Document {
+ fn deep_assert_eq(left: &Self, right: &Self) {
+ DeepAssertEq::deep_assert_eq(&left.info, &right.info);
+ DeepAssertEq::deep_assert_eq(&left.nodes, &right.nodes);
+ }
+}
+
+impl<T: DeepAssertEq> DeepAssertEq for Vec<T> {
+ fn deep_assert_eq(left: &Self, right: &Self) {
+ for (left, right) in left.iter().zip(right.iter()) {
+ DeepAssertEq::deep_assert_eq(left, right);
+ }
+ assert_eq!(left, right);
+ }
+}
+
+impl DeepAssertEq for Info {}
+
+impl DeepAssertEq for Node {
+ fn deep_assert_eq(left: &Self, right: &Self) {
+ DeepAssertEq::deep_assert_eq(&left.props, &right.props);
+ DeepAssertEq::deep_assert_eq(&left.values, &right.values);
+ DeepAssertEq::deep_assert_eq(&left.children, &right.children);
+ assert_eq!(left, right);
+ }
+}
+
+impl DeepAssertEq for node::Prop {}
+impl DeepAssertEq for node::Value {}
+impl DeepAssertEq for node::Children {
+ fn deep_assert_eq(left: &Self, right: &Self) {
+ DeepAssertEq::deep_assert_eq(&left.nodes, &right.nodes);
+ assert_eq!(left, right);
+ }
+}
+
+#[test]
+fn schema_loads() -> miette::Result<()> {
+ let schema: Schema = knuffel::parse("kdl-schema.kdl", include_str!("kdl-schema.kdl"))?;
+ DeepAssertEq::deep_assert_eq(&schema, &*SCHEMA_SCHEMA);
+ Ok(())
+}