use crate::parser::yaml::YamlConsumer;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum YamlVersion {
V1_1,
V1_2Core,
}
pub fn version_of(consumer: YamlConsumer) -> YamlVersion {
match consumer {
YamlConsumer::Libyaml | YamlConsumer::RYaml => YamlVersion::V1_1,
YamlConsumer::Jsyaml => YamlVersion::V1_2Core,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Resolved {
Bool(bool),
Int(i64),
Str,
}
pub fn resolve_plain(text: &str, version: YamlVersion) -> Resolved {
let t = text.trim();
if t.is_empty() {
return Resolved::Str;
}
if let Some(b) = resolve_bool(t, version) {
return Resolved::Bool(b);
}
if let Some(i) = resolve_int(t, version) {
return Resolved::Int(i);
}
Resolved::Str
}
fn resolve_bool(t: &str, version: YamlVersion) -> Option<bool> {
match version {
YamlVersion::V1_1 => match t {
"y" | "Y" | "yes" | "Yes" | "YES" | "true" | "True" | "TRUE" | "on" | "On" | "ON" => {
Some(true)
}
"n" | "N" | "no" | "No" | "NO" | "false" | "False" | "FALSE" | "off" | "Off"
| "OFF" => Some(false),
_ => None,
},
YamlVersion::V1_2Core => match t {
"true" | "True" | "TRUE" => Some(true),
"false" | "False" | "FALSE" => Some(false),
_ => None,
},
}
}
fn resolve_int(t: &str, version: YamlVersion) -> Option<i64> {
let (sign, rest) = match t.strip_prefix('-') {
Some(r) => (-1i64, r),
None => (1i64, t.strip_prefix('+').unwrap_or(t)),
};
if rest.is_empty() {
return None;
}
if let Some(hex) = rest.strip_prefix("0x").or_else(|| rest.strip_prefix("0X")) {
if hex.is_empty() || !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
return None;
}
return i64::from_str_radix(hex, 16).ok().map(|v| sign * v);
}
match version {
YamlVersion::V1_1 => {
if rest.len() >= 2 && rest.starts_with('0') {
if rest.bytes().all(|b| b.is_ascii_digit() && b <= b'7') {
return i64::from_str_radix(rest, 8).ok().map(|v| sign * v);
}
return None;
}
if rest.bytes().all(|b| b.is_ascii_digit()) {
return rest.parse::<i64>().ok().map(|v| sign * v);
}
None
}
YamlVersion::V1_2Core => {
if let Some(oct) = rest.strip_prefix("0o") {
if oct.is_empty() || !oct.bytes().all(|b| b.is_ascii_digit() && b <= b'7') {
return None;
}
return i64::from_str_radix(oct, 8).ok().map(|v| sign * v);
}
if rest == "0" {
return Some(0);
}
if rest.starts_with('0') || !rest.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
rest.parse::<i64>().ok().map(|v| sign * v)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn r11(t: &str) -> Resolved {
resolve_plain(t, YamlVersion::V1_1)
}
fn r12(t: &str) -> Resolved {
resolve_plain(t, YamlVersion::V1_2Core)
}
#[test]
fn version_mapping() {
assert_eq!(version_of(YamlConsumer::Libyaml), YamlVersion::V1_1);
assert_eq!(version_of(YamlConsumer::RYaml), YamlVersion::V1_1);
assert_eq!(version_of(YamlConsumer::Jsyaml), YamlVersion::V1_2Core);
}
#[test]
fn norway_booleans_diverge() {
for s in ["no", "No", "NO", "yes", "on", "off", "y", "n", "Y", "N"] {
assert!(matches!(r11(s), Resolved::Bool(_)), "1.1 {s}");
assert_eq!(r12(s), Resolved::Str, "1.2 {s}");
}
assert_eq!(r11("no"), Resolved::Bool(false));
assert_eq!(r11("yes"), Resolved::Bool(true));
}
#[test]
fn canonical_booleans_agree() {
for s in ["true", "True", "TRUE", "false", "False", "FALSE"] {
assert_eq!(r11(s), r12(s), "{s}");
assert!(matches!(r11(s), Resolved::Bool(_)));
}
}
#[test]
fn leading_zero_octal_diverges() {
assert_eq!(r11("0755"), Resolved::Int(0o755));
assert_eq!(r12("0755"), Resolved::Str);
assert_eq!(r11("010"), Resolved::Int(8));
assert_eq!(r12("010"), Resolved::Str);
}
#[test]
fn leading_zero_non_octal_agrees_as_string() {
assert_eq!(r11("0789"), Resolved::Str);
assert_eq!(r12("0789"), Resolved::Str);
}
#[test]
fn plain_decimals_and_hex_agree() {
for s in ["0", "42", "-7", "+3", "0x1f", "-0x10"] {
assert_eq!(r11(s), r12(s), "{s}");
assert!(matches!(r11(s), Resolved::Int(_)), "{s}");
}
assert_eq!(r11("0x1f"), Resolved::Int(31));
}
#[test]
fn floats_and_words_are_strings_both() {
for s in [
"3.14",
".inf",
".nan",
"-.inf",
"1e3",
"hello",
"2024-01-01",
"",
] {
assert_eq!(r11(s), Resolved::Str, "1.1 {s}");
assert_eq!(r12(s), Resolved::Str, "1.2 {s}");
}
}
}