use crate::version::YamlVersion;
use crate::{Error, Position};
use std::collections::HashMap;
#[must_use]
pub fn value_tag_error(position: Position) -> Error {
Error::construction(
position,
"the YAML 1.1 `=` indicator (tag:yaml.org,2002:value) has no \
constructor in rust-yaml; drop the `%YAML 1.1` directive or \
quote the value (`'='`) to keep it as a string",
)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PlainScalarType {
Null,
Bool(bool),
Int(i64),
Float(f64),
Str,
Value,
}
#[must_use]
pub fn resolve_plain_scalar(value: &str, version: YamlVersion) -> PlainScalarType {
if value.is_empty() {
return PlainScalarType::Null;
}
if let Ok(i) = value.parse::<i64>() {
return PlainScalarType::Int(i);
}
if let Ok(f) = value.parse::<f64>() {
return PlainScalarType::Float(f);
}
if version == YamlVersion::V1_1 && value == "=" {
return PlainScalarType::Value;
}
let lower = value.to_lowercase();
match lower.as_str() {
"true" => PlainScalarType::Bool(true),
"false" => PlainScalarType::Bool(false),
"null" | "~" => PlainScalarType::Null,
"yes" | "on" if version == YamlVersion::V1_1 => PlainScalarType::Bool(true),
"no" | "off" if version == YamlVersion::V1_1 => PlainScalarType::Bool(false),
_ => PlainScalarType::Str,
}
}
pub trait Resolver {
fn resolve_tag(&self, value: &str, implicit: bool) -> Option<String>;
fn add_implicit_resolver(&mut self, tag: String, pattern: String);
fn reset(&mut self);
}
#[derive(Debug)]
pub struct BasicResolver {
implicit_resolvers: HashMap<String, String>,
}
impl BasicResolver {
pub fn new() -> Self {
let mut resolver = Self {
implicit_resolvers: HashMap::new(),
};
resolver.add_standard_resolvers();
resolver
}
fn add_standard_resolvers(&mut self) {
self.implicit_resolvers
.insert("true".to_string(), "tag:yaml.org,2002:bool".to_string());
self.implicit_resolvers
.insert("True".to_string(), "tag:yaml.org,2002:bool".to_string());
self.implicit_resolvers
.insert("TRUE".to_string(), "tag:yaml.org,2002:bool".to_string());
self.implicit_resolvers
.insert("false".to_string(), "tag:yaml.org,2002:bool".to_string());
self.implicit_resolvers
.insert("False".to_string(), "tag:yaml.org,2002:bool".to_string());
self.implicit_resolvers
.insert("FALSE".to_string(), "tag:yaml.org,2002:bool".to_string());
self.implicit_resolvers
.insert("null".to_string(), "tag:yaml.org,2002:null".to_string());
self.implicit_resolvers
.insert("Null".to_string(), "tag:yaml.org,2002:null".to_string());
self.implicit_resolvers
.insert("NULL".to_string(), "tag:yaml.org,2002:null".to_string());
self.implicit_resolvers
.insert("~".to_string(), "tag:yaml.org,2002:null".to_string());
}
pub fn is_int(&self, value: &str) -> bool {
value.parse::<i64>().is_ok()
}
pub fn is_float(&self, value: &str) -> bool {
value.parse::<f64>().is_ok()
}
}
impl Default for BasicResolver {
fn default() -> Self {
Self::new()
}
}
impl Resolver for BasicResolver {
fn resolve_tag(&self, value: &str, implicit: bool) -> Option<String> {
if !implicit {
return None;
}
if let Some(tag) = self.implicit_resolvers.get(value) {
return Some(tag.clone());
}
if self.is_int(value) {
return Some("tag:yaml.org,2002:int".to_string());
}
if self.is_float(value) {
return Some("tag:yaml.org,2002:float".to_string());
}
Some("tag:yaml.org,2002:str".to_string())
}
fn add_implicit_resolver(&mut self, tag: String, pattern: String) {
self.implicit_resolvers.insert(pattern, tag);
}
fn reset(&mut self) {
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plain_scalar_decimal_int() {
assert_eq!(
resolve_plain_scalar("42", YamlVersion::V1_2),
PlainScalarType::Int(42)
);
assert_eq!(
resolve_plain_scalar("-7", YamlVersion::V1_2),
PlainScalarType::Int(-7)
);
}
#[test]
fn plain_scalar_float() {
assert_eq!(
resolve_plain_scalar("3.14", YamlVersion::V1_2),
PlainScalarType::Float(3.14)
);
}
#[test]
fn plain_scalar_bool_1_2_only_true_false() {
assert_eq!(
resolve_plain_scalar("true", YamlVersion::V1_2),
PlainScalarType::Bool(true)
);
assert_eq!(
resolve_plain_scalar("TRUE", YamlVersion::V1_2),
PlainScalarType::Bool(true)
);
assert_eq!(
resolve_plain_scalar("False", YamlVersion::V1_2),
PlainScalarType::Bool(false)
);
}
#[test]
fn plain_scalar_bool_1_2_rejects_yes_no_on_off() {
for s in ["yes", "no", "on", "off", "Yes", "NO", "On", "OFF"] {
assert_eq!(
resolve_plain_scalar(s, YamlVersion::V1_2),
PlainScalarType::Str,
"{s:?} should fall through to Str under 1.2"
);
}
}
#[test]
fn plain_scalar_bool_1_1_accepts_yes_no_on_off() {
assert_eq!(
resolve_plain_scalar("yes", YamlVersion::V1_1),
PlainScalarType::Bool(true)
);
assert_eq!(
resolve_plain_scalar("no", YamlVersion::V1_1),
PlainScalarType::Bool(false)
);
assert_eq!(
resolve_plain_scalar("on", YamlVersion::V1_1),
PlainScalarType::Bool(true)
);
assert_eq!(
resolve_plain_scalar("off", YamlVersion::V1_1),
PlainScalarType::Bool(false)
);
}
#[test]
fn plain_scalar_null_any_version() {
for v in [YamlVersion::V1_1, YamlVersion::V1_2] {
assert_eq!(resolve_plain_scalar("null", v), PlainScalarType::Null);
assert_eq!(resolve_plain_scalar("Null", v), PlainScalarType::Null);
assert_eq!(resolve_plain_scalar("NULL", v), PlainScalarType::Null);
assert_eq!(resolve_plain_scalar("~", v), PlainScalarType::Null);
}
}
#[test]
fn plain_scalar_string_fallback() {
assert_eq!(
resolve_plain_scalar("hello", YamlVersion::V1_2),
PlainScalarType::Str
);
assert_eq!(
resolve_plain_scalar("=", YamlVersion::V1_2),
PlainScalarType::Str
);
}
#[test]
fn plain_scalar_value_tag_1_1() {
assert_eq!(
resolve_plain_scalar("=", YamlVersion::V1_1),
PlainScalarType::Value
);
}
#[test]
fn plain_scalar_value_tag_1_1_only_bare_equals() {
for s in ["==", "a=b", "= ", " =", " = "] {
assert_eq!(
resolve_plain_scalar(s, YamlVersion::V1_1),
PlainScalarType::Str,
"{s:?} should fall through to Str — only bare `=` is the value tag"
);
}
}
#[test]
fn test_resolver_creation() {
let resolver = BasicResolver::new();
assert!(!resolver.implicit_resolvers.is_empty());
}
#[test]
fn test_boolean_resolution() {
let resolver = BasicResolver::new();
assert_eq!(
resolver.resolve_tag("true", true),
Some("tag:yaml.org,2002:bool".to_string())
);
assert_eq!(
resolver.resolve_tag("false", true),
Some("tag:yaml.org,2002:bool".to_string())
);
}
#[test]
fn test_null_resolution() {
let resolver = BasicResolver::new();
assert_eq!(
resolver.resolve_tag("null", true),
Some("tag:yaml.org,2002:null".to_string())
);
assert_eq!(
resolver.resolve_tag("~", true),
Some("tag:yaml.org,2002:null".to_string())
);
}
#[test]
fn test_numeric_resolution() {
let resolver = BasicResolver::new();
assert_eq!(
resolver.resolve_tag("42", true),
Some("tag:yaml.org,2002:int".to_string())
);
assert_eq!(
resolver.resolve_tag("3.14", true),
Some("tag:yaml.org,2002:float".to_string())
);
}
#[test]
fn test_string_resolution() {
let resolver = BasicResolver::new();
assert_eq!(
resolver.resolve_tag("hello", true),
Some("tag:yaml.org,2002:str".to_string())
);
}
#[test]
fn test_explicit_tag_resolution() {
let resolver = BasicResolver::new();
assert_eq!(resolver.resolve_tag("true", false), None);
}
#[test]
fn test_custom_resolver() {
let mut resolver = BasicResolver::new();
resolver.add_implicit_resolver(
"tag:example.com,2002:custom".to_string(),
"CUSTOM".to_string(),
);
assert_eq!(
resolver.resolve_tag("CUSTOM", true),
Some("tag:example.com,2002:custom".to_string())
);
}
}