use crate::node::ScalarStyle;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ValueType {
Unknown,
Sequence,
Mapping,
Scalar,
String,
Bool,
Null,
Int8,
Int16,
Int32,
Int64,
Uint8,
Uint16,
Uint32,
Uint64,
Float,
Double,
Extension,
}
impl ValueType {
pub fn as_str(self) -> &'static str {
match self {
ValueType::Unknown => "<unknown>",
ValueType::Sequence => "sequence",
ValueType::Mapping => "mapping",
ValueType::Scalar => "scalar",
ValueType::String => "string",
ValueType::Bool => "bool",
ValueType::Null => "null",
ValueType::Int8 => "int8",
ValueType::Int16 => "int16",
ValueType::Int32 => "int32",
ValueType::Int64 => "int64",
ValueType::Uint8 => "uint8",
ValueType::Uint16 => "uint16",
ValueType::Uint32 => "uint32",
ValueType::Uint64 => "uint64",
ValueType::Float => "float",
ValueType::Double => "double",
ValueType::Extension => "<extension>",
}
}
pub fn is_int(self) -> bool {
matches!(
self,
ValueType::Int8
| ValueType::Int16
| ValueType::Int32
| ValueType::Int64
| ValueType::Uint8
| ValueType::Uint16
| ValueType::Uint32
| ValueType::Uint64
)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum Schema {
#[default]
Libasdf,
Yaml11,
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum Resolved {
Null,
Bool(bool),
Uint(u64, ValueType),
Int(i64, ValueType),
Double(f64),
IntOverflow,
String,
}
impl Resolved {
pub fn value_type(self) -> ValueType {
match self {
Resolved::Null => ValueType::Null,
Resolved::Bool(_) => ValueType::Bool,
Resolved::Uint(_, t) | Resolved::Int(_, t) => t,
Resolved::Double(_) => ValueType::Double,
Resolved::IntOverflow => ValueType::Unknown,
Resolved::String => ValueType::String,
}
}
}
fn narrow_uint(v: u64) -> ValueType {
if v <= u64::from(u8::MAX) {
ValueType::Uint8
} else if v <= u64::from(u16::MAX) {
ValueType::Uint16
} else if v <= u64::from(u32::MAX) {
ValueType::Uint32
} else {
ValueType::Uint64
}
}
fn narrow_int(v: i64) -> ValueType {
if v >= i64::from(i8::MIN) && v <= i64::from(i8::MAX) {
ValueType::Int8
} else if v >= i64::from(i16::MIN) && v <= i64::from(i16::MAX) {
ValueType::Int16
} else if v >= i64::from(i32::MIN) && v <= i64::from(i32::MAX) {
ValueType::Int32
} else {
ValueType::Int64
}
}
pub fn is_null(s: &str) -> bool {
s.is_empty() || s == "~" || s == "null" || s == "Null" || s == "NULL"
}
pub fn parse_bool_libasdf(s: &str) -> Option<bool> {
match s {
"0" => Some(false),
"1" => Some(true),
"true" | "True" | "TRUE" => Some(true),
"false" | "False" | "FALSE" => Some(false),
_ => None,
}
}
pub fn parse_bool_yaml11(s: &str) -> Option<bool> {
match s {
"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,
}
}
fn strtoull_base0_full(s: &str) -> Option<Result<u64, Overflow>> {
let t = s.trim_start();
let body = t.strip_prefix('+').unwrap_or(t);
if !body.starts_with(|c: char| c.is_ascii_digit()) {
return None;
}
let (digits, radix) = split_radix(body);
if digits.is_empty() || !digits.chars().all(|c| c.is_digit(radix)) {
return None;
}
Some(u64::from_str_radix(digits, radix).map_err(|_| Overflow))
}
fn strtoll_base0_full(s: &str) -> Option<Result<i64, Overflow>> {
let t = s.trim_start();
let (neg, body) = match t.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, t.strip_prefix('+').unwrap_or(t)),
};
if !body.starts_with(|c: char| c.is_ascii_digit()) {
return None;
}
let (digits, radix) = split_radix(body);
if digits.is_empty() || !digits.chars().all(|c| c.is_digit(radix)) {
return None;
}
let signed = if neg { format!("-{digits}") } else { digits.to_string() };
Some(i64::from_str_radix(&signed, radix).map_err(|_| Overflow))
}
fn split_radix(body: &str) -> (&str, u32) {
if let Some(hex) = body.strip_prefix("0x").or_else(|| body.strip_prefix("0X")) {
(hex, 16)
} else if body.len() > 1 && body.starts_with('0') {
(&body[1..], 8)
} else {
(body, 10)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Overflow;
fn strtod_full(s: &str) -> Option<f64> {
let body = s.strip_prefix(['+', '-']).unwrap_or(s);
if !body.starts_with(|c: char| c.is_ascii_digit() || c == '.') {
return None;
}
s.parse::<f64>().ok()
}
fn special_float(text: &str) -> Option<f64> {
if let ".nan" | ".NaN" | ".NAN" = text {
return Some(f64::NAN);
}
let (negative, body) = match text.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, text.strip_prefix('+').unwrap_or(text)),
};
match body {
".inf" | ".Inf" | ".INF" => Some(if negative { f64::NEG_INFINITY } else { f64::INFINITY }),
_ => None,
}
}
pub fn resolve(text: &str, style: ScalarStyle, schema: Schema) -> Resolved {
if style.is_quoted() {
return Resolved::String;
}
match schema {
Schema::Libasdf => resolve_libasdf(text),
Schema::Yaml11 => resolve_yaml11(text),
}
}
pub fn resolve_tagged(text: &str, tag_suffix: &str, schema: Schema) -> Option<Resolved> {
match tag_suffix {
"null" => Some(if is_null(text) { Resolved::Null } else { Resolved::String }),
"bool" => {
let parsed = match schema {
Schema::Libasdf => parse_bool_libasdf(text),
Schema::Yaml11 => parse_bool_yaml11(text),
};
Some(parsed.map_or(Resolved::String, Resolved::Bool))
}
"int" => Some(resolve_int_only(text).unwrap_or(Resolved::String)),
"float" => Some(
strtod_full(text)
.or_else(|| special_float(text))
.map_or(Resolved::String, Resolved::Double),
),
"str" => Some(Resolved::String),
_ => None,
}
}
fn resolve_int_only(text: &str) -> Option<Resolved> {
match strtoull_base0_full(text) {
Some(Ok(v)) => return Some(Resolved::Uint(v, narrow_uint(v))),
Some(Err(Overflow)) => return Some(Resolved::IntOverflow),
None => {}
}
match strtoll_base0_full(text) {
Some(Ok(v)) => Some(Resolved::Int(v, narrow_int(v))),
Some(Err(Overflow)) => Some(Resolved::IntOverflow),
None => None,
}
}
fn resolve_libasdf(text: &str) -> Resolved {
if is_null(text) {
return Resolved::Null;
}
if let Some(r) = resolve_int_only(text) {
return r;
}
if let Some(b) = parse_bool_libasdf(text) {
return Resolved::Bool(b);
}
if let Some(d) = strtod_full(text).or_else(|| special_float(text)) {
return Resolved::Double(d);
}
Resolved::String
}
fn resolve_yaml11(text: &str) -> Resolved {
if is_null(text) {
return Resolved::Null;
}
if let Some(b) = parse_bool_yaml11(text) {
return Resolved::Bool(b);
}
if let Some(r) = resolve_int_yaml11(text) {
return r;
}
if let Some(d) = resolve_float_yaml11(text) {
return Resolved::Double(d);
}
Resolved::String
}
fn resolve_int_yaml11(text: &str) -> Option<Resolved> {
let t = text.replace('_', "");
let (neg, body) = match t.strip_prefix('-') {
Some(rest) => (true, rest.to_string()),
None => (false, t.strip_prefix('+').unwrap_or(&t).to_string()),
};
if body.is_empty() {
return None;
}
let (digits, radix) =
if let Some(h) = body.strip_prefix("0x").or_else(|| body.strip_prefix("0X")) {
(h.to_string(), 16)
} else if let Some(o) = body.strip_prefix("0o").or_else(|| body.strip_prefix("0O")) {
(o.to_string(), 8)
} else if let Some(b) = body.strip_prefix("0b").or_else(|| body.strip_prefix("0B")) {
(b.to_string(), 2)
} else if body.contains(':') {
let mut acc: i128 = 0;
for part in body.split(':') {
if part.is_empty() || !part.chars().all(|c| c.is_ascii_digit()) {
return None;
}
acc = acc.checked_mul(60)?.checked_add(part.parse::<i128>().ok()?)?;
}
let v = if neg { -acc } else { acc };
return finish_int_yaml11(v);
} else if body.len() > 1 && body.starts_with('0') {
(body[1..].to_string(), 8)
} else {
(body.clone(), 10)
};
if digits.is_empty() || !digits.chars().all(|c| c.is_digit(radix)) {
return None;
}
let mag = i128::from_str_radix(&digits, radix).ok()?;
finish_int_yaml11(if neg { -mag } else { mag })
}
fn finish_int_yaml11(v: i128) -> Option<Resolved> {
if v >= 0 {
let u = u64::try_from(v).ok()?;
Some(Resolved::Uint(u, narrow_uint(u)))
} else {
let i = i64::try_from(v).ok()?;
Some(Resolved::Int(i, narrow_int(i)))
}
}
fn resolve_float_yaml11(text: &str) -> Option<f64> {
let t = text.replace('_', "");
if let Some(d) = special_float(&t) {
return Some(d);
}
let (sign, rest) = match t.strip_prefix('-') {
Some(r) => (-1.0, r),
None => (1.0, t.strip_prefix('+').unwrap_or(&t)),
};
if rest.contains(':') {
let mut acc = 0f64;
for part in rest.split(':') {
let p: f64 = part.parse().ok()?;
acc = acc * 60.0 + p;
}
return Some(sign * acc);
}
strtod_full(&t)
}
#[cfg(test)]
mod tests {
use super::*;
fn lib(s: &str) -> Resolved {
resolve(s, ScalarStyle::Plain, Schema::Libasdf)
}
fn y11(s: &str) -> Resolved {
resolve(s, ScalarStyle::Plain, Schema::Yaml11)
}
#[test]
fn nulls() {
for s in ["", "~", "null", "Null", "NULL"] {
assert_eq!(lib(s), Resolved::Null, "{s:?}");
}
assert_ne!(lib("nUll"), Resolved::Null);
}
#[test]
fn unsigned_is_preferred_and_narrowed() {
assert_eq!(lib("42"), Resolved::Uint(42, ValueType::Uint8));
assert_eq!(lib("300"), Resolved::Uint(300, ValueType::Uint16));
assert_eq!(lib("70000"), Resolved::Uint(70000, ValueType::Uint32));
assert_eq!(lib("5000000000"), Resolved::Uint(5_000_000_000, ValueType::Uint64));
}
#[test]
fn negatives_are_signed_and_narrowed() {
assert_eq!(lib("-1"), Resolved::Int(-1, ValueType::Int8));
assert_eq!(lib("-200"), Resolved::Int(-200, ValueType::Int16));
assert_eq!(lib("-40000"), Resolved::Int(-40000, ValueType::Int32));
}
#[test]
fn base0_radix_matches_c_not_yaml() {
assert_eq!(lib("010"), Resolved::Uint(8, ValueType::Uint8));
assert_eq!(lib("0x10"), Resolved::Uint(16, ValueType::Uint8));
assert_eq!(lib("0o10"), Resolved::String);
}
#[test]
fn int_is_tried_before_bool() {
assert_eq!(lib("1"), Resolved::Uint(1, ValueType::Uint8));
assert_eq!(lib("0"), Resolved::Uint(0, ValueType::Uint8));
assert_eq!(resolve_tagged("1", "bool", Schema::Libasdf), Some(Resolved::Bool(true)));
}
#[test]
fn bools() {
for s in ["true", "True", "TRUE"] {
assert_eq!(lib(s), Resolved::Bool(true), "{s:?}");
}
for s in ["false", "False", "FALSE"] {
assert_eq!(lib(s), Resolved::Bool(false), "{s:?}");
}
assert_eq!(lib("tRue"), Resolved::String);
}
#[test]
fn floats() {
assert_eq!(lib("1.5"), Resolved::Double(1.5));
assert_eq!(lib("1e3"), Resolved::Double(1000.0));
assert_eq!(lib(".5"), Resolved::Double(0.5));
assert_eq!(lib("-2.25"), Resolved::Double(-2.25));
}
#[test]
fn yaml_infinity_spellings_resolve_under_both_schemas() {
for resolve in [lib as fn(&str) -> Resolved, y11] {
for s in [".inf", ".Inf", ".INF", "+.inf"] {
assert_eq!(resolve(s), Resolved::Double(f64::INFINITY), "{s:?}");
}
for s in ["-.inf", "-.Inf", "-.INF"] {
assert_eq!(resolve(s), Resolved::Double(f64::NEG_INFINITY), "{s:?}");
}
for s in [".nan", ".NaN", ".NAN"] {
assert!(matches!(resolve(s), Resolved::Double(d) if d.is_nan()), "{s:?}");
}
}
}
#[test]
fn near_miss_non_finite_spellings_are_strings() {
for s in ["inf", "Inf", "-inf", "infinity", "nan", "NaN", ".iNf", "-.nan", "+.nan"] {
assert_eq!(lib(s), Resolved::String, "{s:?}");
assert_eq!(y11(s), Resolved::String, "{s:?}");
}
}
#[test]
fn quoting_forces_string() {
for style in [
ScalarStyle::SingleQuoted,
ScalarStyle::DoubleQuoted,
ScalarStyle::Literal,
ScalarStyle::Folded,
] {
assert_eq!(resolve("42", style, Schema::Libasdf), Resolved::String);
assert_eq!(resolve("true", style, Schema::Libasdf), Resolved::String);
}
}
#[test]
fn yaml11_bool_spellings() {
for s in ["yes", "Yes", "YES", "on", "y", "true"] {
assert_eq!(y11(s), Resolved::Bool(true), "{s:?}");
}
for s in ["no", "No", "off", "n", "false"] {
assert_eq!(y11(s), Resolved::Bool(false), "{s:?}");
}
assert_eq!(lib("yes"), Resolved::String);
assert_eq!(y11("yes"), Resolved::Bool(true));
}
#[test]
fn yaml11_underscores_and_sexagesimals() {
assert_eq!(y11("1_000"), Resolved::Uint(1000, ValueType::Uint16));
assert_eq!(y11("0o17"), Resolved::Uint(15, ValueType::Uint8));
assert_eq!(y11("190:20:30"), Resolved::Uint(685230, ValueType::Uint32));
}
#[test]
fn an_integer_too_large_for_any_type_is_neither_int_nor_string() {
for huge in [
"99999999999999999999999999",
"18446744073709551616",
"-99999999999999999999999999",
] {
assert_eq!(lib(huge), Resolved::IntOverflow, "{huge}");
assert_eq!(lib(huge).value_type(), ValueType::Unknown, "{huge}");
}
assert_eq!(lib("18446744073709551615"), Resolved::Uint(u64::MAX, ValueType::Uint64));
assert_eq!(lib("-9223372036854775808"), Resolved::Int(i64::MIN, ValueType::Int64));
}
#[test]
fn plain_text_is_a_string() {
for s in ["hello", "core/ndarray-1.1.0", "1.2.3", "a b c"] {
assert_eq!(lib(s), Resolved::String, "{s:?}");
}
}
#[test]
fn value_type_names_match_libasdf() {
assert_eq!(ValueType::Uint8.as_str(), "uint8");
assert_eq!(ValueType::Double.as_str(), "double");
assert_eq!(ValueType::Unknown.as_str(), "<unknown>");
assert_eq!(ValueType::Extension.as_str(), "<extension>");
}
}