Skip to main content

noyalib/
schema.rs

1//! YAML 1.2 schema validation helpers.
2//!
3//! This module provides validation functions for the three standard YAML 1.2
4//! schemas defined in the YAML 1.2.2 specification, §10:
5//!
6//! - **Failsafe Schema**: The most basic schema, supporting only strings,
7//!   sequences, and mappings.
8//! - **JSON Schema (YAML 1.2)**: Extends failsafe with nulls, booleans,
9//!   integers, and floats. Note: this is the *YAML 1.2 JSON-compatible*
10//!   schema level — it is **not** the JSON Schema 2020-12 (`json-schema.org`)
11//!   document validation language. A native JSON Schema 2020-12 validator
12//!   is tracked for a future release.
13//! - **Core Schema**: The default YAML 1.2 schema, with more flexible tag
14//!   resolution.
15//!
16//! # Examples
17//!
18//! ```rust
19//! use noyalib::{
20//!     from_str, validate_yaml_core_schema, validate_yaml_json_schema, Value,
21//! };
22//!
23//! let yaml = "count: 42\nenabled: true";
24//! let value: Value = from_str(yaml).unwrap();
25//!
26//! // Validate against the YAML 1.2 JSON-compatible schema level
27//! assert!(validate_yaml_json_schema(&value).is_ok());
28//!
29//! // Validate against the YAML 1.2 Core schema (the default)
30//! assert!(validate_yaml_core_schema(&value).is_ok());
31//! ```
32
33// SPDX-License-Identifier: MIT OR Apache-2.0
34// Copyright (c) 2026 Noyalib. All rights reserved.
35
36use crate::error::{Error, Result};
37use crate::prelude::*;
38use crate::value::{Number, Value};
39
40/// Validate a value against the YAML 1.2 Failsafe Schema.
41///
42/// The failsafe schema is the most basic schema and supports only:
43/// - Strings (tag:yaml.org,2002:str)
44/// - Sequences (tag:yaml.org,2002:seq)
45/// - Mappings (tag:yaml.org,2002:map)
46///
47/// # Errors
48///
49/// Returns an error if the value contains types not supported by the failsafe
50/// schema (nulls, booleans, numbers, or tagged values).
51///
52/// # Examples
53///
54/// ```rust
55/// use noyalib::{validate_yaml_failsafe_schema, Mapping, Value};
56///
57/// // Valid failsafe values
58/// let valid = Value::String("hello".to_string());
59/// assert!(validate_yaml_failsafe_schema(&valid).is_ok());
60///
61/// // Invalid: numbers not allowed in failsafe
62/// let invalid = Value::from(42);
63/// assert!(validate_yaml_failsafe_schema(&invalid).is_err());
64/// ```
65pub fn validate_yaml_failsafe_schema(value: &Value) -> Result<()> {
66    validate_failsafe_recursive(value, &mut String::from("root"))
67}
68
69fn validate_failsafe_recursive(value: &Value, path: &mut String) -> Result<()> {
70    use core::fmt::Write as _;
71    match value {
72        Value::String(_) => Ok(()),
73        Value::Sequence(seq) => {
74            let base_len = path.len();
75            for (i, item) in seq.iter().enumerate() {
76                let _ = write!(path, "[{i}]");
77                validate_failsafe_recursive(item, path)?;
78                path.truncate(base_len);
79            }
80            Ok(())
81        }
82        Value::Mapping(map) => {
83            let base_len = path.len();
84            for (key, val) in map {
85                let _ = write!(path, ".{key}");
86                validate_failsafe_recursive(val, path)?;
87                path.truncate(base_len);
88            }
89            Ok(())
90        }
91        Value::Null => Err(Error::Invalid(format!(
92            "failsafe schema: null not allowed at {path}"
93        ))),
94        Value::Bool(_) => Err(Error::Invalid(format!(
95            "failsafe schema: boolean not allowed at {path}"
96        ))),
97        Value::Number(_) => Err(Error::Invalid(format!(
98            "failsafe schema: number not allowed at {path}"
99        ))),
100        Value::Tagged(_) => Err(Error::Invalid(format!(
101            "failsafe schema: tagged values not allowed at {path}"
102        ))),
103    }
104}
105
106/// Validate a value against the YAML 1.2 JSON-compatible schema level.
107///
108/// **Not** the JSON Schema 2020-12 document validation language. This validates
109/// that a value uses only types that round-trip through JSON: nulls, booleans,
110/// integers, finite floats, strings, sequences, and mappings — i.e. the YAML
111/// 1.2.2 §10.2 *JSON Schema*. A native validator for the
112/// `https://json-schema.org/draft/2020-12/schema` language is tracked for a
113/// future release; this name space is reserved.
114///
115/// The JSON schema extends the failsafe schema with:
116/// - Null (tag:yaml.org,2002:null)
117/// - Boolean (tag:yaml.org,2002:bool)
118/// - Integer (tag:yaml.org,2002:int)
119/// - Float (tag:yaml.org,2002:float)
120/// - Strings, sequences, and mappings from failsafe
121///
122/// # Errors
123///
124/// Returns an error if the value contains types not supported by JSON schema
125/// (primarily tagged values with non-standard tags) or non-finite floats
126/// (NaN / Infinity), which JSON cannot represent.
127///
128/// # Examples
129///
130/// ```rust
131/// use noyalib::{validate_yaml_json_schema, Value};
132///
133/// // Valid JSON schema values
134/// let valid = Value::from(42);
135/// assert!(validate_yaml_json_schema(&valid).is_ok());
136///
137/// let null = Value::Null;
138/// assert!(validate_yaml_json_schema(&null).is_ok());
139/// ```
140pub fn validate_yaml_json_schema(value: &Value) -> Result<()> {
141    validate_json_recursive(value, &mut String::from("root"))
142}
143
144fn validate_json_recursive(value: &Value, path: &mut String) -> Result<()> {
145    use core::fmt::Write as _;
146    match value {
147        Value::Null | Value::Bool(_) | Value::String(_) => Ok(()),
148        Value::Number(n) => {
149            // JSON doesn't support NaN or Infinity
150            if let Number::Float(f) = n
151                && (f.is_nan() || f.is_infinite())
152            {
153                return Err(Error::Invalid(format!(
154                    "JSON schema: NaN/Infinity not allowed at {path}"
155                )));
156            }
157            Ok(())
158        }
159        Value::Sequence(seq) => {
160            let base_len = path.len();
161            for (i, item) in seq.iter().enumerate() {
162                let _ = write!(path, "[{i}]");
163                validate_json_recursive(item, path)?;
164                path.truncate(base_len);
165            }
166            Ok(())
167        }
168        Value::Mapping(map) => {
169            let base_len = path.len();
170            for (key, val) in map {
171                let _ = write!(path, ".{key}");
172                validate_json_recursive(val, path)?;
173                path.truncate(base_len);
174            }
175            Ok(())
176        }
177        Value::Tagged(_) => Err(Error::Invalid(format!(
178            "JSON schema: tagged values not allowed at {path}"
179        ))),
180    }
181}
182
183/// Validate a value against the YAML 1.2 Core Schema.
184///
185/// The core schema is the default YAML schema and is the most flexible.
186/// It supports all JSON schema types plus:
187/// - NaN and Infinity for floats
188/// - Tagged values with standard YAML tags
189///
190/// # Errors
191///
192/// Returns an error only for malformed values (this is very permissive).
193///
194/// # Examples
195///
196/// ```rust
197/// use noyalib::{validate_yaml_core_schema, Value};
198///
199/// // Core schema accepts everything
200/// let value = Value::from(f64::NAN);
201/// assert!(validate_yaml_core_schema(&value).is_ok());
202///
203/// let null = Value::Null;
204/// assert!(validate_yaml_core_schema(&null).is_ok());
205/// ```
206pub fn validate_yaml_core_schema(value: &Value) -> Result<()> {
207    validate_core_recursive(value, &mut String::from("root"))
208}
209
210fn validate_core_recursive(value: &Value, path: &mut String) -> Result<()> {
211    use core::fmt::Write as _;
212    match value {
213        Value::Null | Value::Bool(_) | Value::String(_) | Value::Number(_) => Ok(()),
214        Value::Sequence(seq) => {
215            let base_len = path.len();
216            for (i, item) in seq.iter().enumerate() {
217                let _ = write!(path, "[{i}]");
218                validate_core_recursive(item, path)?;
219                path.truncate(base_len);
220            }
221            Ok(())
222        }
223        Value::Mapping(map) => {
224            let base_len = path.len();
225            for (key, val) in map {
226                let _ = write!(path, ".{key}");
227                validate_core_recursive(val, path)?;
228                path.truncate(base_len);
229            }
230            Ok(())
231        }
232        Value::Tagged(tagged) => {
233            let base_len = path.len();
234            let _ = write!(path, "!{}", tagged.tag());
235            let result = validate_core_recursive(tagged.value(), path);
236            path.truncate(base_len);
237            result
238        }
239    }
240}
241
242/// Check if a value is valid against the YAML 1.2 JSON-compatible schema.
243///
244/// Equivalent to [`validate_yaml_json_schema`] but returns a boolean.
245/// **Not** related to JSON Schema 2020-12 document validation.
246///
247/// # Examples
248///
249/// ```
250/// use noyalib::{is_yaml_json_compatible, Value};
251/// assert!(is_yaml_json_compatible(&Value::from(42)));
252/// ```
253#[must_use]
254pub fn is_yaml_json_compatible(value: &Value) -> bool {
255    validate_yaml_json_schema(value).is_ok()
256}
257
258/// Check if a value uses only YAML 1.2 Failsafe schema types.
259///
260/// # Examples
261///
262/// ```
263/// use noyalib::{is_yaml_failsafe_compatible, Value};
264/// assert!(is_yaml_failsafe_compatible(&Value::String("x".into())));
265/// assert!(!is_yaml_failsafe_compatible(&Value::from(42)));
266/// ```
267#[must_use]
268pub fn is_yaml_failsafe_compatible(value: &Value) -> bool {
269    validate_yaml_failsafe_schema(value).is_ok()
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275    use crate::value::Mapping;
276
277    #[test]
278    fn test_failsafe_valid() {
279        assert!(validate_yaml_failsafe_schema(&Value::String("test".to_string())).is_ok());
280        assert!(validate_yaml_failsafe_schema(&Value::Sequence(vec![])).is_ok());
281        assert!(validate_yaml_failsafe_schema(&Value::Mapping(Mapping::new())).is_ok());
282    }
283
284    #[test]
285    fn test_failsafe_invalid() {
286        assert!(validate_yaml_failsafe_schema(&Value::Null).is_err());
287        assert!(validate_yaml_failsafe_schema(&Value::Bool(true)).is_err());
288        assert!(validate_yaml_failsafe_schema(&Value::from(42)).is_err());
289    }
290
291    #[test]
292    fn test_json_valid() {
293        assert!(validate_yaml_json_schema(&Value::Null).is_ok());
294        assert!(validate_yaml_json_schema(&Value::Bool(true)).is_ok());
295        assert!(validate_yaml_json_schema(&Value::from(42)).is_ok());
296        assert!(validate_yaml_json_schema(&Value::from(3.125)).is_ok());
297        assert!(validate_yaml_json_schema(&Value::String("test".to_string())).is_ok());
298    }
299
300    #[test]
301    fn test_json_invalid_nan() {
302        let nan = Value::from(f64::NAN);
303        assert!(validate_yaml_json_schema(&nan).is_err());
304    }
305
306    #[test]
307    fn test_json_invalid_infinity() {
308        let inf = Value::from(f64::INFINITY);
309        assert!(validate_yaml_json_schema(&inf).is_err());
310    }
311
312    #[test]
313    fn test_core_accepts_all() {
314        assert!(validate_yaml_core_schema(&Value::Null).is_ok());
315        assert!(validate_yaml_core_schema(&Value::Bool(true)).is_ok());
316        assert!(validate_yaml_core_schema(&Value::from(42)).is_ok());
317        assert!(validate_yaml_core_schema(&Value::from(f64::NAN)).is_ok());
318        assert!(validate_yaml_core_schema(&Value::from(f64::INFINITY)).is_ok());
319    }
320
321    #[test]
322    fn test_is_yaml_json_compatible() {
323        assert!(is_yaml_json_compatible(&Value::from(42)));
324        assert!(!is_yaml_json_compatible(&Value::from(f64::NAN)));
325    }
326
327    #[test]
328    fn test_is_yaml_failsafe_compatible() {
329        assert!(is_yaml_failsafe_compatible(&Value::String(
330            "test".to_string()
331        )));
332        assert!(!is_yaml_failsafe_compatible(&Value::from(42)));
333    }
334}