yaml_schema/
lib.rs

1use std::rc::Rc;
2
3use hashlink::LinkedHashMap;
4
5pub mod engine;
6#[macro_use]
7pub mod error;
8pub mod loader;
9pub mod reference;
10pub mod schemas;
11pub mod validation;
12
13pub use engine::Engine;
14pub use error::Error;
15pub use reference::Reference;
16pub use schemas::AnyOfSchema;
17pub use schemas::ArraySchema;
18pub use schemas::BoolOrTypedSchema;
19pub use schemas::ConstSchema;
20pub use schemas::EnumSchema;
21pub use schemas::IntegerSchema;
22pub use schemas::NotSchema;
23pub use schemas::NumberSchema;
24pub use schemas::ObjectSchema;
25pub use schemas::OneOfSchema;
26pub use schemas::StringSchema;
27pub use validation::Context;
28pub use validation::Validator;
29
30use schemas::TypedSchema;
31
32// Returns the library version, which reflects the crate version
33pub fn version() -> String {
34    clap::crate_version!().to_string()
35}
36
37// Alias for std::result::Result<T, yaml_schema::Error>
38pub type Result<T> = std::result::Result<T, Error>;
39
40/// A RootSchema is a YamlSchema document
41#[derive(Debug, Default)]
42pub struct RootSchema {
43    pub id: Option<String>,
44    pub meta_schema: Option<String>,
45    pub defs: Option<LinkedHashMap<String, YamlSchema>>,
46    pub schema: Rc<YamlSchema>,
47}
48
49impl RootSchema {
50    /// Create a new RootSchema with a YamlSchema
51    pub fn new(schema: YamlSchema) -> RootSchema {
52        RootSchema {
53            id: None,
54            meta_schema: None,
55            defs: None,
56            schema: Rc::new(schema),
57        }
58    }
59
60    /// Create a new RootSchema with a Schema
61    pub fn new_with_schema(schema: Schema) -> RootSchema {
62        RootSchema::new(YamlSchema::from(schema))
63    }
64
65    /// Load a RootSchema from a file
66    pub fn load_file(path: &str) -> Result<RootSchema> {
67        loader::load_file(path)
68    }
69
70    pub fn load_from_str(schema: &str) -> Result<RootSchema> {
71        let docs = saphyr::Yaml::load_from_str(schema)?;
72        if docs.is_empty() {
73            return Ok(RootSchema::new(YamlSchema::empty())); // empty schema
74        }
75        loader::load_from_doc(docs.first().unwrap())
76    }
77
78    pub fn validate(&self, context: &Context, value: &saphyr::MarkedYaml) -> Result<()> {
79        self.schema.validate(context, value)?;
80        Ok(())
81    }
82
83    pub fn get_def(&self, name: &str) -> Option<&YamlSchema> {
84        if let Some(defs) = &self.defs {
85            return defs.get(&name.to_owned());
86        }
87        None
88    }
89}
90
91/// A Number is either an integer or a float
92#[derive(Debug, Clone, Copy, PartialEq)]
93pub enum Number {
94    Integer(i64),
95    Float(f64),
96}
97
98impl Number {
99    /// Create a new integer Number
100    pub fn integer(value: i64) -> Number {
101        Number::Integer(value)
102    }
103
104    /// Create a new float Number
105    pub fn float(value: f64) -> Number {
106        Number::Float(value)
107    }
108}
109
110impl std::fmt::Display for Number {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        match self {
113            Number::Integer(v) => write!(f, "{}", v),
114            Number::Float(v) => write!(f, "{}", v),
115        }
116    }
117}
118
119#[derive(Debug, PartialEq)]
120pub enum ConstValue {
121    Boolean(bool),
122    Null,
123    Number(Number),
124    String(String),
125}
126
127impl ConstValue {
128    pub fn boolean(value: bool) -> ConstValue {
129        ConstValue::Boolean(value)
130    }
131    pub fn integer(value: i64) -> ConstValue {
132        ConstValue::Number(Number::integer(value))
133    }
134    pub fn float(value: f64) -> ConstValue {
135        ConstValue::Number(Number::float(value))
136    }
137    pub fn null() -> ConstValue {
138        ConstValue::Null
139    }
140    pub fn string<V: Into<String>>(value: V) -> ConstValue {
141        ConstValue::String(value.into())
142    }
143    pub fn from_saphyr_yaml(value: &saphyr::Yaml) -> ConstValue {
144        match value {
145            saphyr::Yaml::Boolean(b) => ConstValue::Boolean(*b),
146            saphyr::Yaml::Integer(i) => ConstValue::Number(Number::integer(*i)),
147            saphyr::Yaml::Real(s) => ConstValue::Number(Number::float(s.parse::<f64>().unwrap())),
148            saphyr::Yaml::String(s) => ConstValue::String(s.clone()),
149            saphyr::Yaml::Null => ConstValue::Null,
150            _ => panic!("Expected a constant value, but got: {:?}", value),
151        }
152    }
153}
154
155impl TryFrom<&saphyr::YamlData<saphyr::MarkedYaml>> for ConstValue {
156    type Error = crate::Error;
157
158    fn try_from(value: &saphyr::YamlData<saphyr::MarkedYaml>) -> Result<Self> {
159        match value {
160            saphyr::YamlData::String(s) => Ok(ConstValue::String(s.clone())),
161            saphyr::YamlData::Integer(i) => Ok(ConstValue::Number(Number::integer(*i))),
162            saphyr::YamlData::Real(f) => {
163                let f = f.parse::<f64>()?;
164                Ok(ConstValue::Number(Number::float(f)))
165            }
166            saphyr::YamlData::Boolean(b) => Ok(ConstValue::Boolean(*b)),
167            saphyr::YamlData::Null => Ok(ConstValue::Null),
168            v => Err(unsupported_type!(
169                "Expected a constant value, but got: {:?}",
170                v
171            )),
172        }
173    }
174}
175
176impl TryFrom<saphyr::Yaml> for ConstValue {
177    type Error = crate::Error;
178
179    fn try_from(value: saphyr::Yaml) -> Result<Self> {
180        match value {
181            saphyr::Yaml::Boolean(b) => Ok(ConstValue::Boolean(b)),
182            saphyr::Yaml::Integer(i) => Ok(ConstValue::Number(Number::integer(i))),
183            saphyr::Yaml::Real(s) => {
184                let f = s.parse::<f64>()?;
185                Ok(ConstValue::Number(Number::float(f)))
186            }
187            saphyr::Yaml::String(s) => Ok(ConstValue::String(s.clone())),
188            saphyr::Yaml::Null => Ok(ConstValue::Null),
189            v => Err(unsupported_type!(
190                "Expected a constant value, but got: {:?}",
191                v
192            )),
193        }
194    }
195}
196
197impl std::fmt::Display for ConstValue {
198    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199        match self {
200            ConstValue::Boolean(b) => write!(f, "{} (bool)", b),
201            ConstValue::Null => write!(f, "null"),
202            ConstValue::Number(n) => write!(f, "{} (number)", n),
203            ConstValue::String(s) => write!(f, "\"{}\"", s),
204        }
205    }
206}
207
208/// YamlSchema is the core of the validation model
209#[derive(Debug, Default, PartialEq)]
210pub struct YamlSchema {
211    pub metadata: Option<LinkedHashMap<String, String>>,
212    pub r#ref: Option<Reference>,
213    pub schema: Option<Schema>,
214}
215
216impl From<Schema> for YamlSchema {
217    fn from(schema: Schema) -> Self {
218        YamlSchema {
219            schema: Some(schema),
220            ..Default::default()
221        }
222    }
223}
224
225impl YamlSchema {
226    pub fn empty() -> YamlSchema {
227        YamlSchema {
228            schema: Some(Schema::Empty),
229            ..Default::default()
230        }
231    }
232
233    pub fn null() -> YamlSchema {
234        YamlSchema {
235            schema: Some(Schema::TypeNull),
236            ..Default::default()
237        }
238    }
239
240    pub fn boolean_literal(value: bool) -> YamlSchema {
241        YamlSchema {
242            schema: Some(Schema::BooleanLiteral(value)),
243            ..Default::default()
244        }
245    }
246
247    pub fn reference(reference: Reference) -> YamlSchema {
248        YamlSchema {
249            r#ref: Some(reference),
250            ..Default::default()
251        }
252    }
253}
254
255#[derive(Debug, Default, PartialEq)]
256pub enum Schema {
257    #[default]
258    Empty, // no value
259    BooleanLiteral(bool),   // `true` or `false`
260    Const(ConstSchema),     // `const`
261    TypeNull,               // `type: null`
262    Array(ArraySchema),     // `type: array`
263    BooleanSchema,          // `type: boolean`
264    Integer(IntegerSchema), // `type: integer`
265    Number(NumberSchema),   // `type: number`
266    Object(ObjectSchema),   // `type: object`
267    String(StringSchema),   // `type: string`
268    Enum(EnumSchema),       // `enum`
269    AnyOf(AnyOfSchema),     // `anyOf`
270    OneOf(OneOfSchema),     // `oneOf`
271    Not(NotSchema),         // `not`
272}
273
274impl std::fmt::Display for Schema {
275    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
276        match &self {
277            Schema::Empty => write!(f, "<empty schema>"),
278            Schema::TypeNull => write!(f, "type: null"),
279            Schema::BooleanLiteral(b) => write!(f, "{}", b),
280            Schema::BooleanSchema => write!(f, "type: boolean"),
281            Schema::Const(c) => write!(f, "{}", c),
282            Schema::Enum(e) => write!(f, "{}", e),
283            Schema::Integer(i) => write!(f, "{}", i),
284            Schema::AnyOf(any_of_schema) => {
285                write!(f, "{}", any_of_schema)
286            }
287            Schema::OneOf(one_of_schema) => {
288                write!(f, "{}", one_of_schema)
289            }
290            Schema::Not(not_schema) => {
291                write!(f, "{}", not_schema)
292            }
293            Schema::String(s) => write!(f, "{}", s),
294            Schema::Number(n) => write!(f, "{}", n),
295            Schema::Object(o) => write!(f, "{}", o),
296            Schema::Array(a) => write!(f, "{}", a),
297        }
298    }
299}
300
301impl std::fmt::Display for YamlSchema {
302    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
303        write!(f, "{{")?;
304        if let Some(metadata) = &self.metadata {
305            write!(f, "metadata: {:?}, ", metadata)?;
306        }
307        if let Some(r#ref) = &self.r#ref {
308            r#ref.fmt(f)?;
309        }
310        if let Some(schema) = &self.schema {
311            write!(f, "schema: {}", schema)?;
312        }
313        write!(f, "}}")
314    }
315}
316
317/// Converts (upcast) a TypedSchema to a YamlSchema
318/// Since a YamlSchema is a superset of a TypedSchema, this is a lossless conversion
319impl From<TypedSchema> for Schema {
320    fn from(schema: TypedSchema) -> Self {
321        match schema {
322            TypedSchema::Array(array_schema) => Schema::Array(array_schema),
323            TypedSchema::BooleanSchema => Schema::BooleanSchema,
324            TypedSchema::Null => Schema::TypeNull,
325            TypedSchema::Integer(integer_schema) => Schema::Integer(integer_schema),
326            TypedSchema::Number(number_schema) => Schema::Number(number_schema),
327            TypedSchema::Object(object_schema) => Schema::Object(object_schema),
328            TypedSchema::String(string_schema) => Schema::String(string_schema),
329        }
330    }
331}
332
333/// Formats a vector of values as a string, by joining them with commas
334fn format_vec<V>(vec: &[V]) -> String
335where
336    V: std::fmt::Display,
337{
338    let items: Vec<String> = vec.iter().map(|v| format!("{}", v)).collect();
339    format!("[{}]", items.join(", "))
340}
341
342/// Formats a saphyr::YamlData as a string
343fn format_yaml_data(data: &saphyr::YamlData<saphyr::MarkedYaml>) -> String {
344    match data {
345        saphyr::YamlData::Null => "null".to_string(),
346        saphyr::YamlData::Boolean(b) => b.to_string(),
347        saphyr::YamlData::Integer(i) => i.to_string(),
348        saphyr::YamlData::Real(s) => s.clone(),
349        saphyr::YamlData::String(s) => format!("\"{}\"", s),
350        saphyr::YamlData::Array(array) => {
351            let items: Vec<String> = array.iter().map(|v| format_yaml_data(&v.data)).collect();
352            format!("[{}]", items.join(", "))
353        }
354        saphyr::YamlData::Hash(hash) => {
355            let items: Vec<String> = hash
356                .iter()
357                .map(|(k, v)| {
358                    format!(
359                        "{}: {}",
360                        format_yaml_data(&k.data),
361                        format_yaml_data(&v.data)
362                    )
363                })
364                .collect();
365            format!("[{}]", items.join(", "))
366        }
367        _ => format!("<unsupported type: {:?}>", data),
368    }
369}
370
371fn format_marker(marker: &saphyr::Marker) -> String {
372    format!("[{}, {}]", marker.line(), marker.col())
373}
374
375/// Use the ctor crate to initialize the logger for tests
376#[cfg(test)]
377#[ctor::ctor]
378fn init() {
379    env_logger::builder()
380        .filter_level(log::LevelFilter::Trace)
381        .format_target(false)
382        .format_timestamp_secs()
383        .target(env_logger::Target::Stdout)
384        .init();
385}
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390
391    #[test]
392    fn test_const_equality() {
393        let i1 = ConstValue::integer(42);
394        let i2 = ConstValue::integer(42);
395        assert_eq!(i1, i2);
396
397        let s1 = ConstValue::string("NW");
398        let s2 = ConstValue::string("NW");
399        assert_eq!(s1, s2);
400    }
401}