1use crate::event::ScalarStyle;
15
16#[derive(Debug, Clone, PartialEq)]
26pub enum Scalar {
27 Null,
29 Bool(bool),
31 Int(i64),
33 Float(f64),
35 String(String),
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum ResolvedTag {
42 Str,
44 Int,
46 Float,
48 Bool,
50 Null,
52 Seq,
54 Map,
56 Unknown(String),
58}
59
60pub trait Schema {
71 fn resolve_scalar(&self, value: &str, tag: Option<&str>, style: ScalarStyle) -> Scalar;
80
81 fn resolve_tag(&self, tag: &str) -> ResolvedTag;
83}
84
85pub struct FailsafeSchema;
94
95impl Schema for FailsafeSchema {
96 fn resolve_scalar(&self, value: &str, _tag: Option<&str>, _style: ScalarStyle) -> Scalar {
97 Scalar::String(value.to_owned())
98 }
99
100 fn resolve_tag(&self, tag: &str) -> ResolvedTag {
101 core_resolve_tag(tag)
102 }
103}
104
105pub struct JsonSchema;
114
115impl Schema for JsonSchema {
116 fn resolve_scalar(&self, value: &str, tag: Option<&str>, style: ScalarStyle) -> Scalar {
117 if let Some(resolved) = apply_explicit_tag(tag, value, self) {
119 return resolved;
120 }
121 if !is_plain(style) {
123 return Scalar::String(value.to_owned());
124 }
125 json_infer(value)
126 }
127
128 fn resolve_tag(&self, tag: &str) -> ResolvedTag {
129 core_resolve_tag(tag)
130 }
131}
132
133pub struct CoreSchema;
142
143impl Schema for CoreSchema {
144 fn resolve_scalar(&self, value: &str, tag: Option<&str>, style: ScalarStyle) -> Scalar {
145 if let Some(resolved) = apply_explicit_tag(tag, value, self) {
147 return resolved;
148 }
149 if !is_plain(style) {
151 return Scalar::String(value.to_owned());
152 }
153 core_infer(value)
154 }
155
156 fn resolve_tag(&self, tag: &str) -> ResolvedTag {
157 core_resolve_tag(tag)
158 }
159}
160
161const fn is_plain(style: ScalarStyle) -> bool {
167 matches!(style, ScalarStyle::Plain)
168}
169
170fn apply_explicit_tag(tag: Option<&str>, value: &str, schema: &dyn Schema) -> Option<Scalar> {
174 match tag {
175 None | Some("?") => None,
176 Some("!") => Some(Scalar::String(value.to_owned())),
177 Some(t) => match schema.resolve_tag(t) {
178 ResolvedTag::Str | ResolvedTag::Unknown(_) => Some(Scalar::String(value.to_owned())),
179 ResolvedTag::Null => Some(Scalar::Null),
180 ResolvedTag::Bool => Some(core_infer_bool(value).unwrap_or(Scalar::Bool(false))),
181 ResolvedTag::Int => Some(core_infer_int(value).unwrap_or(Scalar::Int(0))),
182 ResolvedTag::Float => Some(core_infer_float(value).unwrap_or(Scalar::Float(0.0))),
183 ResolvedTag::Seq | ResolvedTag::Map => None,
184 },
185 }
186}
187
188fn core_resolve_tag(tag: &str) -> ResolvedTag {
190 match tag {
191 "tag:yaml.org,2002:str" => ResolvedTag::Str,
192 "tag:yaml.org,2002:int" => ResolvedTag::Int,
193 "tag:yaml.org,2002:float" => ResolvedTag::Float,
194 "tag:yaml.org,2002:bool" => ResolvedTag::Bool,
195 "tag:yaml.org,2002:null" => ResolvedTag::Null,
196 "tag:yaml.org,2002:seq" => ResolvedTag::Seq,
197 "tag:yaml.org,2002:map" => ResolvedTag::Map,
198 other => ResolvedTag::Unknown(other.to_owned()),
199 }
200}
201
202fn json_infer(value: &str) -> Scalar {
204 if value == "null" {
205 return Scalar::Null;
206 }
207 if let Some(b) = json_infer_bool(value) {
208 return b;
209 }
210 if let Some(i) = json_infer_int(value) {
211 return i;
212 }
213 if let Some(f) = json_infer_float(value) {
214 return f;
215 }
216 Scalar::String(value.to_owned())
217}
218
219fn core_infer(value: &str) -> Scalar {
221 if matches!(value, "null" | "Null" | "NULL" | "~" | "") {
222 return Scalar::Null;
223 }
224 if let Some(b) = core_infer_bool(value) {
225 return b;
226 }
227 if let Some(i) = core_infer_int(value) {
228 return i;
229 }
230 if let Some(f) = core_infer_float(value) {
231 return f;
232 }
233 Scalar::String(value.to_owned())
234}
235
236fn json_infer_bool(value: &str) -> Option<Scalar> {
238 match value {
239 "true" => Some(Scalar::Bool(true)),
240 "false" => Some(Scalar::Bool(false)),
241 _ => None,
242 }
243}
244
245fn core_infer_bool(value: &str) -> Option<Scalar> {
247 match value {
248 "true" | "True" | "TRUE" => Some(Scalar::Bool(true)),
249 "false" | "False" | "FALSE" => Some(Scalar::Bool(false)),
250 _ => None,
251 }
252}
253
254fn json_infer_int(value: &str) -> Option<Scalar> {
256 let digits = value.strip_prefix('-').unwrap_or(value);
257 if digits.is_empty() {
258 return None;
259 }
260 if digits.len() > 1 && digits.starts_with('0') {
262 return None;
263 }
264 if !digits.chars().all(|c| c.is_ascii_digit()) {
265 return None;
266 }
267 value.parse::<i64>().ok().map(Scalar::Int)
268}
269
270fn core_infer_int(value: &str) -> Option<Scalar> {
273 let (neg, rest) = value.strip_prefix('-').map_or_else(
274 || (false, value.strip_prefix('+').unwrap_or(value)),
275 |r| (true, r),
276 );
277 if rest.is_empty() {
278 return None;
279 }
280 let magnitude: i64 = if let Some(oct) = rest.strip_prefix("0o") {
281 if oct.is_empty() {
282 return None;
283 }
284 i64::from_str_radix(oct, 8).ok()?
285 } else if let Some(hex) = rest.strip_prefix("0x") {
286 if hex.is_empty() {
287 return None;
288 }
289 i64::from_str_radix(hex, 16).ok()?
290 } else {
291 if rest.len() > 1 && rest.starts_with('0') {
293 return None;
294 }
295 if !rest.chars().all(|c| c.is_ascii_digit()) {
296 return None;
297 }
298 rest.parse::<i64>().ok()?
299 };
300 Some(Scalar::Int(if neg { -magnitude } else { magnitude }))
301}
302
303fn json_infer_float(value: &str) -> Option<Scalar> {
305 match value {
306 ".inf" => return Some(Scalar::Float(f64::INFINITY)),
307 "-.inf" => return Some(Scalar::Float(f64::NEG_INFINITY)),
308 ".nan" => return Some(Scalar::Float(f64::NAN)),
309 _ => {}
310 }
311 let signed = value.strip_prefix('-').unwrap_or(value);
312 if signed.contains('.') || signed.contains('e') || signed.contains('E') {
313 return value.parse::<f64>().ok().map(Scalar::Float);
314 }
315 None
316}
317
318fn core_infer_float(value: &str) -> Option<Scalar> {
320 match value {
321 ".inf" | ".Inf" | ".INF" => return Some(Scalar::Float(f64::INFINITY)),
322 "-.inf" | "-.Inf" | "-.INF" => return Some(Scalar::Float(f64::NEG_INFINITY)),
323 ".nan" | ".NaN" | ".NAN" => return Some(Scalar::Float(f64::NAN)),
324 _ => {}
325 }
326 let stripped = value.strip_prefix('+').unwrap_or(value);
327 let signed = stripped.strip_prefix('-').unwrap_or(stripped);
328 if signed.contains('.') || signed.contains('e') || signed.contains('E') {
329 return value
330 .trim_start_matches('+')
331 .parse::<f64>()
332 .ok()
333 .map(Scalar::Float);
334 }
335 None
336}
337
338#[cfg(test)]
343#[allow(
344 clippy::expect_used,
345 clippy::unwrap_used,
346 clippy::doc_markdown,
347 clippy::float_cmp
348)]
349mod tests {
350 use super::*;
351 use crate::event::{Chomp, ScalarStyle};
352
353 fn core() -> CoreSchema {
358 CoreSchema
359 }
360 fn json() -> JsonSchema {
361 JsonSchema
362 }
363 fn failsafe() -> FailsafeSchema {
364 FailsafeSchema
365 }
366
367 fn assert_float_eq(actual: &Scalar, expected: f64) {
368 match actual {
369 Scalar::Float(f) => {
370 assert!(
371 (f - expected).abs() < 1e-10,
372 "expected Float({expected}), got Float({f})"
373 );
374 }
375 other @ (Scalar::Null | Scalar::Bool(_) | Scalar::Int(_) | Scalar::String(_)) => {
376 panic!("expected Scalar::Float, got {other:?}")
377 }
378 }
379 }
380
381 fn assert_float_inf_pos(actual: &Scalar) {
382 match actual {
383 Scalar::Float(f) => assert!(f.is_infinite() && f.is_sign_positive()),
384 other @ (Scalar::Null | Scalar::Bool(_) | Scalar::Int(_) | Scalar::String(_)) => {
385 panic!("expected positive infinity, got {other:?}")
386 }
387 }
388 }
389
390 fn assert_float_inf_neg(actual: &Scalar) {
391 match actual {
392 Scalar::Float(f) => assert!(f.is_infinite() && f.is_sign_negative()),
393 other @ (Scalar::Null | Scalar::Bool(_) | Scalar::Int(_) | Scalar::String(_)) => {
394 panic!("expected negative infinity, got {other:?}")
395 }
396 }
397 }
398
399 fn assert_float_nan(actual: &Scalar) {
400 match actual {
401 Scalar::Float(f) => assert!(f.is_nan(), "expected NaN, got {f}"),
402 other @ (Scalar::Null | Scalar::Bool(_) | Scalar::Int(_) | Scalar::String(_)) => {
403 panic!("expected NaN float, got {other:?}")
404 }
405 }
406 }
407
408 #[test]
414 fn scalar_null_equals_null() {
415 assert_eq!(Scalar::Null, Scalar::Null);
416 assert_eq!(
418 core().resolve_scalar("null", None, ScalarStyle::Plain),
419 Scalar::Null
420 );
421 }
422
423 #[test]
425 fn scalar_bool_true_equals_bool_true() {
426 assert_eq!(Scalar::Bool(true), Scalar::Bool(true));
427 }
428
429 #[test]
431 fn scalar_bool_true_does_not_equal_false() {
432 assert_ne!(Scalar::Bool(true), Scalar::Bool(false));
433 }
434
435 #[test]
437 fn scalar_int_equality() {
438 assert_eq!(Scalar::Int(42), Scalar::Int(42));
439 }
440
441 #[test]
443 fn scalar_string_equality() {
444 assert_eq!(
445 Scalar::String("hello".to_owned()),
446 Scalar::String("hello".to_owned())
447 );
448 }
449
450 #[test]
456 fn failsafe_plain_null_is_string() {
457 assert_eq!(
458 failsafe().resolve_scalar("null", None, ScalarStyle::Plain),
459 Scalar::String("null".to_owned())
460 );
461 }
462
463 #[test]
465 fn failsafe_plain_true_is_string() {
466 assert_eq!(
467 failsafe().resolve_scalar("true", None, ScalarStyle::Plain),
468 Scalar::String("true".to_owned())
469 );
470 }
471
472 #[test]
474 fn failsafe_plain_integer_is_string() {
475 assert_eq!(
476 failsafe().resolve_scalar("42", None, ScalarStyle::Plain),
477 Scalar::String("42".to_owned())
478 );
479 }
480
481 #[test]
483 fn failsafe_quoted_value_is_string() {
484 assert_eq!(
485 failsafe().resolve_scalar("hello", None, ScalarStyle::SingleQuoted),
486 Scalar::String("hello".to_owned())
487 );
488 }
489
490 #[test]
492 fn failsafe_explicit_tag_ignored_still_string() {
493 assert_eq!(
494 failsafe().resolve_scalar("42", Some("tag:yaml.org,2002:int"), ScalarStyle::Plain),
495 Scalar::String("42".to_owned())
496 );
497 }
498
499 #[test]
505 fn core_plain_null_lowercase_is_null() {
506 assert_eq!(
507 core().resolve_scalar("null", None, ScalarStyle::Plain),
508 Scalar::Null
509 );
510 }
511
512 #[test]
514 fn core_plain_null_titlecase_is_null() {
515 assert_eq!(
516 core().resolve_scalar("Null", None, ScalarStyle::Plain),
517 Scalar::Null
518 );
519 }
520
521 #[test]
523 fn core_plain_null_uppercase_is_null() {
524 assert_eq!(
525 core().resolve_scalar("NULL", None, ScalarStyle::Plain),
526 Scalar::Null
527 );
528 }
529
530 #[test]
532 fn core_plain_tilde_is_null() {
533 assert_eq!(
534 core().resolve_scalar("~", None, ScalarStyle::Plain),
535 Scalar::Null
536 );
537 }
538
539 #[test]
541 fn core_empty_plain_scalar_is_null() {
542 assert_eq!(
543 core().resolve_scalar("", None, ScalarStyle::Plain),
544 Scalar::Null
545 );
546 }
547
548 #[test]
550 fn core_quoted_null_is_string() {
551 assert_eq!(
552 core().resolve_scalar("null", None, ScalarStyle::SingleQuoted),
553 Scalar::String("null".to_owned())
554 );
555 }
556
557 #[test]
559 fn core_quoted_empty_is_string() {
560 assert_eq!(
561 core().resolve_scalar("", None, ScalarStyle::DoubleQuoted),
562 Scalar::String(String::new())
563 );
564 }
565
566 #[test]
572 fn core_plain_true_lowercase_is_bool_true() {
573 assert_eq!(
574 core().resolve_scalar("true", None, ScalarStyle::Plain),
575 Scalar::Bool(true)
576 );
577 }
578
579 #[test]
581 fn core_plain_false_lowercase_is_bool_false() {
582 assert_eq!(
583 core().resolve_scalar("false", None, ScalarStyle::Plain),
584 Scalar::Bool(false)
585 );
586 }
587
588 #[test]
590 fn core_plain_true_titlecase_is_bool_true() {
591 assert_eq!(
592 core().resolve_scalar("True", None, ScalarStyle::Plain),
593 Scalar::Bool(true)
594 );
595 }
596
597 #[test]
599 fn core_plain_false_titlecase_is_bool_false() {
600 assert_eq!(
601 core().resolve_scalar("False", None, ScalarStyle::Plain),
602 Scalar::Bool(false)
603 );
604 }
605
606 #[test]
608 fn core_plain_true_uppercase_is_bool_true() {
609 assert_eq!(
610 core().resolve_scalar("TRUE", None, ScalarStyle::Plain),
611 Scalar::Bool(true)
612 );
613 }
614
615 #[test]
617 fn core_plain_false_uppercase_is_bool_false() {
618 assert_eq!(
619 core().resolve_scalar("FALSE", None, ScalarStyle::Plain),
620 Scalar::Bool(false)
621 );
622 }
623
624 #[test]
626 fn core_quoted_true_is_string() {
627 assert_eq!(
628 core().resolve_scalar("true", None, ScalarStyle::SingleQuoted),
629 Scalar::String("true".to_owned())
630 );
631 }
632
633 #[test]
635 fn core_quoted_false_is_string() {
636 assert_eq!(
637 core().resolve_scalar("false", None, ScalarStyle::DoubleQuoted),
638 Scalar::String("false".to_owned())
639 );
640 }
641
642 #[test]
644 fn core_plain_yes_is_string() {
645 assert_eq!(
646 core().resolve_scalar("yes", None, ScalarStyle::Plain),
647 Scalar::String("yes".to_owned())
648 );
649 }
650
651 #[test]
653 fn core_plain_on_is_string() {
654 assert_eq!(
655 core().resolve_scalar("on", None, ScalarStyle::Plain),
656 Scalar::String("on".to_owned())
657 );
658 }
659
660 #[test]
666 fn core_plain_decimal_zero_is_int() {
667 assert_eq!(
668 core().resolve_scalar("0", None, ScalarStyle::Plain),
669 Scalar::Int(0)
670 );
671 }
672
673 #[test]
675 fn core_plain_positive_decimal_is_int() {
676 assert_eq!(
677 core().resolve_scalar("42", None, ScalarStyle::Plain),
678 Scalar::Int(42)
679 );
680 }
681
682 #[test]
684 fn core_plain_negative_decimal_is_int() {
685 assert_eq!(
686 core().resolve_scalar("-17", None, ScalarStyle::Plain),
687 Scalar::Int(-17)
688 );
689 }
690
691 #[test]
693 fn core_plain_explicit_plus_decimal_is_int() {
694 assert_eq!(
695 core().resolve_scalar("+5", None, ScalarStyle::Plain),
696 Scalar::Int(5)
697 );
698 }
699
700 #[test]
702 fn core_plain_octal_is_int() {
703 assert_eq!(
704 core().resolve_scalar("0o777", None, ScalarStyle::Plain),
705 Scalar::Int(0o777)
706 );
707 }
708
709 #[test]
711 fn core_plain_octal_zero_is_int() {
712 assert_eq!(
713 core().resolve_scalar("0o0", None, ScalarStyle::Plain),
714 Scalar::Int(0)
715 );
716 }
717
718 #[test]
720 fn core_plain_negative_octal_is_int() {
721 assert_eq!(
722 core().resolve_scalar("-0o7", None, ScalarStyle::Plain),
723 Scalar::Int(-7)
724 );
725 }
726
727 #[test]
729 fn core_plain_hex_is_int() {
730 assert_eq!(
731 core().resolve_scalar("0xFF", None, ScalarStyle::Plain),
732 Scalar::Int(255)
733 );
734 }
735
736 #[test]
738 fn core_plain_hex_lowercase_is_int() {
739 assert_eq!(
740 core().resolve_scalar("0xff", None, ScalarStyle::Plain),
741 Scalar::Int(255)
742 );
743 }
744
745 #[test]
747 fn core_plain_negative_hex_is_int() {
748 assert_eq!(
749 core().resolve_scalar("-0x10", None, ScalarStyle::Plain),
750 Scalar::Int(-16)
751 );
752 }
753
754 #[test]
756 fn core_plain_leading_zero_decimal_is_string() {
757 assert_eq!(
758 core().resolve_scalar("007", None, ScalarStyle::Plain),
759 Scalar::String("007".to_owned())
760 );
761 }
762
763 #[test]
769 fn core_plain_decimal_float_is_float() {
770 let result = core().resolve_scalar("1.25", None, ScalarStyle::Plain);
771 assert_float_eq(&result, 1.25_f64);
772 }
773
774 #[test]
776 fn core_plain_negative_float_is_float() {
777 let result = core().resolve_scalar("-1.5", None, ScalarStyle::Plain);
778 assert_float_eq(&result, -1.5_f64);
779 }
780
781 #[test]
783 fn core_plain_positive_float_is_float() {
784 let result = core().resolve_scalar("+0.5", None, ScalarStyle::Plain);
785 assert_float_eq(&result, 0.5_f64);
786 }
787
788 #[test]
790 fn core_plain_float_scientific_lowercase_e_is_float() {
791 let result = core().resolve_scalar("1.0e10", None, ScalarStyle::Plain);
792 assert_float_eq(&result, 1.0e10_f64);
793 }
794
795 #[test]
797 fn core_plain_float_scientific_uppercase_e_is_float() {
798 let result = core().resolve_scalar("2.5E3", None, ScalarStyle::Plain);
799 assert_float_eq(&result, 2500.0_f64);
800 }
801
802 #[test]
804 fn core_plain_dot_inf_lowercase_is_positive_infinity() {
805 let result = core().resolve_scalar(".inf", None, ScalarStyle::Plain);
806 assert_float_inf_pos(&result);
807 }
808
809 #[test]
811 fn core_plain_dot_inf_titlecase_is_positive_infinity() {
812 let result = core().resolve_scalar(".Inf", None, ScalarStyle::Plain);
813 assert_float_inf_pos(&result);
814 }
815
816 #[test]
818 fn core_plain_dot_inf_uppercase_is_positive_infinity() {
819 let result = core().resolve_scalar(".INF", None, ScalarStyle::Plain);
820 assert_float_inf_pos(&result);
821 }
822
823 #[test]
825 fn core_plain_negative_dot_inf_lowercase_is_negative_infinity() {
826 let result = core().resolve_scalar("-.inf", None, ScalarStyle::Plain);
827 assert_float_inf_neg(&result);
828 }
829
830 #[test]
832 fn core_plain_negative_dot_inf_titlecase_is_negative_infinity() {
833 let result = core().resolve_scalar("-.Inf", None, ScalarStyle::Plain);
834 assert_float_inf_neg(&result);
835 }
836
837 #[test]
839 fn core_plain_negative_dot_inf_uppercase_is_negative_infinity() {
840 let result = core().resolve_scalar("-.INF", None, ScalarStyle::Plain);
841 assert_float_inf_neg(&result);
842 }
843
844 #[test]
846 fn core_plain_dot_nan_lowercase_is_nan() {
847 let result = core().resolve_scalar(".nan", None, ScalarStyle::Plain);
848 assert_float_nan(&result);
849 }
850
851 #[test]
853 fn core_plain_dot_nan_titlecase_is_nan() {
854 let result = core().resolve_scalar(".NaN", None, ScalarStyle::Plain);
855 assert_float_nan(&result);
856 }
857
858 #[test]
860 fn core_plain_dot_nan_uppercase_is_nan() {
861 let result = core().resolve_scalar(".NAN", None, ScalarStyle::Plain);
862 assert_float_nan(&result);
863 }
864
865 #[test]
871 fn core_plain_arbitrary_word_is_string() {
872 assert_eq!(
873 core().resolve_scalar("hello", None, ScalarStyle::Plain),
874 Scalar::String("hello".to_owned())
875 );
876 }
877
878 #[test]
880 fn core_plain_integer_looking_with_letters_is_string() {
881 assert_eq!(
882 core().resolve_scalar("42abc", None, ScalarStyle::Plain),
883 Scalar::String("42abc".to_owned())
884 );
885 }
886
887 #[test]
889 fn core_plain_partial_octal_prefix_is_string() {
890 assert_eq!(
891 core().resolve_scalar("0o", None, ScalarStyle::Plain),
892 Scalar::String("0o".to_owned())
893 );
894 }
895
896 #[test]
898 fn core_plain_partial_hex_prefix_is_string() {
899 assert_eq!(
900 core().resolve_scalar("0x", None, ScalarStyle::Plain),
901 Scalar::String("0x".to_owned())
902 );
903 }
904
905 #[test]
907 fn core_literal_block_scalar_is_string() {
908 assert_eq!(
909 core().resolve_scalar("hello\n", None, ScalarStyle::Literal(Chomp::Clip)),
910 Scalar::String("hello\n".to_owned())
911 );
912 }
913
914 #[test]
920 fn core_str_tag_on_integer_looking_plain_is_string() {
921 assert_eq!(
922 core().resolve_scalar("123", Some("tag:yaml.org,2002:str"), ScalarStyle::Plain),
923 Scalar::String("123".to_owned())
924 );
925 }
926
927 #[test]
929 fn core_str_tag_on_null_looking_plain_is_string() {
930 assert_eq!(
931 core().resolve_scalar("null", Some("tag:yaml.org,2002:str"), ScalarStyle::Plain),
932 Scalar::String("null".to_owned())
933 );
934 }
935
936 #[test]
938 fn core_str_tag_on_bool_looking_plain_is_string() {
939 assert_eq!(
940 core().resolve_scalar("true", Some("tag:yaml.org,2002:str"), ScalarStyle::Plain),
941 Scalar::String("true".to_owned())
942 );
943 }
944
945 #[test]
947 fn core_int_tag_on_decimal_is_int() {
948 assert_eq!(
949 core().resolve_scalar("42", Some("tag:yaml.org,2002:int"), ScalarStyle::Plain),
950 Scalar::Int(42)
951 );
952 }
953
954 #[test]
956 fn core_float_tag_on_decimal_is_float() {
957 let result =
958 core().resolve_scalar("1.25", Some("tag:yaml.org,2002:float"), ScalarStyle::Plain);
959 assert_float_eq(&result, 1.25_f64);
960 }
961
962 #[test]
964 fn core_null_tag_on_string_value_is_null() {
965 assert_eq!(
966 core().resolve_scalar(
967 "anything",
968 Some("tag:yaml.org,2002:null"),
969 ScalarStyle::Plain
970 ),
971 Scalar::Null
972 );
973 }
974
975 #[test]
977 fn core_bool_tag_on_true_is_bool() {
978 assert_eq!(
979 core().resolve_scalar("true", Some("tag:yaml.org,2002:bool"), ScalarStyle::Plain),
980 Scalar::Bool(true)
981 );
982 }
983
984 #[test]
986 fn core_bool_tag_on_false_is_bool() {
987 assert_eq!(
988 core().resolve_scalar("false", Some("tag:yaml.org,2002:bool"), ScalarStyle::Plain),
989 Scalar::Bool(false)
990 );
991 }
992
993 #[test]
998 fn core_bang_str_shorthand_tag_is_string() {
999 assert_eq!(
1000 core().resolve_scalar("123", Some("tag:yaml.org,2002:str"), ScalarStyle::Plain),
1001 Scalar::String("123".to_owned())
1002 );
1003 }
1004
1005 #[test]
1007 fn core_verbatim_str_tag_is_string() {
1008 assert_eq!(
1009 core().resolve_scalar("42", Some("tag:yaml.org,2002:str"), ScalarStyle::Plain),
1010 Scalar::String("42".to_owned())
1011 );
1012 }
1013
1014 #[test]
1016 fn core_unknown_tag_falls_back_to_string() {
1017 assert_eq!(
1018 core().resolve_scalar("hello", Some("tag:example.com:custom"), ScalarStyle::Plain),
1019 Scalar::String("hello".to_owned())
1020 );
1021 }
1022
1023 #[test]
1029 fn core_bang_tag_forces_string() {
1030 assert_eq!(
1031 core().resolve_scalar("null", Some("!"), ScalarStyle::Plain),
1032 Scalar::String("null".to_owned())
1033 );
1034 }
1035
1036 #[test]
1038 fn core_question_tag_uses_schema_inference() {
1039 assert_eq!(
1040 core().resolve_scalar("null", Some("?"), ScalarStyle::Plain),
1041 Scalar::Null
1042 );
1043 }
1044
1045 #[test]
1047 fn core_question_tag_on_plain_int_uses_inference() {
1048 assert_eq!(
1049 core().resolve_scalar("42", Some("?"), ScalarStyle::Plain),
1050 Scalar::Int(42)
1051 );
1052 }
1053
1054 #[test]
1056 fn core_question_tag_on_quoted_int_is_string() {
1057 assert_eq!(
1058 core().resolve_scalar("42", Some("?"), ScalarStyle::SingleQuoted),
1059 Scalar::String("42".to_owned())
1060 );
1061 }
1062
1063 #[test]
1069 fn json_plain_null_only_lowercase_is_null() {
1070 assert_eq!(
1071 json().resolve_scalar("null", None, ScalarStyle::Plain),
1072 Scalar::Null
1073 );
1074 }
1075
1076 #[test]
1078 fn json_plain_null_titlecase_is_string() {
1079 assert_eq!(
1080 json().resolve_scalar("Null", None, ScalarStyle::Plain),
1081 Scalar::String("Null".to_owned())
1082 );
1083 }
1084
1085 #[test]
1087 fn json_plain_tilde_is_string() {
1088 assert_eq!(
1089 json().resolve_scalar("~", None, ScalarStyle::Plain),
1090 Scalar::String("~".to_owned())
1091 );
1092 }
1093
1094 #[test]
1096 fn json_plain_empty_is_string() {
1097 assert_eq!(
1098 json().resolve_scalar("", None, ScalarStyle::Plain),
1099 Scalar::String(String::new())
1100 );
1101 }
1102
1103 #[test]
1105 fn json_plain_true_lowercase_is_bool_true() {
1106 assert_eq!(
1107 json().resolve_scalar("true", None, ScalarStyle::Plain),
1108 Scalar::Bool(true)
1109 );
1110 }
1111
1112 #[test]
1114 fn json_plain_false_lowercase_is_bool_false() {
1115 assert_eq!(
1116 json().resolve_scalar("false", None, ScalarStyle::Plain),
1117 Scalar::Bool(false)
1118 );
1119 }
1120
1121 #[test]
1123 fn json_plain_true_titlecase_is_string() {
1124 assert_eq!(
1125 json().resolve_scalar("True", None, ScalarStyle::Plain),
1126 Scalar::String("True".to_owned())
1127 );
1128 }
1129
1130 #[test]
1132 fn json_plain_decimal_int_is_int() {
1133 assert_eq!(
1134 json().resolve_scalar("42", None, ScalarStyle::Plain),
1135 Scalar::Int(42)
1136 );
1137 }
1138
1139 #[test]
1141 fn json_plain_negative_decimal_int_is_int() {
1142 assert_eq!(
1143 json().resolve_scalar("-5", None, ScalarStyle::Plain),
1144 Scalar::Int(-5)
1145 );
1146 }
1147
1148 #[test]
1150 fn json_plain_octal_is_string() {
1151 assert_eq!(
1152 json().resolve_scalar("0o77", None, ScalarStyle::Plain),
1153 Scalar::String("0o77".to_owned())
1154 );
1155 }
1156
1157 #[test]
1159 fn json_plain_hex_is_string() {
1160 assert_eq!(
1161 json().resolve_scalar("0xFF", None, ScalarStyle::Plain),
1162 Scalar::String("0xFF".to_owned())
1163 );
1164 }
1165
1166 #[test]
1168 fn json_plain_decimal_float_is_float() {
1169 let result = json().resolve_scalar("1.5", None, ScalarStyle::Plain);
1170 assert_float_eq(&result, 1.5_f64);
1171 }
1172
1173 #[test]
1175 fn json_plain_dot_inf_is_float_infinity() {
1176 let result = json().resolve_scalar(".inf", None, ScalarStyle::Plain);
1177 assert_float_inf_pos(&result);
1178 }
1179
1180 #[test]
1182 fn json_plain_dot_nan_is_float_nan() {
1183 let result = json().resolve_scalar(".nan", None, ScalarStyle::Plain);
1184 assert_float_nan(&result);
1185 }
1186
1187 #[test]
1189 fn json_quoted_null_is_string() {
1190 assert_eq!(
1191 json().resolve_scalar("null", None, ScalarStyle::DoubleQuoted),
1192 Scalar::String("null".to_owned())
1193 );
1194 }
1195
1196 #[test]
1202 fn custom_schema_via_trait_object_is_callable() {
1203 struct AlwaysNull;
1204 impl Schema for AlwaysNull {
1205 fn resolve_scalar(
1206 &self,
1207 _value: &str,
1208 _tag: Option<&str>,
1209 _style: ScalarStyle,
1210 ) -> Scalar {
1211 Scalar::Null
1212 }
1213 fn resolve_tag(&self, tag: &str) -> ResolvedTag {
1214 ResolvedTag::Unknown(tag.to_owned())
1215 }
1216 }
1217 let schema: &dyn Schema = &AlwaysNull;
1218 assert_eq!(
1219 schema.resolve_scalar("hello", None, ScalarStyle::Plain),
1220 Scalar::Null
1221 );
1222 }
1223
1224 #[test]
1226 fn custom_schema_overrides_core_behavior() {
1227 struct StringOnly;
1228 impl Schema for StringOnly {
1229 fn resolve_scalar(
1230 &self,
1231 value: &str,
1232 _tag: Option<&str>,
1233 _style: ScalarStyle,
1234 ) -> Scalar {
1235 Scalar::String(value.to_owned())
1236 }
1237 fn resolve_tag(&self, tag: &str) -> ResolvedTag {
1238 ResolvedTag::Unknown(tag.to_owned())
1239 }
1240 }
1241 assert_eq!(
1242 StringOnly.resolve_scalar("42", None, ScalarStyle::Plain),
1243 Scalar::String("42".to_owned())
1244 );
1245 }
1246
1247 #[test]
1249 fn resolve_tag_on_core_schema_returns_known_tags() {
1250 assert_eq!(
1251 core().resolve_tag("tag:yaml.org,2002:str"),
1252 ResolvedTag::Str
1253 );
1254 }
1255
1256 #[test]
1258 fn resolve_tag_on_core_schema_returns_unknown_for_foreign_tag() {
1259 assert!(matches!(
1260 core().resolve_tag("tag:example.com:custom"),
1261 ResolvedTag::Unknown(_)
1262 ));
1263 }
1264
1265 #[test]
1271 fn schema_module_is_accessible_from_crate() {
1272 let result = crate::schema::CoreSchema.resolve_scalar("null", None, ScalarStyle::Plain);
1273 assert_eq!(result, Scalar::Null);
1274 }
1275
1276 #[test]
1278 fn schema_scalar_is_accessible_from_crate() {
1279 let s = crate::schema::Scalar::Null;
1280 assert_ne!(s, crate::schema::Scalar::Bool(false));
1281 }
1282
1283 #[test]
1285 fn core_schema_is_default_schema() {
1286 assert_eq!(
1288 CoreSchema.resolve_scalar("42", None, ScalarStyle::Plain),
1289 Scalar::Int(42)
1290 );
1291 }
1292
1293 #[test]
1295 fn failsafe_schema_is_accessible_from_crate() {
1296 assert_eq!(
1297 crate::schema::FailsafeSchema.resolve_scalar("null", None, ScalarStyle::Plain),
1298 Scalar::String("null".to_owned())
1299 );
1300 }
1301
1302 #[test]
1304 fn json_schema_is_accessible_from_crate() {
1305 assert_eq!(
1306 crate::schema::JsonSchema.resolve_scalar("true", None, ScalarStyle::Plain),
1307 Scalar::Bool(true)
1308 );
1309 }
1310}