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}