1use serde::de::DeserializeOwned;
4
5pub fn from_str<T>(input: &str) -> Result<T, serde_saphyr::Error>
27where
28 T: DeserializeOwned,
29{
30 let mut budget = serde_saphyr::Budget::default();
31 budget.max_nodes = usize::MAX;
32 budget.max_events = usize::MAX;
33
34 let options = serde_saphyr::options! {
35 budget: Some(budget),
36 emit_comments: false,
37 legacy_octal_numbers: true,
38 reject_non_finite_typeless_float: false,
39 with_snippet: false,
40 };
41 serde_saphyr::from_str_with_options(input, options)
42}
43
44#[cfg(test)]
45mod tests {
46 use serde_json::{json, Value};
47
48 use super::*;
49
50 #[test]
51 fn resolves_plain_scalars_with_yaml_1_1_rules() {
52 let value: Value = from_str(
53 "yes: yes\noff: Off\nshort: n\noctal: 012\nhex: 0x1F\nnot_octal: 08\nquoted: '012'\nkeyword: \"on\"\n",
54 )
55 .unwrap();
56
57 assert_eq!(
58 value,
59 json!({
60 "yes": true,
61 "off": false,
62 "short": false,
63 "octal": 10,
64 "hex": 31,
65 "not_octal": 8.0,
66 "quoted": "012",
67 "keyword": "on",
68 })
69 );
70 }
71
72 #[test]
73 fn expands_merge_keys() {
74 let value: Value = from_str("base: &base {a: 1, b: 2}\nderived:\n <<: *base\n b: 3\n").unwrap();
75
76 assert_eq!(value["derived"], json!({"a": 1, "b": 3}));
77 }
78
79 #[test]
80 fn keeps_non_finite_floats_as_strings() {
81 let value: Value = from_str("a: .inf\nb: -.inf\nc: .nan\n").unwrap();
82
83 assert_eq!(value, json!({"a": ".inf", "b": "-.inf", "c": ".nan"}));
84 }
85
86 #[test]
87 fn parses_documents_with_very_large_lists() {
88 let mut input = String::from("items:\n");
89 for i in 0..300_000 {
90 input.push_str(&format!(" - item_{i}\n"));
91 }
92
93 let value: Value = from_str(&input).unwrap();
94
95 assert_eq!(value["items"].as_array().unwrap().len(), 300_000);
96 }
97
98 #[test]
99 fn rejects_duplicate_keys() {
100 assert!(from_str::<Value>("a: 1\na: 2\n").is_err());
101 }
102
103 #[test]
104 fn errors_do_not_include_source_lines() {
105 let input = "api_key: 0123456789abcdef0123456789abcdef\nlog_level: [unclosed\n";
106
107 let error = from_str::<Value>(input).unwrap_err().to_string();
108
109 assert!(!error.contains("0123456789abcdef"), "error leaks source: {error}");
110 assert!(error.contains("line 2"), "error lacks location: {error}");
111 }
112
113 #[test]
114 fn parses_long_runs_of_comments() {
115 let mut input = String::from("proxy: # optional\n");
116 for i in 0..100 {
117 input.push_str(&format!("# documentation line {i}\n"));
118 }
119 input.push_str("log_level: info\n");
120
121 let value: Value = from_str(&input).unwrap();
122
123 assert_eq!(value, json!({"proxy": null, "log_level": "info"}));
124 }
125}