kdl_schema/
lib.rs

1#![doc = include_str!("../README.md")]
2#![warn(
3    elided_lifetimes_in_paths,
4    explicit_outlives_requirements,
5    missing_debug_implementations,
6    missing_docs,
7    noop_method_call,
8    single_use_lifetimes,
9    trivial_casts,
10    trivial_numeric_casts,
11    unreachable_pub,
12    unsafe_code,
13    unused_crate_dependencies,
14    unused_qualifications
15)]
16#![warn(clippy::pedantic, clippy::cargo)]
17
18#[cfg(feature = "parse-knuffel")]
19use knuffel::{Decode, DecodeScalar};
20
21mod schema_schema;
22pub use schema_schema::SCHEMA_SCHEMA;
23
24pub(crate) trait BuildFromRef {
25    fn ref_to(query: impl Into<String>) -> Self;
26}
27
28fn get_id_from_ref(r#ref: &str) -> Option<&str> {
29    r#ref
30        .strip_prefix(r#"[id=""#)
31        .and_then(|r#ref| r#ref.strip_suffix(r#""]"#))
32}
33
34/// the schema itself
35#[derive(Debug, PartialEq, Eq, Default)]
36#[cfg_attr(feature = "parse-knuffel", derive(Decode))]
37pub struct Schema {
38    /// the document this schema defines
39    ///
40    /// this is redundant but it matches the schema document structure
41    #[cfg_attr(feature = "parse-knuffel", knuffel(child))]
42    pub document: Document,
43}
44
45impl Schema {
46    /// find the node matching the given ref
47    ///
48    /// # Panics
49    ///
50    /// Panics if ref is not of the form `[id="foo"]`.
51    #[must_use]
52    pub fn resolve_node_ref(&self, r#ref: &str) -> Option<&Node> {
53        let id = get_id_from_ref(r#ref).expect("invalid ref");
54        self.document
55            .nodes
56            .iter()
57            .find_map(|node| node.find_node_by_id(id))
58    }
59
60    /// find the prop matching the given ref
61    ///
62    /// # Panics
63    ///
64    /// Panics if ref is not of the form `[id="foo"]`.
65    #[must_use]
66    pub fn resolve_prop_ref(&self, r#ref: &str) -> Option<&Prop> {
67        let id = get_id_from_ref(r#ref).expect("invalid ref");
68        self.document
69            .nodes
70            .iter()
71            .find_map(|node| node.find_prop_by_id(id))
72    }
73
74    /// find the value matching the given ref
75    ///
76    /// # Panics
77    ///
78    /// Panics if ref is not of the form `[id="foo"]`.
79    #[must_use]
80    pub fn resolve_value_ref(&self, r#ref: &str) -> Option<&Value> {
81        let id = get_id_from_ref(r#ref).expect("invalid ref");
82        self.document
83            .nodes
84            .iter()
85            .find_map(|node| node.find_value_by_id(id))
86    }
87
88    /// find the children matching the given ref
89    ///
90    /// # Panics
91    ///
92    /// Panics if ref is not of the form `[id="foo"]`.
93    #[must_use]
94    pub fn resolve_children_ref(&self, r#ref: &str) -> Option<&Children> {
95        let id = get_id_from_ref(r#ref).expect("invalid ref");
96        self.document
97            .nodes
98            .iter()
99            .find_map(|node| node.find_children_by_id(id))
100    }
101}
102
103#[cfg(feature = "parse-knuffel")]
104impl Schema {
105    /// parse a KDL schema definition
106    ///
107    /// # Errors
108    ///
109    /// returns an error if knuffel can't parse the document as a Schema
110    pub fn parse(
111        schema_kdl: &str,
112    ) -> Result<Self, knuffel::Error<impl knuffel::traits::ErrorSpan>> {
113        knuffel::parse("<Schema::parse argument>", schema_kdl)
114    }
115}
116
117/// the schema document
118#[derive(Debug, PartialEq, Eq, Default)]
119#[cfg_attr(feature = "parse-knuffel", derive(Decode))]
120pub struct Document {
121    /// schema metadata
122    #[cfg_attr(feature = "parse-knuffel", knuffel(child))]
123    pub info: Info,
124    /// top-level node definitions
125    #[cfg_attr(feature = "parse-knuffel", knuffel(children(name = "node")))]
126    pub nodes: Vec<Node>,
127}
128
129/// schema metadata
130#[derive(Debug, PartialEq, Eq, Default)]
131#[cfg_attr(feature = "parse-knuffel", derive(Decode))]
132pub struct Info {
133    /// schema titles
134    #[cfg_attr(feature = "parse-knuffel", knuffel(children(name = "title")))]
135    pub title: Vec<TextValue>,
136    /// schema descriptions
137    #[cfg_attr(feature = "parse-knuffel", knuffel(children(name = "description")))]
138    pub description: Vec<TextValue>,
139    /// schema authors
140    #[cfg_attr(feature = "parse-knuffel", knuffel(children(name = "author")))]
141    pub authors: Vec<Person>,
142    /// schema contributors
143    #[cfg_attr(feature = "parse-knuffel", knuffel(children(name = "contributor")))]
144    pub contributors: Vec<Person>,
145    /// schema links
146    #[cfg_attr(feature = "parse-knuffel", knuffel(children(name = "link")))]
147    pub links: Vec<Link>,
148    /// schema licenses
149    #[cfg_attr(feature = "parse-knuffel", knuffel(children(name = "license")))]
150    pub licenses: Vec<License>,
151    /// schema publication date
152    #[cfg_attr(feature = "parse-knuffel", knuffel(child))]
153    pub published: Option<Date>,
154    /// schema modification date
155    #[cfg_attr(feature = "parse-knuffel", knuffel(child))]
156    pub modified: Option<Date>,
157}
158
159/// a text value with an optional language tag
160#[derive(Debug, PartialEq, Eq)]
161#[cfg_attr(feature = "parse-knuffel", derive(Decode))]
162pub struct TextValue {
163    /// text itself
164    #[cfg_attr(feature = "parse-knuffel", knuffel(argument))]
165    pub text: String,
166    /// BCP 47 language tag
167    #[cfg_attr(feature = "parse-knuffel", knuffel(property))]
168    pub lang: Option<String>,
169}
170
171/// information about a schema author/contributor
172#[derive(Debug, PartialEq, Eq)]
173#[cfg_attr(feature = "parse-knuffel", derive(Decode))]
174pub struct Person {
175    /// name
176    #[cfg_attr(feature = "parse-knuffel", knuffel(argument))]
177    pub name: String,
178    /// [ORCID](https://orcid.org)
179    #[cfg_attr(feature = "parse-knuffel", knuffel(property))]
180    pub orcid: Option<String>,
181    /// relevant links
182    #[cfg_attr(feature = "parse-knuffel", knuffel(children(name = "link")))]
183    pub links: Vec<Link>,
184}
185
186/// link related to specification metadata, with optional relationship and language tag
187#[derive(Debug, PartialEq, Eq)]
188#[cfg_attr(feature = "parse-knuffel", derive(Decode))]
189pub struct Link {
190    /// URI/IRI of link target
191    #[cfg_attr(feature = "parse-knuffel", knuffel(argument))]
192    pub iri: String,
193    /// relationship of link to schema (`self`, `documentation`)
194    #[cfg_attr(feature = "parse-knuffel", knuffel(property))]
195    pub rel: Option<String>,
196    /// BCP 47 language tag
197    #[cfg_attr(feature = "parse-knuffel", knuffel(property))]
198    pub lang: Option<String>,
199}
200
201/// schema license information
202#[derive(Debug, PartialEq, Eq)]
203#[cfg_attr(feature = "parse-knuffel", derive(Decode))]
204pub struct License {
205    /// license name
206    #[cfg_attr(feature = "parse-knuffel", knuffel(argument))]
207    pub name: String,
208    /// license [SPDX identifier](https://spdx.org/licenses/)
209    #[cfg_attr(feature = "parse-knuffel", knuffel(property))]
210    pub spdx: Option<String>,
211    /// links for license information
212    #[cfg_attr(feature = "parse-knuffel", knuffel(children(name = "link")))]
213    pub link: Vec<Link>,
214}
215
216/// date with optional time
217#[derive(Debug, PartialEq, Eq)]
218#[cfg_attr(feature = "parse-knuffel", derive(Decode))]
219pub struct Date {
220    /// date
221    #[cfg_attr(feature = "parse-knuffel", knuffel(argument))]
222    pub date: String,
223    /// time
224    #[cfg_attr(feature = "parse-knuffel", knuffel(property))]
225    pub time: Option<String>,
226}
227
228/// schema for a node
229#[derive(Debug, PartialEq, Eq, Default)]
230#[cfg_attr(feature = "parse-knuffel", derive(Decode))]
231pub struct Node {
232    /// name of the node (applies to all nodes at this level if `None`)
233    #[cfg_attr(feature = "parse-knuffel", knuffel(argument))]
234    pub name: Option<String>,
235    /// id of the node (can be used for refs)
236    #[cfg_attr(feature = "parse-knuffel", knuffel(property))]
237    pub id: Option<String>,
238    /// human-readable description of the node's purpose
239    #[cfg_attr(feature = "parse-knuffel", knuffel(property))]
240    pub description: Option<String>,
241    /// KDL query from which to load node information instead of specifying it inline (allows for recursion)
242    #[cfg_attr(feature = "parse-knuffel", knuffel(property))]
243    pub ref_: Option<String>,
244    /// minimum number of occurrences of this node
245    #[cfg_attr(feature = "parse-knuffel", knuffel(child, unwrap(argument)))]
246    pub min: Option<usize>,
247    /// maximum number of occurrences of this node
248    #[cfg_attr(feature = "parse-knuffel", knuffel(child, unwrap(argument)))]
249    pub max: Option<usize>,
250    /// properties allowed on this node
251    #[cfg_attr(feature = "parse-knuffel", knuffel(children(name = "prop")))]
252    pub props: Vec<Prop>,
253    /// values allowed on this node
254    #[cfg_attr(feature = "parse-knuffel", knuffel(children(name = "value")))]
255    pub values: Vec<Value>,
256    /// children allowed on this node
257    #[cfg_attr(feature = "parse-knuffel", knuffel(children(name = "children")))]
258    pub children: Vec<Children>,
259}
260
261impl Node {
262    fn find_node_by_id(&self, id: &str) -> Option<&Node> {
263        if self.id.as_deref() == Some(id) {
264            Some(self)
265        } else {
266            self.children
267                .iter()
268                .find_map(|children| children.find_node_by_id(id))
269        }
270    }
271
272    fn find_prop_by_id(&self, id: &str) -> Option<&Prop> {
273        self.props
274            .iter()
275            .find_map(|prop| prop.find_prop_by_id(id))
276            .or_else(|| {
277                self.children
278                    .iter()
279                    .find_map(|children| children.find_prop_by_id(id))
280            })
281    }
282
283    fn find_value_by_id(&self, id: &str) -> Option<&Value> {
284        self.values
285            .iter()
286            .find_map(|value| value.find_value_by_id(id))
287            .or_else(|| {
288                self.children
289                    .iter()
290                    .find_map(|children| children.find_value_by_id(id))
291            })
292    }
293
294    fn find_children_by_id(&self, id: &str) -> Option<&Children> {
295        self.children
296            .iter()
297            .find_map(|children| children.find_children_by_id(id))
298    }
299}
300
301impl BuildFromRef for Node {
302    fn ref_to(query: impl Into<String>) -> Self {
303        Self {
304            ref_: Some(query.into()),
305            ..Self::default()
306        }
307    }
308}
309
310/// schema for a property
311#[derive(Debug, PartialEq, Eq, Default)]
312#[cfg_attr(feature = "parse-knuffel", derive(Decode))]
313pub struct Prop {
314    /// property key (applies to all properties in this node if `None`)
315    #[cfg_attr(feature = "parse-knuffel", knuffel(argument))]
316    pub key: Option<String>,
317    /// id of the property (can be used for refs)
318    #[cfg_attr(feature = "parse-knuffel", knuffel(property))]
319    pub id: Option<String>,
320    /// human-readable description of the property
321    #[cfg_attr(feature = "parse-knuffel", knuffel(property))]
322    pub description: Option<String>,
323    /// KDL query from which to load property information instead of specifying it inline (allows for recursion)
324    #[cfg_attr(feature = "parse-knuffel", knuffel(property))]
325    pub ref_: Option<String>,
326    /// whether or not this property is required
327    #[cfg_attr(feature = "parse-knuffel", knuffel(child))]
328    pub required: bool,
329    /// validations to apply to the property value
330    #[cfg_attr(feature = "parse-knuffel", knuffel(children))]
331    pub validations: Vec<Validation>,
332}
333
334impl Prop {
335    fn find_prop_by_id(&self, id: &str) -> Option<&Prop> {
336        if self.id.as_deref() == Some(id) {
337            Some(self)
338        } else {
339            None
340        }
341    }
342}
343
344impl BuildFromRef for Prop {
345    fn ref_to(query: impl Into<String>) -> Self {
346        Self {
347            ref_: Some(query.into()),
348            ..Self::default()
349        }
350    }
351}
352
353/// schema for a value
354#[derive(Debug, PartialEq, Eq, Default)]
355#[cfg_attr(feature = "parse-knuffel", derive(Decode))]
356pub struct Value {
357    /// id of the value (can be used for refs)
358    #[cfg_attr(feature = "parse-knuffel", knuffel(property))]
359    pub id: Option<String>,
360    /// human readable description of the value
361    #[cfg_attr(feature = "parse-knuffel", knuffel(property))]
362    pub description: Option<String>,
363    /// KDL query from which to load value information instead of specifying it inline (allows for recursion)
364    #[cfg_attr(feature = "parse-knuffel", knuffel(property))]
365    pub ref_: Option<String>,
366    /// minimum number of occurrences of this value
367    #[cfg_attr(feature = "parse-knuffel", knuffel(child, unwrap(argument)))]
368    pub min: Option<usize>,
369    /// maximum number of occurrences of this value
370    #[cfg_attr(feature = "parse-knuffel", knuffel(child, unwrap(argument)))]
371    pub max: Option<usize>,
372    /// validations to apply to this value
373    #[cfg_attr(feature = "parse-knuffel", knuffel(children))]
374    pub validations: Vec<Validation>,
375}
376
377impl Value {
378    fn find_value_by_id(&self, id: &str) -> Option<&Value> {
379        if self.id.as_deref() == Some(id) {
380            Some(self)
381        } else {
382            None
383        }
384    }
385}
386
387impl BuildFromRef for Value {
388    fn ref_to(query: impl Into<String>) -> Self {
389        Self {
390            ref_: Some(query.into()),
391            ..Self::default()
392        }
393    }
394}
395
396/// schema for a node's children
397#[derive(Debug, PartialEq, Eq, Default)]
398#[cfg_attr(feature = "parse-knuffel", derive(Decode))]
399pub struct Children {
400    /// id for these children (can be used for refs)
401    #[cfg_attr(feature = "parse-knuffel", knuffel(property))]
402    pub id: Option<String>,
403    /// human readable description of these children
404    #[cfg_attr(feature = "parse-knuffel", knuffel(property))]
405    pub description: Option<String>,
406    /// KDL query from which to load children information instead of specifying it inline (allows for recursion)
407    #[cfg_attr(feature = "parse-knuffel", knuffel(property))]
408    pub ref_: Option<String>,
409    /// nodes which can appear as children
410    #[cfg_attr(feature = "parse-knuffel", knuffel(children(name = "node")))]
411    pub nodes: Vec<Node>,
412}
413
414impl Children {
415    fn find_node_by_id(&self, id: &str) -> Option<&Node> {
416        self.nodes.iter().find_map(|node| node.find_node_by_id(id))
417    }
418
419    fn find_prop_by_id(&self, id: &str) -> Option<&Prop> {
420        self.nodes.iter().find_map(|node| node.find_prop_by_id(id))
421    }
422
423    fn find_value_by_id(&self, id: &str) -> Option<&Value> {
424        self.nodes.iter().find_map(|node| node.find_value_by_id(id))
425    }
426
427    fn find_children_by_id(&self, id: &str) -> Option<&Children> {
428        if self.id.as_deref() == Some(id) {
429            Some(self)
430        } else {
431            self.nodes
432                .iter()
433                .find_map(|node| node.find_children_by_id(id))
434        }
435    }
436}
437
438impl BuildFromRef for Children {
439    fn ref_to(query: impl Into<String>) -> Self {
440        Self {
441            ref_: Some(query.into()),
442            ..Self::default()
443        }
444    }
445}
446
447/// a validation to apply to some value or property value
448#[derive(Debug, PartialEq, Eq)]
449#[cfg_attr(feature = "parse-knuffel", derive(Decode))]
450pub enum Validation {
451    /// ensure the value is of the given type
452    Type(#[cfg_attr(feature = "parse-knuffel", knuffel(argument))] String),
453    /// ensure the value is one of the given options
454    Enum(#[cfg_attr(feature = "parse-knuffel", knuffel(arguments))] Vec<String>),
455    /// ensure the value matches the given regular expression
456    Pattern(#[cfg_attr(feature = "parse-knuffel", knuffel(argument))] String),
457    /// ensure the value is of the given format
458    Format(#[cfg_attr(feature = "parse-knuffel", knuffel(arguments))] Vec<Format>),
459}
460
461/// a format to ensure a value has
462#[derive(Clone, Debug, PartialEq, Eq)]
463#[cfg_attr(feature = "parse-knuffel", derive(DecodeScalar))]
464pub enum Format {
465    /// iso 8601 datetime string
466    DateTime,
467    /// iso 8601 date string
468    Date,
469    /// iso 8601 time string
470    Time,
471    /// iso 8601 duration string
472    Duration,
473    /// ieee 754-2008 decimal string
474    Decimal,
475    /// iso 4217 currency code string
476    Currency,
477    /// iso 3166-1 alpha-2 country code string
478    Country2,
479    /// iso 3166-1 alpha-3 country code string
480    Country3,
481    /// iso 3166-2 country subdivision code string
482    CountrySubdivision,
483    /// rfc 5302 email address string
484    Email,
485    /// rfc 6531 internationalized email address string
486    IdnEmail,
487    /// rfc 1132 internet hostname string
488    Hostname,
489    /// rfc 5890 internationalized internet hostname string
490    IdnHostname,
491    /// rfc 2673 ipv4 address string
492    Ipv4,
493    /// rfc 2373 ipv6 address string
494    Ipv6,
495    /// rfc 3986 uri string
496    Url,
497    /// rfc 3986 uri reference string
498    UrlReference,
499    /// rfc 3987 iri string
500    Irl,
501    /// rfc 3987 iri reference string
502    IrlReference,
503    /// rfc 6750 uri template string
504    UrlTemplate,
505    /// rfc 4122 uuid string
506    Uuid,
507    /// regular expression string
508    Regex,
509    /// base64 encoded string
510    Base64,
511    /// KDL query string
512    KdlQuery,
513}