1use hashlink::LinkedHashMap;
4use jsonptr::Pointer;
5use log::debug;
6use saphyr::MarkedYaml;
7use saphyr::Scalar;
8use saphyr::YamlData;
9
10#[macro_use]
11pub mod error;
12pub mod engine;
13pub mod loader;
14pub mod reference;
15pub mod schemas;
16pub mod utils;
17pub mod validation;
18
19pub use engine::Engine;
20pub use error::Error;
21pub use reference::Reference;
22pub use schemas::YamlSchema;
23pub use validation::Context;
24pub use validation::Validator;
25
26use utils::format_marker;
27
28use crate::loader::marked_yaml_to_string;
29
30pub fn version() -> String {
32 clap::crate_version!().to_string()
33}
34
35pub type Result<T> = std::result::Result<T, Error>;
37
38#[derive(Debug, PartialEq)]
42pub struct RootSchema<'r> {
43 pub meta_schema: Option<String>,
44 pub schema: YamlSchema<'r>,
45}
46
47impl<'r> RootSchema<'r> {
48 pub fn empty() -> Self {
50 Self {
51 meta_schema: None,
52 schema: YamlSchema::Empty,
53 }
54 }
55
56 pub fn new(schema: YamlSchema<'r>) -> Self {
58 Self {
59 meta_schema: None,
60 schema,
61 }
62 }
63
64 pub fn resolve(&self, pointer: &Pointer) -> Option<&YamlSchema<'_>> {
66 let components = pointer.components().collect::<Vec<_>>();
67 debug!("[RootSchema#resolve] components: {components:?}");
68 components.first().and_then(|component| {
69 debug!("[RootSchema#resolve] component: {component:?}");
70 match component {
71 jsonptr::Component::Root => {
72 let components = &components[1..];
73 components.first().and_then(|component| {
74 debug!("[RootSchema#resolve] component: {component:?}");
75 match component {
76 jsonptr::Component::Root => unimplemented!(),
77 jsonptr::Component::Token(token) => {
78 self.schema.resolve(Some(token), &components[1..])
79 }
80 }
81 })
82 }
83 jsonptr::Component::Token(token) => {
84 self.schema.resolve(Some(token), &components[1..])
85 }
86 }
87 })
88 }
89}
90
91impl<'r> TryFrom<&MarkedYaml<'r>> for RootSchema<'r> {
92 type Error = crate::Error;
93
94 fn try_from(marked_yaml: &MarkedYaml<'r>) -> Result<Self> {
95 match &marked_yaml.data {
96 YamlData::Value(scalar) => match scalar {
97 Scalar::Boolean(r#bool) => Ok(Self {
98 meta_schema: None,
99 schema: YamlSchema::<'r>::BooleanLiteral(*r#bool),
100 }),
101 Scalar::Null => Ok(RootSchema {
102 meta_schema: None,
103 schema: YamlSchema::<'r>::Null,
104 }),
105 _ => Err(generic_error!(
106 "[loader#load_from_doc] Don't know how to a handle scalar: {:?}",
107 scalar
108 )),
109 },
110 YamlData::Mapping(mapping) => {
111 debug!(
112 "[loader#load_from_doc] Found mapping, trying to load as RootSchema: {mapping:?}"
113 );
114 let meta_schema = mapping
115 .get(&MarkedYaml::value_from_str("$schema"))
116 .map(|my| marked_yaml_to_string(my, "$schema must be a string"))
117 .transpose()?;
118
119 let schema = YamlSchema::try_from(marked_yaml)?;
120 Ok(RootSchema {
121 meta_schema,
122 schema,
123 })
124 }
125 _ => Err(generic_error!(
126 "[loader#load_from_doc] Don't know how to load: {:?}",
127 marked_yaml
128 )),
129 }
130 }
131}
132
133impl Validator for RootSchema<'_> {
134 fn validate(&self, context: &Context, value: &saphyr::MarkedYaml) -> Result<()> {
135 self.schema.validate(context, value)
136 }
137}
138
139#[derive(Debug, Clone, Copy, PartialEq)]
141pub enum Number {
142 Integer(i64),
143 Float(f64),
144}
145
146impl Number {
147 pub fn integer(value: i64) -> Number {
149 Number::Integer(value)
150 }
151
152 pub fn float(value: f64) -> Number {
154 Number::Float(value)
155 }
156
157 pub fn to_f64(self) -> f64 {
158 match self {
159 Number::Integer(i) => i as f64,
160 Number::Float(f) => f,
161 }
162 }
163
164 pub fn is_multiple_of(self, divisor: Number) -> bool {
165 match (self, divisor) {
166 (Number::Integer(a), Number::Integer(b)) => b != 0 && a % b == 0,
167 _ => {
168 let d = divisor.to_f64();
169 d != 0.0 && self.to_f64() % d == 0.0
170 }
171 }
172 }
173}
174
175impl PartialOrd for Number {
176 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
177 match (self, other) {
178 (Number::Integer(a), Number::Integer(b)) => a.partial_cmp(b),
179 _ => self.to_f64().partial_cmp(&other.to_f64()),
180 }
181 }
182}
183
184impl TryFrom<&MarkedYaml<'_>> for Number {
185 type Error = Error;
186 fn try_from(value: &MarkedYaml) -> Result<Number> {
187 if let YamlData::Value(scalar) = &value.data {
188 match scalar {
189 Scalar::Integer(i) => Ok(Number::integer(*i)),
190 Scalar::FloatingPoint(o) => Ok(Number::float(o.into_inner())),
191 _ => Err(generic_error!(
192 "{} Expected type: integer or float, but got: {:?}",
193 format_marker(&value.span.start),
194 value
195 )),
196 }
197 } else {
198 Err(generic_error!(
199 "{} Expected scalar, but got: {:?}",
200 format_marker(&value.span.start),
201 value
202 ))
203 }
204 }
205}
206
207impl std::fmt::Display for Number {
208 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209 match self {
210 Number::Integer(v) => write!(f, "{v}"),
211 Number::Float(v) => write!(f, "{v}"),
212 }
213 }
214}
215
216#[derive(Debug, PartialEq)]
220pub enum ConstValue {
221 Null,
222 Boolean(bool),
223 Number(Number),
224 String(String),
225 Array(Vec<ConstValue>),
226 Object(LinkedHashMap<String, ConstValue>),
227}
228
229impl ConstValue {
230 pub fn null() -> ConstValue {
231 ConstValue::Null
232 }
233 pub fn boolean(value: bool) -> ConstValue {
234 ConstValue::Boolean(value)
235 }
236 pub fn integer(value: i64) -> ConstValue {
237 ConstValue::Number(Number::integer(value))
238 }
239 pub fn float(value: f64) -> ConstValue {
240 ConstValue::Number(Number::float(value))
241 }
242 pub fn string<V: Into<String>>(value: V) -> ConstValue {
243 ConstValue::String(value.into())
244 }
245
246 pub fn accepts(&self, value: &saphyr::MarkedYaml) -> bool {
247 match self {
248 ConstValue::Null => matches!(&value.data, YamlData::Value(Scalar::Null)),
249 ConstValue::Boolean(expected) => {
250 matches!(&value.data, YamlData::Value(Scalar::Boolean(actual)) if *expected == *actual)
251 }
252 ConstValue::Number(number) => match (number, &value.data) {
253 (Number::Integer(expected), YamlData::Value(Scalar::Integer(actual))) => {
254 *actual == *expected
255 }
256 (Number::Float(expected), YamlData::Value(Scalar::FloatingPoint(of))) => {
257 of.into_inner() == *expected
258 }
259 _ => false,
260 },
261 ConstValue::String(expected) => {
262 matches!(&value.data, YamlData::Value(Scalar::String(actual)) if expected == actual.as_ref())
263 }
264 ConstValue::Array(expected) => {
265 if let YamlData::Sequence(actual) = &value.data {
266 expected.len() == actual.len()
267 && expected
268 .iter()
269 .zip(actual.iter())
270 .all(|(exp, act)| exp.accepts(act))
271 } else {
272 false
273 }
274 }
275 ConstValue::Object(expected) => {
276 if let YamlData::Mapping(actual) = &value.data {
277 expected.len() == actual.len()
278 && expected.iter().all(|(key, exp_val)| {
279 let key_yaml = MarkedYaml::value_from_str(key);
280 actual
281 .get(&key_yaml)
282 .is_some_and(|act_yaml| exp_val.accepts(act_yaml))
283 })
284 } else {
285 false
286 }
287 }
288 }
289 }
290}
291
292impl TryFrom<&Scalar<'_>> for ConstValue {
293 type Error = crate::Error;
294
295 fn try_from(scalar: &Scalar) -> std::result::Result<ConstValue, Self::Error> {
296 match scalar {
297 Scalar::Null => Ok(ConstValue::Null),
298 Scalar::Boolean(b) => Ok(ConstValue::Boolean(*b)),
299 Scalar::Integer(i) => Ok(ConstValue::Number(Number::integer(*i))),
300 Scalar::FloatingPoint(o) => Ok(ConstValue::Number(Number::float(o.into_inner()))),
301 Scalar::String(s) => Ok(ConstValue::String(s.to_string())),
302 }
303 }
304}
305
306impl<'a> TryFrom<&YamlData<'a, MarkedYaml<'a>>> for ConstValue {
307 type Error = crate::Error;
308
309 fn try_from(value: &YamlData<'a, MarkedYaml<'a>>) -> Result<Self> {
310 match value {
311 YamlData::Value(scalar) => scalar.try_into(),
312 YamlData::Sequence(seq) => {
313 let arr = seq
314 .iter()
315 .map(|item| item.try_into())
316 .collect::<Result<Vec<_>>>()?;
317 Ok(ConstValue::Array(arr))
318 }
319 YamlData::Mapping(mapping) => {
320 let mut obj = LinkedHashMap::new();
321 for (key, val) in mapping.iter() {
322 let key_str = marked_yaml_to_string(key, "const object key must be a string")?;
323 let val_cv: ConstValue = val.try_into()?;
324 obj.insert(key_str, val_cv);
325 }
326 Ok(ConstValue::Object(obj))
327 }
328 YamlData::Tagged(_, inner) => (&inner.data).try_into(),
329 YamlData::Representation(_, _, _) | YamlData::Alias(_) | YamlData::BadValue => Err(
330 generic_error!("Unsupported YamlData variant for const: {:?}", value),
331 ),
332 }
333 }
334}
335
336impl<'a> TryFrom<&MarkedYaml<'a>> for ConstValue {
337 type Error = crate::Error;
338 fn try_from(value: &MarkedYaml<'a>) -> Result<ConstValue> {
339 (&value.data).try_into()
340 }
341}
342
343impl std::fmt::Display for ConstValue {
344 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
345 match self {
346 ConstValue::Boolean(b) => write!(f, "{b} (bool)"),
347 ConstValue::Null => write!(f, "null"),
348 ConstValue::Number(n) => write!(f, "{n} (number)"),
349 ConstValue::String(s) => write!(f, "\"{s}\""),
350 ConstValue::Array(arr) => {
351 write!(f, "[")?;
352 for (i, v) in arr.iter().enumerate() {
353 if i > 0 {
354 write!(f, ", ")?;
355 }
356 write!(f, "{v}")?;
357 }
358 write!(f, "]")
359 }
360 ConstValue::Object(obj) => {
361 write!(f, "{{")?;
362 for (i, (k, v)) in obj.iter().enumerate() {
363 if i > 0 {
364 write!(f, ", ")?;
365 }
366 write!(f, "\"{k}\": {v}")?;
367 }
368 write!(f, "}}")
369 }
370 }
371 }
372}
373
374#[cfg(test)]
376#[ctor::ctor]
377fn init() {
378 env_logger::builder()
379 .filter_level(log::LevelFilter::Trace)
380 .format_target(false)
381 .format_timestamp_secs()
382 .target(env_logger::Target::Stdout)
383 .init();
384}
385
386#[cfg(test)]
387mod tests {
388 use saphyr::LoadableYamlNode;
389
390 use super::*;
391 use ordered_float::OrderedFloat;
392
393 #[test]
394 fn test_const_equality() {
395 let i1 = ConstValue::integer(42);
396 let i2 = ConstValue::integer(42);
397 assert_eq!(i1, i2);
398
399 let s1 = ConstValue::string("NW");
400 let s2 = ConstValue::string("NW");
401 assert_eq!(s1, s2);
402 }
403
404 #[test]
405 #[allow(clippy::approx_constant)]
406 fn test_scalar_to_constvalue() -> Result<()> {
407 let scalars = [
408 Scalar::Null,
409 Scalar::Boolean(true),
410 Scalar::Boolean(false),
411 Scalar::Integer(42),
412 Scalar::Integer(-1),
413 Scalar::FloatingPoint(OrderedFloat::from(3.14)),
414 Scalar::String("foo".into()),
415 ];
416
417 let expected = [
418 ConstValue::Null,
419 ConstValue::Boolean(true),
420 ConstValue::Boolean(false),
421 ConstValue::Number(Number::Integer(42)),
422 ConstValue::Number(Number::Integer(-1)),
423 ConstValue::Number(Number::Float(3.14)),
424 ConstValue::String("foo".to_string()),
425 ];
426
427 for (scalar, expected) in scalars.iter().zip(expected.iter()) {
428 let actual: ConstValue = scalar.try_into()?;
429 assert_eq!(*expected, actual);
430 }
431
432 Ok(())
433 }
434
435 #[test]
436 fn test_const_value_array_try_from() -> Result<()> {
437 let docs = MarkedYaml::load_from_str("[1, 2, 3]").unwrap();
438 let cv: ConstValue = docs.first().unwrap().try_into()?;
439 assert_eq!(
440 cv,
441 ConstValue::Array(vec![
442 ConstValue::integer(1),
443 ConstValue::integer(2),
444 ConstValue::integer(3),
445 ])
446 );
447 Ok(())
448 }
449
450 #[test]
451 fn test_const_value_object_try_from() -> Result<()> {
452 let docs = MarkedYaml::load_from_str("a: 1\nb: two").unwrap();
453 let cv: ConstValue = docs.first().unwrap().try_into()?;
454 let mut expected = LinkedHashMap::new();
455 expected.insert("a".into(), ConstValue::integer(1));
456 expected.insert("b".into(), ConstValue::string("two"));
457 assert_eq!(cv, ConstValue::Object(expected));
458 Ok(())
459 }
460
461 #[test]
462 fn test_const_value_accepts_array() -> Result<()> {
463 let cv = ConstValue::Array(vec![ConstValue::integer(1), ConstValue::string("foo")]);
464 let matching = MarkedYaml::load_from_str("[1, \"foo\"]").unwrap();
465 let not_matching = MarkedYaml::load_from_str("[1, \"bar\"]").unwrap();
466 assert!(cv.accepts(matching.first().unwrap()));
467 assert!(!cv.accepts(not_matching.first().unwrap()));
468 Ok(())
469 }
470
471 #[test]
472 fn test_const_value_accepts_object() -> Result<()> {
473 let mut obj = LinkedHashMap::new();
474 obj.insert("x".into(), ConstValue::integer(42));
475 obj.insert("y".into(), ConstValue::string("hi"));
476 let cv = ConstValue::Object(obj);
477 let matching = MarkedYaml::load_from_str("x: 42\ny: hi").unwrap();
478 let not_matching = MarkedYaml::load_from_str("x: 43\ny: hi").unwrap();
479 assert!(cv.accepts(matching.first().unwrap()));
480 assert!(!cv.accepts(not_matching.first().unwrap()));
481 Ok(())
482 }
483}