1use crate::version::YamlVersion;
4use crate::{Error, Position};
5use std::collections::HashMap;
6
7#[must_use]
14pub fn value_tag_error(position: Position) -> Error {
15 Error::construction(
16 position,
17 "the YAML 1.1 `=` indicator (tag:yaml.org,2002:value) has no \
18 constructor in rust-yaml; drop the `%YAML 1.1` directive or \
19 quote the value (`'='`) to keep it as a string",
20 )
21}
22
23#[derive(Debug, Clone, Copy, PartialEq)]
28pub enum PlainScalarType {
29 Null,
31 Bool(bool),
34 Int(i64),
36 Float(f64),
38 Str,
40 Value,
48}
49
50#[must_use]
60pub fn resolve_plain_scalar(value: &str, version: YamlVersion) -> PlainScalarType {
61 if value.is_empty() {
64 return PlainScalarType::Null;
65 }
66
67 if let Ok(i) = value.parse::<i64>() {
69 return PlainScalarType::Int(i);
70 }
71
72 if let Some(i) = resolve_radix_int(value) {
75 return PlainScalarType::Int(i);
76 }
77
78 if let Some(f) = resolve_inf_nan(value) {
81 return PlainScalarType::Float(f);
82 }
83
84 if value.bytes().any(|b| b.is_ascii_digit()) {
88 if let Ok(f) = value.parse::<f64>() {
89 return PlainScalarType::Float(f);
90 }
91 }
92
93 if version == YamlVersion::V1_1 && value == "=" {
97 return PlainScalarType::Value;
98 }
99
100 let lower = value.to_lowercase();
101 match lower.as_str() {
102 "true" => PlainScalarType::Bool(true),
103 "false" => PlainScalarType::Bool(false),
104 "null" | "~" => PlainScalarType::Null,
105 "yes" | "on" if version == YamlVersion::V1_1 => PlainScalarType::Bool(true),
106 "no" | "off" if version == YamlVersion::V1_1 => PlainScalarType::Bool(false),
107 _ => PlainScalarType::Str,
108 }
109}
110
111fn resolve_radix_int(value: &str) -> Option<i64> {
117 let (radix, digits) = if let Some(d) = value
118 .strip_prefix("0x")
119 .or_else(|| value.strip_prefix("0X"))
120 {
121 (16, d)
122 } else if let Some(d) = value
123 .strip_prefix("0o")
124 .or_else(|| value.strip_prefix("0O"))
125 {
126 (8, d)
127 } else if let Some(d) = value
128 .strip_prefix("0b")
129 .or_else(|| value.strip_prefix("0B"))
130 {
131 (2, d)
132 } else {
133 return None;
134 };
135 if digits.is_empty() {
136 return None;
137 }
138 i64::from_str_radix(digits, radix).ok()
139}
140
141fn resolve_inf_nan(value: &str) -> Option<f64> {
144 match value {
145 ".inf" | ".Inf" | ".INF" | "+.inf" | "+.Inf" | "+.INF" => Some(f64::INFINITY),
146 "-.inf" | "-.Inf" | "-.INF" => Some(f64::NEG_INFINITY),
147 ".nan" | ".NaN" | ".NAN" => Some(f64::NAN),
148 _ => None,
149 }
150}
151
152pub trait Resolver {
154 fn resolve_tag(&self, value: &str, implicit: bool) -> Option<String>;
156
157 fn add_implicit_resolver(&mut self, tag: String, pattern: String);
159
160 fn reset(&mut self);
162}
163
164#[derive(Debug)]
166pub struct BasicResolver {
167 implicit_resolvers: HashMap<String, String>,
168}
169
170impl BasicResolver {
171 pub fn new() -> Self {
173 let mut resolver = Self {
174 implicit_resolvers: HashMap::new(),
175 };
176
177 resolver.add_standard_resolvers();
179 resolver
180 }
181
182 fn add_standard_resolvers(&mut self) {
183 self.implicit_resolvers
185 .insert("true".to_string(), "tag:yaml.org,2002:bool".to_string());
186 self.implicit_resolvers
187 .insert("True".to_string(), "tag:yaml.org,2002:bool".to_string());
188 self.implicit_resolvers
189 .insert("TRUE".to_string(), "tag:yaml.org,2002:bool".to_string());
190 self.implicit_resolvers
191 .insert("false".to_string(), "tag:yaml.org,2002:bool".to_string());
192 self.implicit_resolvers
193 .insert("False".to_string(), "tag:yaml.org,2002:bool".to_string());
194 self.implicit_resolvers
195 .insert("FALSE".to_string(), "tag:yaml.org,2002:bool".to_string());
196
197 self.implicit_resolvers
199 .insert("null".to_string(), "tag:yaml.org,2002:null".to_string());
200 self.implicit_resolvers
201 .insert("Null".to_string(), "tag:yaml.org,2002:null".to_string());
202 self.implicit_resolvers
203 .insert("NULL".to_string(), "tag:yaml.org,2002:null".to_string());
204 self.implicit_resolvers
205 .insert("~".to_string(), "tag:yaml.org,2002:null".to_string());
206 }
207
208 pub fn is_int(&self, value: &str) -> bool {
210 value.parse::<i64>().is_ok()
211 }
212
213 pub fn is_float(&self, value: &str) -> bool {
215 value.parse::<f64>().is_ok()
216 }
217}
218
219impl Default for BasicResolver {
220 fn default() -> Self {
221 Self::new()
222 }
223}
224
225impl Resolver for BasicResolver {
226 fn resolve_tag(&self, value: &str, implicit: bool) -> Option<String> {
227 if !implicit {
228 return None;
229 }
230
231 if let Some(tag) = self.implicit_resolvers.get(value) {
233 return Some(tag.clone());
234 }
235
236 if self.is_int(value) {
238 return Some("tag:yaml.org,2002:int".to_string());
239 }
240
241 if self.is_float(value) {
242 return Some("tag:yaml.org,2002:float".to_string());
243 }
244
245 Some("tag:yaml.org,2002:str".to_string())
247 }
248
249 fn add_implicit_resolver(&mut self, tag: String, pattern: String) {
250 self.implicit_resolvers.insert(pattern, tag);
251 }
252
253 fn reset(&mut self) {
254 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
263 fn plain_scalar_decimal_int() {
264 assert_eq!(
265 resolve_plain_scalar("42", YamlVersion::V1_2),
266 PlainScalarType::Int(42)
267 );
268 assert_eq!(
269 resolve_plain_scalar("-7", YamlVersion::V1_2),
270 PlainScalarType::Int(-7)
271 );
272 }
273
274 #[test]
275 fn plain_scalar_float() {
276 assert_eq!(
277 resolve_plain_scalar("3.14", YamlVersion::V1_2),
278 PlainScalarType::Float(3.14)
279 );
280 }
281
282 #[test]
283 fn plain_scalar_bool_1_2_only_true_false() {
284 assert_eq!(
285 resolve_plain_scalar("true", YamlVersion::V1_2),
286 PlainScalarType::Bool(true)
287 );
288 assert_eq!(
289 resolve_plain_scalar("TRUE", YamlVersion::V1_2),
290 PlainScalarType::Bool(true)
291 );
292 assert_eq!(
293 resolve_plain_scalar("False", YamlVersion::V1_2),
294 PlainScalarType::Bool(false)
295 );
296 }
297
298 #[test]
299 fn plain_scalar_bool_1_2_rejects_yes_no_on_off() {
300 for s in ["yes", "no", "on", "off", "Yes", "NO", "On", "OFF"] {
301 assert_eq!(
302 resolve_plain_scalar(s, YamlVersion::V1_2),
303 PlainScalarType::Str,
304 "{s:?} should fall through to Str under 1.2"
305 );
306 }
307 }
308
309 #[test]
310 fn plain_scalar_bool_1_1_accepts_yes_no_on_off() {
311 assert_eq!(
312 resolve_plain_scalar("yes", YamlVersion::V1_1),
313 PlainScalarType::Bool(true)
314 );
315 assert_eq!(
316 resolve_plain_scalar("no", YamlVersion::V1_1),
317 PlainScalarType::Bool(false)
318 );
319 assert_eq!(
320 resolve_plain_scalar("on", YamlVersion::V1_1),
321 PlainScalarType::Bool(true)
322 );
323 assert_eq!(
324 resolve_plain_scalar("off", YamlVersion::V1_1),
325 PlainScalarType::Bool(false)
326 );
327 }
328
329 #[test]
330 fn plain_scalar_null_any_version() {
331 for v in [YamlVersion::V1_1, YamlVersion::V1_2] {
332 assert_eq!(resolve_plain_scalar("null", v), PlainScalarType::Null);
333 assert_eq!(resolve_plain_scalar("Null", v), PlainScalarType::Null);
334 assert_eq!(resolve_plain_scalar("NULL", v), PlainScalarType::Null);
335 assert_eq!(resolve_plain_scalar("~", v), PlainScalarType::Null);
336 }
337 }
338
339 #[test]
340 fn plain_scalar_string_fallback() {
341 assert_eq!(
342 resolve_plain_scalar("hello", YamlVersion::V1_2),
343 PlainScalarType::Str
344 );
345 assert_eq!(
347 resolve_plain_scalar("=", YamlVersion::V1_2),
348 PlainScalarType::Str
349 );
350 }
351
352 #[test]
358 fn plain_scalar_value_tag_1_1() {
359 assert_eq!(
360 resolve_plain_scalar("=", YamlVersion::V1_1),
361 PlainScalarType::Value
362 );
363 }
364
365 #[test]
369 fn plain_scalar_value_tag_1_1_only_bare_equals() {
370 for s in ["==", "a=b", "= ", " =", " = "] {
371 assert_eq!(
372 resolve_plain_scalar(s, YamlVersion::V1_1),
373 PlainScalarType::Str,
374 "{s:?} should fall through to Str — only bare `=` is the value tag"
375 );
376 }
377 }
378
379 #[test]
380 fn plain_scalar_hex_octal_binary_int() {
381 assert_eq!(
383 resolve_plain_scalar("0xFF", YamlVersion::V1_2),
384 PlainScalarType::Int(255)
385 );
386 assert_eq!(
387 resolve_plain_scalar("0xff", YamlVersion::V1_2),
388 PlainScalarType::Int(255)
389 );
390 assert_eq!(
391 resolve_plain_scalar("0o17", YamlVersion::V1_2),
392 PlainScalarType::Int(15)
393 );
394 assert_eq!(
395 resolve_plain_scalar("0b101", YamlVersion::V1_2),
396 PlainScalarType::Int(5)
397 );
398 }
399
400 #[test]
401 fn plain_scalar_malformed_radix_int_is_string() {
402 for s in ["0x", "0o8", "0b102", "0xZZ"] {
404 assert_eq!(
405 resolve_plain_scalar(s, YamlVersion::V1_2),
406 PlainScalarType::Str,
407 "{s:?} should fall through to Str"
408 );
409 }
410 }
411
412 #[test]
413 fn plain_scalar_dotted_inf_nan_are_floats() {
414 assert_eq!(
416 resolve_plain_scalar(".inf", YamlVersion::V1_2),
417 PlainScalarType::Float(f64::INFINITY)
418 );
419 assert_eq!(
420 resolve_plain_scalar("-.inf", YamlVersion::V1_2),
421 PlainScalarType::Float(f64::NEG_INFINITY)
422 );
423 assert!(matches!(
424 resolve_plain_scalar(".nan", YamlVersion::V1_2),
425 PlainScalarType::Float(f) if f.is_nan()
426 ));
427 }
428
429 #[test]
430 fn plain_scalar_bare_inf_nan_are_strings() {
431 for s in ["inf", "nan", "Inf", "NaN", "infinity"] {
435 assert_eq!(
436 resolve_plain_scalar(s, YamlVersion::V1_2),
437 PlainScalarType::Str,
438 "{s:?} should fall through to Str"
439 );
440 }
441 }
442
443 #[test]
444 fn test_resolver_creation() {
445 let resolver = BasicResolver::new();
446 assert!(!resolver.implicit_resolvers.is_empty());
447 }
448
449 #[test]
450 fn test_boolean_resolution() {
451 let resolver = BasicResolver::new();
452
453 assert_eq!(
454 resolver.resolve_tag("true", true),
455 Some("tag:yaml.org,2002:bool".to_string())
456 );
457 assert_eq!(
458 resolver.resolve_tag("false", true),
459 Some("tag:yaml.org,2002:bool".to_string())
460 );
461 }
462
463 #[test]
464 fn test_null_resolution() {
465 let resolver = BasicResolver::new();
466
467 assert_eq!(
468 resolver.resolve_tag("null", true),
469 Some("tag:yaml.org,2002:null".to_string())
470 );
471 assert_eq!(
472 resolver.resolve_tag("~", true),
473 Some("tag:yaml.org,2002:null".to_string())
474 );
475 }
476
477 #[test]
478 fn test_numeric_resolution() {
479 let resolver = BasicResolver::new();
480
481 assert_eq!(
482 resolver.resolve_tag("42", true),
483 Some("tag:yaml.org,2002:int".to_string())
484 );
485 assert_eq!(
486 resolver.resolve_tag("3.14", true),
487 Some("tag:yaml.org,2002:float".to_string())
488 );
489 }
490
491 #[test]
492 fn test_string_resolution() {
493 let resolver = BasicResolver::new();
494
495 assert_eq!(
496 resolver.resolve_tag("hello", true),
497 Some("tag:yaml.org,2002:str".to_string())
498 );
499 }
500
501 #[test]
502 fn test_explicit_tag_resolution() {
503 let resolver = BasicResolver::new();
504
505 assert_eq!(resolver.resolve_tag("true", false), None);
507 }
508
509 #[test]
510 fn test_custom_resolver() {
511 let mut resolver = BasicResolver::new();
512
513 resolver.add_implicit_resolver(
514 "tag:example.com,2002:custom".to_string(),
515 "CUSTOM".to_string(),
516 );
517
518 assert_eq!(
519 resolver.resolve_tag("CUSTOM", true),
520 Some("tag:example.com,2002:custom".to_string())
521 );
522 }
523}