toml_schema/
lib.rs

1//! A schema parser for TOML files
2//! 
3//! This crates aims to provide something similar to [JSON schemas](https://json-schema.org/understanding-json-schema/about) for TOML
4//! - Schemas are written in TOML
5//! - All TOML types are currently supported
6//! - References (and recursive schemas) are not yet supported
7//! 
8//! This crate is very much new and a lot of functionnalities are not fully tested
9//! 
10//! 
11//! ## Syntax
12//!  
13//! A schema is represented by a table with a `type` key, its value may be any of
14//! - `string` : any string that matches the specified regex
15//! - `int` : a 64 bit signed integer with optional bounds
16//! - `float` : a 64 bit float with optional bounds
17//! - `bool` : a boolean
18//! - `date` : a date
19//! - `array` : an array of values that all match a specific schema
20//! - `table` : a TOML table with specific keys
21//! - `alternative` : an OR operation on sub-patterns
22//! 
23//! If the parser expects a schema but finds not `type` key, it will assume the type is `table`, but if the type is none of
24//! the above, parsing will fail
25//! 
26//! For each type of schema there, are other keys that are either required of optional to give more details
27//! about the schema
28//! 
29//! A `default` key may also be provided when the schema is the value of a key in a `table` schema
30//! to make that key optional, `default` will be ignored in other positions
31//! 
32//! Any extra keys will be ignored (except in `table`
33//! 
34//! ### string
35//! - `regex` (optional, default = `/.*/`) : a regular expression that must be found in the string, if you want to match the whole string,
36//! use '^' and '$'
37//! 
38//! ### int
39//! - `min` (optional, default = [i64::MIN]) : the minmimum value allowed
40//! - `max` (optional, default = [i64::MAX]) : the maximum value allowed
41//! 
42//! ### float
43//! - `min` (optional, default = [f64::NEG_INFINITY]) : the minmimum value allowed
44//! - `max` (optional, default = [f64::INFINITY]) : the maximum value allowed
45//! - `nan_ok` (optional, default = `false`) : if this is true, [f64::NAN] is accepted
46//! 
47//! ### bool
48//! 
49//! ### date
50//! 
51//! ### array
52//! - `child` (required) : a schema that all elements of this array must match
53//! - `min` (optional, default = `0`) : the minimum number of elements
54//! - `max` (optional, default = [usize::MAX]) : the maximum number of elements
55//! 
56//! ### table
57//! - `extras` (optional, default = `[]`) : an array of tables with a `key` and `schema` key that defines regex-based key-value pairs
58//! - `extras[n].key` (required) : a regular expression that must be found in the key 
59//! - `extras[n].schema` (required) : a schema that must be matched by the value
60//! - `min` (optional, default = `0`) : the minimum number of extra keys
61//! - `max` (optional, default = `0`) : the maximum number of extra keys
62//! 
63//! All other keys must be schemas, they defined a table key (optional if `default` is provided in this schema) that must match
64//! the schema, a '$' is stripped from the beginning of the key if it exists to allow escaping schema keywords, if you want a key
65//! that starts with '$', start your key with "$$" etc...
66//! 
67//! All keys in the TOML table beeing matched are matched against entries before extra keys, this means that if a key matches an
68//! entry and an extra, it will not count towards the number of extra keys, this means that you may want to make extra key 
69//! regular expressions mutually excusive with the table entries
70//! 
71//! ### alternative
72//! - `options` (required) : an array of schemas, a TOML value matches if any of them match
73//! 
74//! ## Examples
75//! 
76//! - To match any table
77//! 
78//! ```toml
79//! type = "table"
80//! extras = [{key = ".*", schema = {type = "anything"}}]
81//! ```
82//! 
83//! - to match an array of strings
84//! ```toml
85//! type = "array"
86//! child = {type = "string"}
87//! ```
88//! - you may find a basic schema for Cargo.toml files on github at "test_files/test_schema.toml"
89//! 
90//! 
91//! ## Planned additions
92//! - `reference` : a link to another schema (or the schema itself)
93//! - `anything` : a schema that matches anything
94//! - `exact` : a schema that matches only one value
95
96
97use std::collections::{HashMap, HashSet};
98use toml::Value;
99use regex::Regex;
100
101mod constructor;
102mod parse_toml;
103mod schema_type;
104
105/// An enum that represents the a kind of schema, used mostly in errors
106pub use schema_type::SchemaType;
107
108
109/// A component of a [TomlSchema], only useful to construct a schema by hand
110#[derive(Debug, Clone)]
111pub struct TableEntry {
112    pub key: Regex,
113    pub value: TomlSchema
114}
115
116
117/// The main type of the crate, it can be constructed from a [toml::Table] object or by hand, the main constructor
118/// for this type is [TomlSchema::try_from]
119#[derive(Debug, Clone)]
120pub enum TomlSchema {
121    Alternative(Vec<TomlSchema>),
122    String{regex: Regex},
123    Integer{min: i64, max: i64},
124    Date,
125    Bool,
126    Float{min: f64, max: f64, nan_ok: bool},
127    Table{extras: Vec<TableEntry>, min: usize, max: usize, entries: HashMap<String, (TomlSchema, Option<Value>)>},
128    Array{cond: Box<TomlSchema>, min: usize, max: usize},
129    Anything,
130    Exact(Value)
131}
132
133
134impl TryFrom<toml::Table> for TomlSchema {
135    type Error = String;
136
137    /// The main constructor for a TomlSchema, calls [TomlSchema::from_table] and discards the default value
138    /// 
139    /// note: default values are not checked against the schema
140    fn try_from(table: toml::Table) -> Result<Self,String>
141    {
142        let (schema, dv) = TomlSchema::from_table(&table)?;
143
144        if dv.is_some() {log::warn!("Tried to construct a TOML Schema with a default value in the root")}
145        
146        Ok(schema)
147    }
148}
149
150
151/// The error type returned by [TomlSchema::check], it cannot outlive the [TomlSchema] or the [toml::Table] it comes from
152#[derive(Clone, PartialEq)]
153pub enum SchemaError<'s, 'v> {
154    TypeMismatch{expected: SchemaType, got: SchemaType},
155    RegexMiss{string: &'v str, re: &'s str},
156    FloatMiss{val: f64, min: f64, max: f64, nan_ok: bool},
157    IntMiss{val: i64, min: i64, max: i64},
158    ArrayCount{count: usize, min: usize, max: usize},
159    ArrayMiss{value: &'v Value, error: Box<SchemaError<'s,'v>>},
160    TableMiss{key: &'v str, value: &'v Value, errors: Vec<SchemaError<'s,'v>>},
161    AtKey{key: &'v String, error: Box<SchemaError<'s,'v>>},
162    InTableElement{val: &'v Value, error: Box<SchemaError<'s,'v>>},
163    TableCount{count: usize, min: usize, max: usize},
164    AlternativeMiss{val: &'v Value, errors: Vec<SchemaError<'s,'v>>},
165}
166
167
168
169impl<'s,'v> std::fmt::Debug for SchemaError<'s,'v> {
170    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171        match self {
172            Self::TypeMismatch{expected, got} => write!(f, "Expected {:?} but got {:?}", expected, got),
173            Self::RegexMiss{string, re} => write!(f, "Regex {:?} does not match {:?}", re, string),
174            Self::FloatMiss { val, min, max, nan_ok } => write!(f, "Float {:?} does not match [{:?},{:?}] (nan:{:?})", val,min,max,nan_ok),
175            Self::IntMiss { val, min, max } => write!(f, "Int {:?} does not match [{:?},{:?}]",val,min,max),
176            Self::ArrayCount { count, min, max } => write!(f, "Array count {:?} does not match [{:?},{:?}]",count,min,max),
177            Self::ArrayMiss { value, error } => write!(f, "Child of Array {:?} does not match because {:?}", value, error),
178            Self::TableMiss { key, value, errors } => write!(f, "No match for (key = {:?}, value = {:?}), error list : {:?}", key, value, errors),
179            Self::AtKey { key, error } => write!(f, "At key '{:?}', got ({:?})", key, error),
180            Self::InTableElement {val, error} => write!(f, "In Array (child {:?}), got ({:?})", val, error),
181            Self::TableCount { count, min, max } => write!(f, "Table extra count {:?} does not match [{:?},{:?}]",count,min,max),
182            Self::AlternativeMiss { val, errors } => write!(f, "No Alternative matched for {:?}, error list : {:?}", val, errors)
183        }
184    }
185}
186
187
188
189
190
191
192#[cfg(test)]
193mod test {
194    use super::*;
195    use std::sync::Once;
196
197    static INIT: Once = Once::new();
198
199    fn init_test() {
200        INIT.call_once(|| {
201        })
202    }
203
204    #[test]
205    fn parse_test() {
206        init_test();
207
208        let schema_toml = std::fs::read_to_string("test_files/test_schema.toml").unwrap().parse::<toml::Table>().unwrap(); 
209        
210        let schema = match TomlSchema::try_from(schema_toml) {
211            Ok(x) => x,
212            Err(e) => {panic!("{}", e.as_str());}
213        };
214
215        let test_file = std::fs::read_to_string("Cargo.toml").unwrap();
216        schema.check(
217            &test_file.parse().unwrap()
218        ).unwrap();
219    }
220
221    #[test]
222    fn parse_fail_test() {
223        init_test();
224
225        let schema_toml = std::fs::read_to_string("test_files/test_schema.toml").unwrap().parse::<toml::Table>().unwrap(); 
226        
227        let schema = match TomlSchema::try_from(schema_toml) {
228            Ok(x) => x,
229            Err(e) =>  {panic!("{}", e.as_str());}
230        };
231        let test_file = std::fs::read_to_string("test_files/test_schema.toml").unwrap();
232        schema.check(
233            &test_file.parse().unwrap()
234        ).unwrap_err();
235    }
236}