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}