yaml_schema/
lib.rs

1use std::rc::Rc;
2
3use hashlink::LinkedHashMap;
4use saphyr::LoadableYamlNode;
5use saphyr::MarkedYaml;
6use saphyr::Scalar;
7use saphyr::YamlData;
8
9pub mod engine;
10#[macro_use]
11pub mod error;
12pub mod loader;
13pub mod reference;
14pub mod schemas;
15pub mod utils;
16pub mod validation;
17
18pub use engine::Engine;
19pub use error::Error;
20pub use reference::Reference;
21pub use schemas::AnyOfSchema;
22pub use schemas::ArraySchema;
23pub use schemas::BoolOrTypedSchema;
24pub use schemas::ConstSchema;
25pub use schemas::EnumSchema;
26pub use schemas::IntegerSchema;
27pub use schemas::NotSchema;
28pub use schemas::NumberSchema;
29pub use schemas::ObjectSchema;
30pub use schemas::OneOfSchema;
31pub use schemas::Schema;
32pub use schemas::StringSchema;
33pub use schemas::TypedSchema;
34pub use schemas::YamlSchema;
35pub use validation::Context;
36pub use validation::Validator;
37
38use crate::utils::format_marker;
39
40// Returns the library version, which reflects the crate version
41pub fn version() -> String {
42    clap::crate_version!().to_string()
43}
44
45// Alias for std::result::Result<T, yaml_schema::Error>
46pub type Result<T> = std::result::Result<T, Error>;
47
48/// A RootSchema represents the root document in a schema file, and can include additional
49/// fields not present in the 'base' YamlSchema
50#[derive(Debug, Default, PartialEq)]
51pub struct RootSchema {
52    pub id: Option<String>,
53    pub meta_schema: Option<String>,
54    pub defs: Option<LinkedHashMap<String, YamlSchema>>,
55    pub schema: Rc<YamlSchema>,
56}
57
58impl RootSchema {
59    /// Create a new RootSchema with a YamlSchema
60    pub fn new(schema: YamlSchema) -> RootSchema {
61        RootSchema {
62            id: None,
63            meta_schema: None,
64            defs: None,
65            schema: Rc::new(schema),
66        }
67    }
68
69    /// Builder pattern for RootSchema
70    pub fn builder() -> RootSchemaBuilder {
71        RootSchemaBuilder::new()
72    }
73
74    /// Create a new RootSchema with a Schema
75    pub fn new_with_schema(schema: Schema) -> RootSchema {
76        RootSchema::new(YamlSchema::from(schema))
77    }
78
79    /// Load a RootSchema from a file
80    pub fn load_file(path: &str) -> Result<RootSchema> {
81        loader::load_file(path)
82    }
83
84    pub fn load_from_str(schema: &str) -> Result<RootSchema> {
85        let docs = MarkedYaml::load_from_str(schema)?;
86        if docs.is_empty() {
87            return Ok(RootSchema::new(YamlSchema::empty())); // empty schema
88        }
89        loader::load_from_doc(docs.first().unwrap())
90    }
91
92    pub fn validate(&self, context: &Context, value: &MarkedYaml) -> Result<()> {
93        self.schema.validate(context, value)?;
94        Ok(())
95    }
96
97    pub fn get_def(&self, name: &str) -> Option<&YamlSchema> {
98        if let Some(defs) = &self.defs {
99            return defs.get(&name.to_owned());
100        }
101        None
102    }
103}
104
105pub struct RootSchemaBuilder(RootSchema);
106
107impl Default for RootSchemaBuilder {
108    fn default() -> Self {
109        Self::new()
110    }
111}
112
113impl RootSchemaBuilder {
114    /// Construct a RootSchemaBuilder
115    pub fn new() -> Self {
116        Self(RootSchema::default())
117    }
118
119    pub fn build(&mut self) -> RootSchema {
120        std::mem::take(&mut self.0)
121    }
122
123    pub fn id<S: Into<String>>(&mut self, id: S) -> &mut Self {
124        self.0.id = Some(id.into());
125        self
126    }
127
128    pub fn meta_schema<S: Into<String>>(&mut self, meta_schema: S) -> &mut Self {
129        self.0.meta_schema = Some(meta_schema.into());
130        self
131    }
132
133    pub fn defs(&mut self, defs: LinkedHashMap<String, YamlSchema>) -> &mut Self {
134        self.0.defs = Some(defs);
135        self
136    }
137
138    pub fn schema(&mut self, schema: YamlSchema) -> &mut Self {
139        self.0.schema = Rc::new(schema);
140        self
141    }
142}
143
144/// A Number is either an integer or a float
145#[derive(Debug, Clone, Copy, PartialEq)]
146pub enum Number {
147    Integer(i64),
148    Float(f64),
149}
150
151impl Number {
152    /// Create a new integer Number
153    pub fn integer(value: i64) -> Number {
154        Number::Integer(value)
155    }
156
157    /// Create a new float Number
158    pub fn float(value: f64) -> Number {
159        Number::Float(value)
160    }
161}
162
163impl TryFrom<&MarkedYaml<'_>> for Number {
164    type Error = Error;
165    fn try_from(value: &MarkedYaml) -> Result<Number> {
166        if let YamlData::Value(scalar) = &value.data {
167            match scalar {
168                Scalar::Integer(i) => Ok(Number::integer(*i)),
169                Scalar::FloatingPoint(o) => Ok(Number::float(o.into_inner())),
170                _ => Err(generic_error!(
171                    "{} Expected type: integer or float, but got: {:?}",
172                    format_marker(&value.span.start),
173                    value
174                )),
175            }
176        } else {
177            Err(generic_error!(
178                "{} Expected scalar, but got: {:?}",
179                format_marker(&value.span.start),
180                value
181            ))
182        }
183    }
184}
185
186impl std::fmt::Display for Number {
187    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188        match self {
189            Number::Integer(v) => write!(f, "{v}"),
190            Number::Float(v) => write!(f, "{v}"),
191        }
192    }
193}
194
195/// A ConstValue is similar to a saphyr::Scalar, but for validating "number" types
196/// we treat integers and floating point values as 'fungible' and represent them
197/// using the `Number` enum.
198#[derive(Debug, PartialEq)]
199pub enum ConstValue {
200    Null,
201    Boolean(bool),
202    Number(Number),
203    String(String),
204}
205
206impl ConstValue {
207    pub fn null() -> ConstValue {
208        ConstValue::Null
209    }
210    pub fn boolean(value: bool) -> ConstValue {
211        ConstValue::Boolean(value)
212    }
213    pub fn integer(value: i64) -> ConstValue {
214        ConstValue::Number(Number::integer(value))
215    }
216    pub fn float(value: f64) -> ConstValue {
217        ConstValue::Number(Number::float(value))
218    }
219    pub fn string<V: Into<String>>(value: V) -> ConstValue {
220        ConstValue::String(value.into())
221    }
222}
223
224impl TryFrom<&Scalar<'_>> for ConstValue {
225    type Error = crate::Error;
226
227    fn try_from(scalar: &Scalar) -> std::result::Result<ConstValue, Self::Error> {
228        match scalar {
229            Scalar::Null => Ok(ConstValue::Null),
230            Scalar::Boolean(b) => Ok(ConstValue::Boolean(*b)),
231            Scalar::Integer(i) => Ok(ConstValue::Number(Number::integer(*i))),
232            Scalar::FloatingPoint(o) => Ok(ConstValue::Number(Number::float(o.into_inner()))),
233            Scalar::String(s) => Ok(ConstValue::String(s.to_string())),
234        }
235    }
236}
237
238impl<'a> TryFrom<&YamlData<'a, MarkedYaml<'a>>> for ConstValue {
239    type Error = crate::Error;
240
241    fn try_from(value: &YamlData<'a, MarkedYaml<'a>>) -> Result<Self> {
242        match value {
243            YamlData::Value(scalar) => scalar.try_into(),
244            v => Err(generic_error!("Expected a scalar value, but got: {:?}", v)),
245        }
246    }
247}
248
249impl<'a> TryFrom<&MarkedYaml<'a>> for ConstValue {
250    type Error = crate::Error;
251    fn try_from(value: &MarkedYaml<'a>) -> Result<ConstValue> {
252        match (&value.data).try_into() {
253            Ok(r) => Ok(r),
254            _ => Err(generic_error!(
255                "{} Expected a scalar value, but got: {:?}",
256                format_marker(&value.span.start),
257                value
258            )),
259        }
260    }
261}
262
263impl std::fmt::Display for ConstValue {
264    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265        match self {
266            ConstValue::Boolean(b) => write!(f, "{b} (bool)"),
267            ConstValue::Null => write!(f, "null"),
268            ConstValue::Number(n) => write!(f, "{n} (number)"),
269            ConstValue::String(s) => write!(f, "\"{s}\""),
270        }
271    }
272}
273
274/// Use the ctor crate to initialize the logger for tests
275#[cfg(test)]
276#[ctor::ctor]
277fn init() {
278    env_logger::builder()
279        .filter_level(log::LevelFilter::Trace)
280        .format_target(false)
281        .format_timestamp_secs()
282        .target(env_logger::Target::Stdout)
283        .init();
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289    use ordered_float::OrderedFloat;
290
291    #[test]
292    fn test_const_equality() {
293        let i1 = ConstValue::integer(42);
294        let i2 = ConstValue::integer(42);
295        assert_eq!(i1, i2);
296
297        let s1 = ConstValue::string("NW");
298        let s2 = ConstValue::string("NW");
299        assert_eq!(s1, s2);
300    }
301
302    #[test]
303    fn test_scalar_to_constvalue() -> Result<()> {
304        let scalars = [
305            Scalar::Null,
306            Scalar::Boolean(true),
307            Scalar::Boolean(false),
308            Scalar::Integer(42),
309            Scalar::Integer(-1),
310            Scalar::FloatingPoint(OrderedFloat::from(3.14)),
311            Scalar::String("foo".into()),
312        ];
313
314        let expected = [
315            ConstValue::Null,
316            ConstValue::Boolean(true),
317            ConstValue::Boolean(false),
318            ConstValue::Number(Number::Integer(42)),
319            ConstValue::Number(Number::Integer(-1)),
320            ConstValue::Number(Number::Float(3.14)),
321            ConstValue::String("foo".to_string()),
322        ];
323
324        for (scalar, expected) in scalars.iter().zip(expected.iter()) {
325            let actual: ConstValue = scalar.try_into()?;
326            assert_eq!(*expected, actual);
327        }
328
329        Ok(())
330    }
331}