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>() {
68 return PlainScalarType::Int(i);
69 }
70
71 if let Ok(f) = value.parse::<f64>() {
72 return PlainScalarType::Float(f);
73 }
74
75 if version == YamlVersion::V1_1 && value == "=" {
79 return PlainScalarType::Value;
80 }
81
82 let lower = value.to_lowercase();
83 match lower.as_str() {
84 "true" => PlainScalarType::Bool(true),
85 "false" => PlainScalarType::Bool(false),
86 "null" | "~" => PlainScalarType::Null,
87 "yes" | "on" if version == YamlVersion::V1_1 => PlainScalarType::Bool(true),
88 "no" | "off" if version == YamlVersion::V1_1 => PlainScalarType::Bool(false),
89 _ => PlainScalarType::Str,
90 }
91}
92
93pub trait Resolver {
95 fn resolve_tag(&self, value: &str, implicit: bool) -> Option<String>;
97
98 fn add_implicit_resolver(&mut self, tag: String, pattern: String);
100
101 fn reset(&mut self);
103}
104
105#[derive(Debug)]
107pub struct BasicResolver {
108 implicit_resolvers: HashMap<String, String>,
109}
110
111impl BasicResolver {
112 pub fn new() -> Self {
114 let mut resolver = Self {
115 implicit_resolvers: HashMap::new(),
116 };
117
118 resolver.add_standard_resolvers();
120 resolver
121 }
122
123 fn add_standard_resolvers(&mut self) {
124 self.implicit_resolvers
126 .insert("true".to_string(), "tag:yaml.org,2002:bool".to_string());
127 self.implicit_resolvers
128 .insert("True".to_string(), "tag:yaml.org,2002:bool".to_string());
129 self.implicit_resolvers
130 .insert("TRUE".to_string(), "tag:yaml.org,2002:bool".to_string());
131 self.implicit_resolvers
132 .insert("false".to_string(), "tag:yaml.org,2002:bool".to_string());
133 self.implicit_resolvers
134 .insert("False".to_string(), "tag:yaml.org,2002:bool".to_string());
135 self.implicit_resolvers
136 .insert("FALSE".to_string(), "tag:yaml.org,2002:bool".to_string());
137
138 self.implicit_resolvers
140 .insert("null".to_string(), "tag:yaml.org,2002:null".to_string());
141 self.implicit_resolvers
142 .insert("Null".to_string(), "tag:yaml.org,2002:null".to_string());
143 self.implicit_resolvers
144 .insert("NULL".to_string(), "tag:yaml.org,2002:null".to_string());
145 self.implicit_resolvers
146 .insert("~".to_string(), "tag:yaml.org,2002:null".to_string());
147 }
148
149 pub fn is_int(&self, value: &str) -> bool {
151 value.parse::<i64>().is_ok()
152 }
153
154 pub fn is_float(&self, value: &str) -> bool {
156 value.parse::<f64>().is_ok()
157 }
158}
159
160impl Default for BasicResolver {
161 fn default() -> Self {
162 Self::new()
163 }
164}
165
166impl Resolver for BasicResolver {
167 fn resolve_tag(&self, value: &str, implicit: bool) -> Option<String> {
168 if !implicit {
169 return None;
170 }
171
172 if let Some(tag) = self.implicit_resolvers.get(value) {
174 return Some(tag.clone());
175 }
176
177 if self.is_int(value) {
179 return Some("tag:yaml.org,2002:int".to_string());
180 }
181
182 if self.is_float(value) {
183 return Some("tag:yaml.org,2002:float".to_string());
184 }
185
186 Some("tag:yaml.org,2002:str".to_string())
188 }
189
190 fn add_implicit_resolver(&mut self, tag: String, pattern: String) {
191 self.implicit_resolvers.insert(pattern, tag);
192 }
193
194 fn reset(&mut self) {
195 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn plain_scalar_decimal_int() {
205 assert_eq!(
206 resolve_plain_scalar("42", YamlVersion::V1_2),
207 PlainScalarType::Int(42)
208 );
209 assert_eq!(
210 resolve_plain_scalar("-7", YamlVersion::V1_2),
211 PlainScalarType::Int(-7)
212 );
213 }
214
215 #[test]
216 fn plain_scalar_float() {
217 assert_eq!(
218 resolve_plain_scalar("3.14", YamlVersion::V1_2),
219 PlainScalarType::Float(3.14)
220 );
221 }
222
223 #[test]
224 fn plain_scalar_bool_1_2_only_true_false() {
225 assert_eq!(
226 resolve_plain_scalar("true", YamlVersion::V1_2),
227 PlainScalarType::Bool(true)
228 );
229 assert_eq!(
230 resolve_plain_scalar("TRUE", YamlVersion::V1_2),
231 PlainScalarType::Bool(true)
232 );
233 assert_eq!(
234 resolve_plain_scalar("False", YamlVersion::V1_2),
235 PlainScalarType::Bool(false)
236 );
237 }
238
239 #[test]
240 fn plain_scalar_bool_1_2_rejects_yes_no_on_off() {
241 for s in ["yes", "no", "on", "off", "Yes", "NO", "On", "OFF"] {
242 assert_eq!(
243 resolve_plain_scalar(s, YamlVersion::V1_2),
244 PlainScalarType::Str,
245 "{s:?} should fall through to Str under 1.2"
246 );
247 }
248 }
249
250 #[test]
251 fn plain_scalar_bool_1_1_accepts_yes_no_on_off() {
252 assert_eq!(
253 resolve_plain_scalar("yes", YamlVersion::V1_1),
254 PlainScalarType::Bool(true)
255 );
256 assert_eq!(
257 resolve_plain_scalar("no", YamlVersion::V1_1),
258 PlainScalarType::Bool(false)
259 );
260 assert_eq!(
261 resolve_plain_scalar("on", YamlVersion::V1_1),
262 PlainScalarType::Bool(true)
263 );
264 assert_eq!(
265 resolve_plain_scalar("off", YamlVersion::V1_1),
266 PlainScalarType::Bool(false)
267 );
268 }
269
270 #[test]
271 fn plain_scalar_null_any_version() {
272 for v in [YamlVersion::V1_1, YamlVersion::V1_2] {
273 assert_eq!(resolve_plain_scalar("null", v), PlainScalarType::Null);
274 assert_eq!(resolve_plain_scalar("Null", v), PlainScalarType::Null);
275 assert_eq!(resolve_plain_scalar("NULL", v), PlainScalarType::Null);
276 assert_eq!(resolve_plain_scalar("~", v), PlainScalarType::Null);
277 }
278 }
279
280 #[test]
281 fn plain_scalar_string_fallback() {
282 assert_eq!(
283 resolve_plain_scalar("hello", YamlVersion::V1_2),
284 PlainScalarType::Str
285 );
286 assert_eq!(
288 resolve_plain_scalar("=", YamlVersion::V1_2),
289 PlainScalarType::Str
290 );
291 }
292
293 #[test]
299 fn plain_scalar_value_tag_1_1() {
300 assert_eq!(
301 resolve_plain_scalar("=", YamlVersion::V1_1),
302 PlainScalarType::Value
303 );
304 }
305
306 #[test]
310 fn plain_scalar_value_tag_1_1_only_bare_equals() {
311 for s in ["==", "a=b", "= ", " =", " = "] {
312 assert_eq!(
313 resolve_plain_scalar(s, YamlVersion::V1_1),
314 PlainScalarType::Str,
315 "{s:?} should fall through to Str — only bare `=` is the value tag"
316 );
317 }
318 }
319
320 #[test]
321 fn test_resolver_creation() {
322 let resolver = BasicResolver::new();
323 assert!(!resolver.implicit_resolvers.is_empty());
324 }
325
326 #[test]
327 fn test_boolean_resolution() {
328 let resolver = BasicResolver::new();
329
330 assert_eq!(
331 resolver.resolve_tag("true", true),
332 Some("tag:yaml.org,2002:bool".to_string())
333 );
334 assert_eq!(
335 resolver.resolve_tag("false", true),
336 Some("tag:yaml.org,2002:bool".to_string())
337 );
338 }
339
340 #[test]
341 fn test_null_resolution() {
342 let resolver = BasicResolver::new();
343
344 assert_eq!(
345 resolver.resolve_tag("null", true),
346 Some("tag:yaml.org,2002:null".to_string())
347 );
348 assert_eq!(
349 resolver.resolve_tag("~", true),
350 Some("tag:yaml.org,2002:null".to_string())
351 );
352 }
353
354 #[test]
355 fn test_numeric_resolution() {
356 let resolver = BasicResolver::new();
357
358 assert_eq!(
359 resolver.resolve_tag("42", true),
360 Some("tag:yaml.org,2002:int".to_string())
361 );
362 assert_eq!(
363 resolver.resolve_tag("3.14", true),
364 Some("tag:yaml.org,2002:float".to_string())
365 );
366 }
367
368 #[test]
369 fn test_string_resolution() {
370 let resolver = BasicResolver::new();
371
372 assert_eq!(
373 resolver.resolve_tag("hello", true),
374 Some("tag:yaml.org,2002:str".to_string())
375 );
376 }
377
378 #[test]
379 fn test_explicit_tag_resolution() {
380 let resolver = BasicResolver::new();
381
382 assert_eq!(resolver.resolve_tag("true", false), None);
384 }
385
386 #[test]
387 fn test_custom_resolver() {
388 let mut resolver = BasicResolver::new();
389
390 resolver.add_implicit_resolver(
391 "tag:example.com,2002:custom".to_string(),
392 "CUSTOM".to_string(),
393 );
394
395 assert_eq!(
396 resolver.resolve_tag("CUSTOM", true),
397 Some("tag:example.com,2002:custom".to_string())
398 );
399 }
400}