From 26a20cf4f3a71ccdd7c6abb06cffd4c12d08395e Mon Sep 17 00:00:00 2001 From: Melody Horn Date: Sat, 26 Mar 2022 19:57:57 -0600 Subject: parse schemas with knuffel --- Cargo.toml | 5 + src/lib.rs | 264 ++++++++++++++- src/schema_schema.rs | 794 ++++++++++++++++++++++++++++++++++++++++++++ tests/kdl-schema.kdl | 373 +++++++++++++++++++++ tests/schema-via-knuffel.rs | 63 ++++ 5 files changed, 1493 insertions(+), 6 deletions(-) create mode 100644 src/schema_schema.rs create mode 100644 tests/kdl-schema.kdl create mode 100644 tests/schema-via-knuffel.rs 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) -> 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, +} + +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, + #[cfg_attr(feature = "knuffel", knuffel(children(name = "description")))] + pub description: Vec, + #[cfg_attr(feature = "knuffel", knuffel(children(name = "author")))] + pub authors: Vec, + #[cfg_attr(feature = "knuffel", knuffel(children(name = "contributor")))] + pub contributors: Vec, + #[cfg_attr(feature = "knuffel", knuffel(children(name = "link")))] + pub links: Vec, + #[cfg_attr(feature = "knuffel", knuffel(children(name = "license")))] + pub licenses: Vec, + #[cfg_attr(feature = "knuffel", knuffel(child))] + pub published: Option, + #[cfg_attr(feature = "knuffel", knuffel(child))] + pub modified: Option, + } + + #[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, + } + + #[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, + #[cfg_attr(feature = "knuffel", knuffel(children(name = "link")))] + pub links: Vec, + } + + #[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, + #[cfg_attr(feature = "knuffel", knuffel(property))] + pub lang: Option, + } + + #[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, + #[cfg_attr(feature = "knuffel", knuffel(children(name = "link")))] + pub link: Vec, + } + + #[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, + } +} + +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, + #[cfg_attr(feature = "knuffel", knuffel(property))] + pub id: Option, + #[cfg_attr(feature = "knuffel", knuffel(property))] + pub description: Option, + #[cfg_attr(feature = "knuffel", knuffel(property))] + pub ref_: Option, + #[cfg_attr(feature = "knuffel", knuffel(child, unwrap(argument)))] + pub min: Option, + #[cfg_attr(feature = "knuffel", knuffel(child, unwrap(argument)))] + pub max: Option, + #[cfg_attr(feature = "knuffel", knuffel(children(name = "prop")))] + pub props: Vec, + #[cfg_attr(feature = "knuffel", knuffel(children(name = "value")))] + pub values: Vec, + #[cfg_attr(feature = "knuffel", knuffel(children(name = "children")))] + pub children: Vec, + } + + impl BuildFromRef for Node { + fn ref_to(query: impl Into) -> 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, + #[cfg_attr(feature = "knuffel", knuffel(property))] + pub id: Option, + #[cfg_attr(feature = "knuffel", knuffel(property))] + pub description: Option, + #[cfg_attr(feature = "knuffel", knuffel(property))] + pub ref_: Option, + #[cfg_attr(feature = "knuffel", knuffel(child))] + pub required: bool, + #[cfg_attr(feature = "knuffel", knuffel(children))] + pub validations: Vec, + } + + impl BuildFromRef for Prop { + fn ref_to(query: impl Into) -> 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, + #[cfg_attr(feature = "knuffel", knuffel(property))] + pub description: Option, + #[cfg_attr(feature = "knuffel", knuffel(property))] + pub ref_: Option, + #[cfg_attr(feature = "knuffel", knuffel(child, unwrap(argument)))] + pub min: Option, + #[cfg_attr(feature = "knuffel", knuffel(child, unwrap(argument)))] + pub max: Option, + #[cfg_attr(feature = "knuffel", knuffel(children))] + pub validations: Vec, + } + + impl BuildFromRef for Value { + fn ref_to(query: impl Into) -> 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, + #[cfg_attr(feature = "knuffel", knuffel(property))] + pub description: Option, + #[cfg_attr(feature = "knuffel", knuffel(property))] + pub ref_: Option, + #[cfg_attr(feature = "knuffel", knuffel(children(name = "node")))] + pub nodes: Vec, + } + + impl BuildFromRef for Children { + fn ref_to(query: impl Into) -> 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), + Pattern(#[cfg_attr(feature = "knuffel", knuffel(argument))] String), + Format(#[cfg_attr(feature = "knuffel", knuffel(arguments))] Vec), + } + + #[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>(text: &str) -> T { + T::from(text.to_string()) +} + +fn ref_to_id(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 DeepAssertEq for Vec { + 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(()) +} -- cgit v1.2.3