1use hashlink::LinkedHashMap;
4use saphyr::MarkedYaml;
5use saphyr::Scalar;
6use saphyr::YamlData;
7
8#[macro_use]
9pub mod error;
10pub mod engine;
11pub mod loader;
12pub mod reference;
13pub mod schemas;
14pub mod utils;
15pub mod validation;
16
17pub use engine::Engine;
18pub use error::Error;
19pub use reference::RefUri;
20pub use reference::Reference;
21pub use schemas::RootSchema;
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, Clone, Copy, PartialEq)]
40pub enum Number {
41 Integer(i64),
42 Float(f64),
43}
44
45impl Number {
46 pub fn integer(value: i64) -> Number {
48 Number::Integer(value)
49 }
50
51 pub fn float(value: f64) -> Number {
53 Number::Float(value)
54 }
55
56 pub fn to_f64(self) -> f64 {
57 match self {
58 Number::Integer(i) => i as f64,
59 Number::Float(f) => f,
60 }
61 }
62
63 pub fn is_multiple_of(self, divisor: Number) -> bool {
64 match (self, divisor) {
65 (Number::Integer(a), Number::Integer(b)) => b != 0 && a % b == 0,
66 _ => {
67 let d = divisor.to_f64();
68 d != 0.0 && self.to_f64() % d == 0.0
69 }
70 }
71 }
72}
73
74impl PartialOrd for Number {
75 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
76 match (self, other) {
77 (Number::Integer(a), Number::Integer(b)) => a.partial_cmp(b),
78 _ => self.to_f64().partial_cmp(&other.to_f64()),
79 }
80 }
81}
82
83impl TryFrom<&MarkedYaml<'_>> for Number {
84 type Error = Error;
85 fn try_from(value: &MarkedYaml) -> Result<Number> {
86 if let YamlData::Value(scalar) = &value.data {
87 match scalar {
88 Scalar::Integer(i) => Ok(Number::integer(*i)),
89 Scalar::FloatingPoint(o) => Ok(Number::float(o.into_inner())),
90 _ => Err(generic_error!(
91 "{} Expected type: integer or float, but got: {:?}",
92 format_marker(&value.span.start),
93 value
94 )),
95 }
96 } else {
97 Err(generic_error!(
98 "{} Expected scalar, but got: {:?}",
99 format_marker(&value.span.start),
100 value
101 ))
102 }
103 }
104}
105
106impl std::fmt::Display for Number {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 match self {
109 Number::Integer(v) => write!(f, "{v}"),
110 Number::Float(v) => write!(f, "{v}"),
111 }
112 }
113}
114
115#[derive(Debug, PartialEq)]
119pub enum ConstValue {
120 Null,
121 Boolean(bool),
122 Number(Number),
123 String(String),
124 Array(Vec<ConstValue>),
125 Object(LinkedHashMap<String, ConstValue>),
126}
127
128impl ConstValue {
129 pub fn null() -> ConstValue {
130 ConstValue::Null
131 }
132 pub fn boolean(value: bool) -> ConstValue {
133 ConstValue::Boolean(value)
134 }
135 pub fn integer(value: i64) -> ConstValue {
136 ConstValue::Number(Number::integer(value))
137 }
138 pub fn float(value: f64) -> ConstValue {
139 ConstValue::Number(Number::float(value))
140 }
141 pub fn string<V: Into<String>>(value: V) -> ConstValue {
142 ConstValue::String(value.into())
143 }
144
145 pub fn accepts(&self, value: &saphyr::MarkedYaml) -> bool {
146 match self {
147 ConstValue::Null => matches!(&value.data, YamlData::Value(Scalar::Null)),
148 ConstValue::Boolean(expected) => {
149 matches!(&value.data, YamlData::Value(Scalar::Boolean(actual)) if *expected == *actual)
150 }
151 ConstValue::Number(number) => match (number, &value.data) {
152 (Number::Integer(expected), YamlData::Value(Scalar::Integer(actual))) => {
153 *actual == *expected
154 }
155 (Number::Float(expected), YamlData::Value(Scalar::FloatingPoint(of))) => {
156 of.into_inner() == *expected
157 }
158 _ => false,
159 },
160 ConstValue::String(expected) => {
161 matches!(&value.data, YamlData::Value(Scalar::String(actual)) if expected == actual.as_ref())
162 }
163 ConstValue::Array(expected) => {
164 if let YamlData::Sequence(actual) = &value.data {
165 expected.len() == actual.len()
166 && expected
167 .iter()
168 .zip(actual.iter())
169 .all(|(exp, act)| exp.accepts(act))
170 } else {
171 false
172 }
173 }
174 ConstValue::Object(expected) => {
175 if let YamlData::Mapping(actual) = &value.data {
176 expected.len() == actual.len()
177 && expected.iter().all(|(key, exp_val)| {
178 let key_yaml = MarkedYaml::value_from_str(key);
179 actual
180 .get(&key_yaml)
181 .is_some_and(|act_yaml| exp_val.accepts(act_yaml))
182 })
183 } else {
184 false
185 }
186 }
187 }
188 }
189}
190
191impl TryFrom<&Scalar<'_>> for ConstValue {
192 type Error = crate::Error;
193
194 fn try_from(scalar: &Scalar) -> std::result::Result<ConstValue, Self::Error> {
195 match scalar {
196 Scalar::Null => Ok(ConstValue::Null),
197 Scalar::Boolean(b) => Ok(ConstValue::Boolean(*b)),
198 Scalar::Integer(i) => Ok(ConstValue::Number(Number::integer(*i))),
199 Scalar::FloatingPoint(o) => Ok(ConstValue::Number(Number::float(o.into_inner()))),
200 Scalar::String(s) => Ok(ConstValue::String(s.to_string())),
201 }
202 }
203}
204
205impl<'a> TryFrom<&YamlData<'a, MarkedYaml<'a>>> for ConstValue {
206 type Error = crate::Error;
207
208 fn try_from(value: &YamlData<'a, MarkedYaml<'a>>) -> Result<Self> {
209 match value {
210 YamlData::Value(scalar) => scalar.try_into(),
211 YamlData::Sequence(seq) => {
212 let arr = seq
213 .iter()
214 .map(|item| item.try_into())
215 .collect::<Result<Vec<_>>>()?;
216 Ok(ConstValue::Array(arr))
217 }
218 YamlData::Mapping(mapping) => {
219 let mut obj = LinkedHashMap::new();
220 for (key, val) in mapping.iter() {
221 let key_str = marked_yaml_to_string(key, "const object key must be a string")?;
222 let val_cv: ConstValue = val.try_into()?;
223 obj.insert(key_str, val_cv);
224 }
225 Ok(ConstValue::Object(obj))
226 }
227 YamlData::Tagged(_, inner) => (&inner.data).try_into(),
228 YamlData::Representation(_, _, _) | YamlData::Alias(_) | YamlData::BadValue => Err(
229 generic_error!("Unsupported YamlData variant for const: {:?}", value),
230 ),
231 }
232 }
233}
234
235impl<'a> TryFrom<&MarkedYaml<'a>> for ConstValue {
236 type Error = crate::Error;
237 fn try_from(value: &MarkedYaml<'a>) -> Result<ConstValue> {
238 (&value.data).try_into()
239 }
240}
241
242impl std::fmt::Display for ConstValue {
243 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244 match self {
245 ConstValue::Boolean(b) => write!(f, "{b} (bool)"),
246 ConstValue::Null => write!(f, "null"),
247 ConstValue::Number(n) => write!(f, "{n} (number)"),
248 ConstValue::String(s) => write!(f, "\"{s}\""),
249 ConstValue::Array(arr) => {
250 write!(f, "[")?;
251 for (i, v) in arr.iter().enumerate() {
252 if i > 0 {
253 write!(f, ", ")?;
254 }
255 write!(f, "{v}")?;
256 }
257 write!(f, "]")
258 }
259 ConstValue::Object(obj) => {
260 write!(f, "{{")?;
261 for (i, (k, v)) in obj.iter().enumerate() {
262 if i > 0 {
263 write!(f, ", ")?;
264 }
265 write!(f, "\"{k}\": {v}")?;
266 }
267 write!(f, "}}")
268 }
269 }
270 }
271}
272
273#[cfg(test)]
275#[ctor::ctor]
276fn init() {
277 env_logger::builder()
278 .filter_level(log::LevelFilter::Trace)
279 .format_target(false)
280 .format_timestamp_secs()
281 .target(env_logger::Target::Stdout)
282 .init();
283}
284
285#[cfg(test)]
286mod tests {
287 use saphyr::LoadableYamlNode;
288
289 use super::*;
290 use ordered_float::OrderedFloat;
291
292 #[test]
293 fn test_const_equality() {
294 let i1 = ConstValue::integer(42);
295 let i2 = ConstValue::integer(42);
296 assert_eq!(i1, i2);
297
298 let s1 = ConstValue::string("NW");
299 let s2 = ConstValue::string("NW");
300 assert_eq!(s1, s2);
301 }
302
303 #[test]
304 #[allow(clippy::approx_constant)]
305 fn test_scalar_to_constvalue() -> Result<()> {
306 let scalars = [
307 Scalar::Null,
308 Scalar::Boolean(true),
309 Scalar::Boolean(false),
310 Scalar::Integer(42),
311 Scalar::Integer(-1),
312 Scalar::FloatingPoint(OrderedFloat::from(3.14)),
313 Scalar::String("foo".into()),
314 ];
315
316 let expected = [
317 ConstValue::Null,
318 ConstValue::Boolean(true),
319 ConstValue::Boolean(false),
320 ConstValue::Number(Number::Integer(42)),
321 ConstValue::Number(Number::Integer(-1)),
322 ConstValue::Number(Number::Float(3.14)),
323 ConstValue::String("foo".to_string()),
324 ];
325
326 for (scalar, expected) in scalars.iter().zip(expected.iter()) {
327 let actual: ConstValue = scalar.try_into()?;
328 assert_eq!(*expected, actual);
329 }
330
331 Ok(())
332 }
333
334 #[test]
335 fn test_const_value_array_try_from() -> Result<()> {
336 let docs = MarkedYaml::load_from_str("[1, 2, 3]").unwrap();
337 let cv: ConstValue = docs.first().unwrap().try_into()?;
338 assert_eq!(
339 cv,
340 ConstValue::Array(vec![
341 ConstValue::integer(1),
342 ConstValue::integer(2),
343 ConstValue::integer(3),
344 ])
345 );
346 Ok(())
347 }
348
349 #[test]
350 fn test_const_value_object_try_from() -> Result<()> {
351 let docs = MarkedYaml::load_from_str("a: 1\nb: two").unwrap();
352 let cv: ConstValue = docs.first().unwrap().try_into()?;
353 let mut expected = LinkedHashMap::new();
354 expected.insert("a".into(), ConstValue::integer(1));
355 expected.insert("b".into(), ConstValue::string("two"));
356 assert_eq!(cv, ConstValue::Object(expected));
357 Ok(())
358 }
359
360 #[test]
361 fn test_const_value_accepts_array() -> Result<()> {
362 let cv = ConstValue::Array(vec![ConstValue::integer(1), ConstValue::string("foo")]);
363 let matching = MarkedYaml::load_from_str("[1, \"foo\"]").unwrap();
364 let not_matching = MarkedYaml::load_from_str("[1, \"bar\"]").unwrap();
365 assert!(cv.accepts(matching.first().unwrap()));
366 assert!(!cv.accepts(not_matching.first().unwrap()));
367 Ok(())
368 }
369
370 #[test]
371 fn test_const_value_accepts_object() -> Result<()> {
372 let mut obj = LinkedHashMap::new();
373 obj.insert("x".into(), ConstValue::integer(42));
374 obj.insert("y".into(), ConstValue::string("hi"));
375 let cv = ConstValue::Object(obj);
376 let matching = MarkedYaml::load_from_str("x: 42\ny: hi").unwrap();
377 let not_matching = MarkedYaml::load_from_str("x: 43\ny: hi").unwrap();
378 assert!(cv.accepts(matching.first().unwrap()));
379 assert!(!cv.accepts(not_matching.first().unwrap()));
380 Ok(())
381 }
382}