diff options
-rw-r--r-- | README.md | 3 | ||||
-rw-r--r-- | src/lib.rs | 117 | ||||
-rw-r--r-- | src/schema_schema.rs | 1 |
3 files changed, 119 insertions, 2 deletions
@@ -82,8 +82,9 @@ assert_eq!(kdl_schema::SCHEMA_SCHEMA.document.info.title[0].text, "KDL Schema"); - can generate KDL from schema object in Rust - can choose kdl or knuffel as parser - make sure `required false` in a `prop` works properly -- validate the schema at parse time (ensure that e.g. refs are resolvable) +- validate the schema at parse time (ensure that e.g. refs are resolvable, IDs are unique) - make sure enums work with int values +- allow rich types for language tags, modification dates, etc ## license @@ -3,6 +3,7 @@ elided_lifetimes_in_paths, explicit_outlives_requirements, missing_debug_implementations, + missing_docs, noop_method_call, single_use_lifetimes, trivial_casts, @@ -20,7 +21,7 @@ use knuffel::{Decode, DecodeScalar}; mod schema_schema; pub use schema_schema::SCHEMA_SCHEMA; -pub trait BuildFromRef { +pub(crate) trait BuildFromRef { fn ref_to(query: impl Into<String>) -> Self; } @@ -30,14 +31,22 @@ fn get_id_from_ref(r#ref: &str) -> Option<&str> { .and_then(|r#ref| r#ref.strip_suffix(r#""]"#)) } +/// the schema itself #[derive(Debug, PartialEq, Eq, Default)] #[cfg_attr(feature = "parse-knuffel", derive(Decode))] pub struct Schema { + /// the document this schema defines + /// + /// this is redundant but it matches the schema document structure #[cfg_attr(feature = "parse-knuffel", knuffel(child))] pub document: Document, } impl Schema { + /// find the node matching the given ref + /// + /// # Panics + /// /// Panics if ref is not of the form `[id="foo"]`. #[must_use] pub fn resolve_node_ref(&self, r#ref: &str) -> Option<&Node> { @@ -48,6 +57,10 @@ impl Schema { .find_map(|node| node.find_node_by_id(id)) } + /// find the prop matching the given ref + /// + /// # Panics + /// /// Panics if ref is not of the form `[id="foo"]`. #[must_use] pub fn resolve_prop_ref(&self, r#ref: &str) -> Option<&Prop> { @@ -58,6 +71,10 @@ impl Schema { .find_map(|node| node.find_prop_by_id(id)) } + /// find the value matching the given ref + /// + /// # Panics + /// /// Panics if ref is not of the form `[id="foo"]`. #[must_use] pub fn resolve_value_ref(&self, r#ref: &str) -> Option<&Value> { @@ -68,6 +85,10 @@ impl Schema { .find_map(|node| node.find_value_by_id(id)) } + /// find the children matching the given ref + /// + /// # Panics + /// /// Panics if ref is not of the form `[id="foo"]`. #[must_use] pub fn resolve_children_ref(&self, r#ref: &str) -> Option<&Children> { @@ -81,6 +102,11 @@ impl Schema { #[cfg(feature = "parse-knuffel")] impl Schema { + /// parse a KDL schema definition + /// + /// # Errors + /// + /// returns an error if knuffel can't parse the document as a Schema pub fn parse( schema_kdl: &str, ) -> Result<Self, knuffel::Error<impl knuffel::traits::ErrorSpan>> { @@ -88,106 +114,146 @@ impl Schema { } } +/// the schema document #[derive(Debug, PartialEq, Eq, Default)] #[cfg_attr(feature = "parse-knuffel", derive(Decode))] pub struct Document { + /// schema metadata #[cfg_attr(feature = "parse-knuffel", knuffel(child))] pub info: Info, + /// top-level node definitions #[cfg_attr(feature = "parse-knuffel", knuffel(children(name = "node")))] pub nodes: Vec<Node>, } +/// schema metadata #[derive(Debug, PartialEq, Eq, Default)] #[cfg_attr(feature = "parse-knuffel", derive(Decode))] pub struct Info { + /// schema titles #[cfg_attr(feature = "parse-knuffel", knuffel(children(name = "title")))] pub title: Vec<TextValue>, + /// schema descriptions #[cfg_attr(feature = "parse-knuffel", knuffel(children(name = "description")))] pub description: Vec<TextValue>, + /// schema authors #[cfg_attr(feature = "parse-knuffel", knuffel(children(name = "author")))] pub authors: Vec<Person>, + /// schema contributors #[cfg_attr(feature = "parse-knuffel", knuffel(children(name = "contributor")))] pub contributors: Vec<Person>, + /// schema links #[cfg_attr(feature = "parse-knuffel", knuffel(children(name = "link")))] pub links: Vec<Link>, + /// schema licenses #[cfg_attr(feature = "parse-knuffel", knuffel(children(name = "license")))] pub licenses: Vec<License>, + /// schema publication date #[cfg_attr(feature = "parse-knuffel", knuffel(child))] pub published: Option<Date>, + /// schema modification date #[cfg_attr(feature = "parse-knuffel", knuffel(child))] pub modified: Option<Date>, } +/// a text value with an optional language tag #[derive(Debug, PartialEq, Eq)] #[cfg_attr(feature = "parse-knuffel", derive(Decode))] pub struct TextValue { + /// text itself #[cfg_attr(feature = "parse-knuffel", knuffel(argument))] pub text: String, + /// BCP 47 language tag #[cfg_attr(feature = "parse-knuffel", knuffel(property))] pub lang: Option<String>, } +/// information about a schema author/contributor #[derive(Debug, PartialEq, Eq)] #[cfg_attr(feature = "parse-knuffel", derive(Decode))] pub struct Person { + /// name #[cfg_attr(feature = "parse-knuffel", knuffel(argument))] pub name: String, + /// [ORCID](https://orcid.org) #[cfg_attr(feature = "parse-knuffel", knuffel(property))] pub orcid: Option<String>, + /// relevant links #[cfg_attr(feature = "parse-knuffel", knuffel(children(name = "link")))] pub links: Vec<Link>, } +/// link related to specification metadata, with optional relationship and language tag #[derive(Debug, PartialEq, Eq)] #[cfg_attr(feature = "parse-knuffel", derive(Decode))] pub struct Link { + /// URI/IRI of link target #[cfg_attr(feature = "parse-knuffel", knuffel(argument))] pub iri: String, + /// relationship of link to schema (`self`, `documentation`) #[cfg_attr(feature = "parse-knuffel", knuffel(property))] pub rel: Option<String>, + /// BCP 47 language tag #[cfg_attr(feature = "parse-knuffel", knuffel(property))] pub lang: Option<String>, } +/// schema license information #[derive(Debug, PartialEq, Eq)] #[cfg_attr(feature = "parse-knuffel", derive(Decode))] pub struct License { + /// license name #[cfg_attr(feature = "parse-knuffel", knuffel(argument))] pub name: String, + /// license [SPDX identifier](https://spdx.org/licenses/) #[cfg_attr(feature = "parse-knuffel", knuffel(property))] pub spdx: Option<String>, + /// links for license information #[cfg_attr(feature = "parse-knuffel", knuffel(children(name = "link")))] pub link: Vec<Link>, } +/// date with optional time #[derive(Debug, PartialEq, Eq)] #[cfg_attr(feature = "parse-knuffel", derive(Decode))] pub struct Date { + /// date #[cfg_attr(feature = "parse-knuffel", knuffel(argument))] pub date: String, + /// time #[cfg_attr(feature = "parse-knuffel", knuffel(property))] pub time: Option<String>, } +/// schema for a node #[derive(Debug, PartialEq, Eq, Default)] #[cfg_attr(feature = "parse-knuffel", derive(Decode))] pub struct Node { + /// name of the node (applies to all nodes at this level if `None`) #[cfg_attr(feature = "parse-knuffel", knuffel(argument))] pub name: Option<String>, + /// id of the node (can be used for refs) #[cfg_attr(feature = "parse-knuffel", knuffel(property))] pub id: Option<String>, + /// human-readable description of the node's purpose #[cfg_attr(feature = "parse-knuffel", knuffel(property))] pub description: Option<String>, + /// KDL query from which to load node information instead of specifying it inline (allows for recursion) #[cfg_attr(feature = "parse-knuffel", knuffel(property))] pub ref_: Option<String>, + /// minimum number of occurrences of this node #[cfg_attr(feature = "parse-knuffel", knuffel(child, unwrap(argument)))] pub min: Option<usize>, + /// maximum number of occurrences of this node #[cfg_attr(feature = "parse-knuffel", knuffel(child, unwrap(argument)))] pub max: Option<usize>, + /// properties allowed on this node #[cfg_attr(feature = "parse-knuffel", knuffel(children(name = "prop")))] pub props: Vec<Prop>, + /// values allowed on this node #[cfg_attr(feature = "parse-knuffel", knuffel(children(name = "value")))] pub values: Vec<Value>, + /// children allowed on this node #[cfg_attr(feature = "parse-knuffel", knuffel(children(name = "children")))] pub children: Vec<Children>, } @@ -241,19 +307,26 @@ impl BuildFromRef for Node { } } +/// schema for a property #[derive(Debug, PartialEq, Eq, Default)] #[cfg_attr(feature = "parse-knuffel", derive(Decode))] pub struct Prop { + /// property key (applies to all properties in this node if `None`) #[cfg_attr(feature = "parse-knuffel", knuffel(argument))] pub key: Option<String>, + /// id of the property (can be used for refs) #[cfg_attr(feature = "parse-knuffel", knuffel(property))] pub id: Option<String>, + /// human-readable description of the property #[cfg_attr(feature = "parse-knuffel", knuffel(property))] pub description: Option<String>, + /// KDL query from which to load property information instead of specifying it inline (allows for recursion) #[cfg_attr(feature = "parse-knuffel", knuffel(property))] pub ref_: Option<String>, + /// whether or not this property is required #[cfg_attr(feature = "parse-knuffel", knuffel(child))] pub required: bool, + /// validations to apply to the property value #[cfg_attr(feature = "parse-knuffel", knuffel(children))] pub validations: Vec<Validation>, } @@ -277,19 +350,26 @@ impl BuildFromRef for Prop { } } +/// schema for a value #[derive(Debug, PartialEq, Eq, Default)] #[cfg_attr(feature = "parse-knuffel", derive(Decode))] pub struct Value { + /// id of the value (can be used for refs) #[cfg_attr(feature = "parse-knuffel", knuffel(property))] pub id: Option<String>, + /// human readable description of the value #[cfg_attr(feature = "parse-knuffel", knuffel(property))] pub description: Option<String>, + /// KDL query from which to load value information instead of specifying it inline (allows for recursion) #[cfg_attr(feature = "parse-knuffel", knuffel(property))] pub ref_: Option<String>, + /// minimum number of occurrences of this value #[cfg_attr(feature = "parse-knuffel", knuffel(child, unwrap(argument)))] pub min: Option<usize>, + /// maximum number of occurrences of this value #[cfg_attr(feature = "parse-knuffel", knuffel(child, unwrap(argument)))] pub max: Option<usize>, + /// validations to apply to this value #[cfg_attr(feature = "parse-knuffel", knuffel(children))] pub validations: Vec<Validation>, } @@ -313,15 +393,20 @@ impl BuildFromRef for Value { } } +/// schema for a node's children #[derive(Debug, PartialEq, Eq, Default)] #[cfg_attr(feature = "parse-knuffel", derive(Decode))] pub struct Children { + /// id for these children (can be used for refs) #[cfg_attr(feature = "parse-knuffel", knuffel(property))] pub id: Option<String>, + /// human readable description of these children #[cfg_attr(feature = "parse-knuffel", knuffel(property))] pub description: Option<String>, + /// KDL query from which to load children information instead of specifying it inline (allows for recursion) #[cfg_attr(feature = "parse-knuffel", knuffel(property))] pub ref_: Option<String>, + /// nodes which can appear as children #[cfg_attr(feature = "parse-knuffel", knuffel(children(name = "node")))] pub nodes: Vec<Node>, } @@ -359,40 +444,70 @@ impl BuildFromRef for Children { } } +/// a validation to apply to some value or property value #[derive(Debug, PartialEq, Eq)] #[cfg_attr(feature = "parse-knuffel", derive(Decode))] pub enum Validation { + /// ensure the value is of the given type Type(#[cfg_attr(feature = "parse-knuffel", knuffel(argument))] String), + /// ensure the value is one of the given options Enum(#[cfg_attr(feature = "parse-knuffel", knuffel(arguments))] Vec<String>), + /// ensure the value matches the given regular expression Pattern(#[cfg_attr(feature = "parse-knuffel", knuffel(argument))] String), + /// ensure the value is of the given format Format(#[cfg_attr(feature = "parse-knuffel", knuffel(arguments))] Vec<Format>), } +/// a format to ensure a value has #[derive(Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "parse-knuffel", derive(DecodeScalar))] pub enum Format { + /// iso 8601 datetime string DateTime, + /// iso 8601 date string Date, + /// iso 8601 time string Time, + /// iso 8601 duration string Duration, + /// ieee 754-2008 decimal string Decimal, + /// iso 4217 currency code string Currency, + /// iso 3166-1 alpha-2 country code string Country2, + /// iso 3166-1 alpha-3 country code string Country3, + /// iso 3166-2 country subdivision code string CountrySubdivision, + /// rfc 5302 email address string Email, + /// rfc 6531 internationalized email address string IdnEmail, + /// rfc 1132 internet hostname string Hostname, + /// rfc 5890 internationalized internet hostname string IdnHostname, + /// rfc 2673 ipv4 address string Ipv4, + /// rfc 2373 ipv6 address string Ipv6, + /// rfc 3986 uri string Url, + /// rfc 3986 uri reference string UrlReference, + /// rfc 3987 iri string Irl, + /// rfc 3987 iri reference string IrlReference, + /// rfc 6750 uri template string UrlTemplate, + /// rfc 4122 uuid string Uuid, + /// regular expression string Regex, + /// base64 encoded string Base64, + /// KDL query string KdlQuery, } diff --git a/src/schema_schema.rs b/src/schema_schema.rs index 11ccb4e..c17eb89 100644 --- a/src/schema_schema.rs +++ b/src/schema_schema.rs @@ -5,6 +5,7 @@ use lazy_static::lazy_static; use super::*; lazy_static! { + /// the schema schema defined by [the canonical `kdl-schema.kdl`](https://github.com/kdl-org/kdl/blob/cc1da35435abbe2f7d0138310f89b18c55a292e7/examples/kdl-schema.kdl) pub static ref SCHEMA_SCHEMA: Schema = make_schema_schema(); } |