use crate::ported::lint::markedjson::error::MarkedError;
use regex::Regex;
use std::collections::HashMap;
use std::sync::OnceLock;
#[derive(Debug, Clone)]
pub struct ResolverError(pub MarkedError);
impl std::fmt::Display for ResolverError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f)
}
}
impl std::error::Error for ResolverError {}
pub const DEFAULT_SCALAR_TAG: &str = "tag:yaml.org,2002:str";
pub const DEFAULT_SEQUENCE_TAG: &str = "tag:yaml.org,2002:seq";
pub const DEFAULT_MAPPING_TAG: &str = "tag:yaml.org,2002:map";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NodeKind {
Scalar,
Sequence,
Mapping,
}
pub struct BaseResolver {
implicit_resolvers: HashMap<char, Vec<(String, Regex)>>,
}
impl Default for BaseResolver {
fn default() -> Self {
Self::new()
}
}
impl BaseResolver {
pub fn new() -> Self {
let mut r = Self {
implicit_resolvers: HashMap::new(),
};
r.register_defaults();
r
}
pub fn add_implicit_resolver(&mut self, tag: &str, regexp: Regex, first: Option<&[char]>) {
let chars: Vec<char> = match first {
Some(s) => s.to_vec(),
None => vec!['\0'],
};
for ch in chars {
self.implicit_resolvers
.entry(ch)
.or_default()
.push((tag.to_string(), regexp.clone()));
}
}
fn register_defaults(&mut self) {
self.add_implicit_resolver(
"tag:yaml.org,2002:bool",
Regex::new(r"^(?:true|false)$").unwrap(),
Some(&['y', 'Y', 'n', 'N', 't', 'T', 'f', 'F', 'o', 'O']),
);
self.add_implicit_resolver(
"tag:yaml.org,2002:float",
Regex::new(r"^-?(?:0|[1-9]\d*)(?:\.\d+(?:[eE][-+]?\d+)?|[eE][-+]?\d+)$").unwrap(),
Some(&['-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']),
);
self.add_implicit_resolver(
"tag:yaml.org,2002:int",
Regex::new(r"^(?:0|-?[1-9]\d*)$").unwrap(),
Some(&['-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']),
);
self.add_implicit_resolver(
"tag:yaml.org,2002:null",
Regex::new(r"^null$").unwrap(),
Some(&['n']),
);
}
pub fn resolve(&self, kind: NodeKind, value: &str, implicit_first: bool) -> String {
if matches!(kind, NodeKind::Scalar) && implicit_first {
let bucket_key: char = if value.is_empty() {
'\0'
} else {
value.chars().next().unwrap()
};
let mut resolvers: Vec<&(String, Regex)> = Vec::new();
if let Some(rs) = self.implicit_resolvers.get(&bucket_key) {
resolvers.extend(rs.iter());
}
if let Some(rs) = self.implicit_resolvers.get(&'\0') {
if bucket_key != '\0' {
resolvers.extend(rs.iter());
}
}
for (tag, regex) in resolvers {
if regex.is_match(value) {
return tag.clone();
}
}
return DEFAULT_SCALAR_TAG.to_string();
}
match kind {
NodeKind::Scalar => DEFAULT_SCALAR_TAG.to_string(),
NodeKind::Sequence => DEFAULT_SEQUENCE_TAG.to_string(),
NodeKind::Mapping => DEFAULT_MAPPING_TAG.to_string(),
}
}
pub fn descend_resolver(&self) {
}
pub fn ascend_resolver(&self) {
}
pub fn check_resolver_prefix(&self) -> bool {
true
}
}
pub fn resolver_singleton() -> &'static BaseResolver {
static R: OnceLock<BaseResolver> = OnceLock::new();
R.get_or_init(BaseResolver::new)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_tags_match_upstream() {
assert_eq!(DEFAULT_SCALAR_TAG, "tag:yaml.org,2002:str");
assert_eq!(DEFAULT_SEQUENCE_TAG, "tag:yaml.org,2002:seq");
assert_eq!(DEFAULT_MAPPING_TAG, "tag:yaml.org,2002:map");
}
#[test]
fn resolve_bool_true() {
let r = BaseResolver::new();
assert_eq!(
r.resolve(NodeKind::Scalar, "true", true),
"tag:yaml.org,2002:bool"
);
}
#[test]
fn resolve_bool_false() {
let r = BaseResolver::new();
assert_eq!(
r.resolve(NodeKind::Scalar, "false", true),
"tag:yaml.org,2002:bool"
);
}
#[test]
fn resolve_int() {
let r = BaseResolver::new();
assert_eq!(
r.resolve(NodeKind::Scalar, "42", true),
"tag:yaml.org,2002:int"
);
assert_eq!(
r.resolve(NodeKind::Scalar, "-7", true),
"tag:yaml.org,2002:int"
);
assert_eq!(
r.resolve(NodeKind::Scalar, "0", true),
"tag:yaml.org,2002:int"
);
}
#[test]
fn resolve_float() {
let r = BaseResolver::new();
assert_eq!(
r.resolve(NodeKind::Scalar, "3.14", true),
"tag:yaml.org,2002:float"
);
assert_eq!(
r.resolve(NodeKind::Scalar, "1e10", true),
"tag:yaml.org,2002:float"
);
assert_eq!(
r.resolve(NodeKind::Scalar, "-2.5e-3", true),
"tag:yaml.org,2002:float"
);
}
#[test]
fn resolve_null() {
let r = BaseResolver::new();
assert_eq!(
r.resolve(NodeKind::Scalar, "null", true),
"tag:yaml.org,2002:null"
);
}
#[test]
fn resolve_unknown_scalar_falls_back_to_string() {
let r = BaseResolver::new();
assert_eq!(
r.resolve(NodeKind::Scalar, "hello", true),
DEFAULT_SCALAR_TAG
);
}
#[test]
fn resolve_non_implicit_scalar_returns_str() {
let r = BaseResolver::new();
assert_eq!(
r.resolve(NodeKind::Scalar, "true", false),
DEFAULT_SCALAR_TAG
);
}
#[test]
fn resolve_sequence_returns_seq_tag() {
let r = BaseResolver::new();
assert_eq!(
r.resolve(NodeKind::Sequence, "", false),
DEFAULT_SEQUENCE_TAG
);
}
#[test]
fn resolve_mapping_returns_map_tag() {
let r = BaseResolver::new();
assert_eq!(r.resolve(NodeKind::Mapping, "", false), DEFAULT_MAPPING_TAG);
}
#[test]
fn add_implicit_resolver_appends_to_first_chars() {
let mut r = BaseResolver::new();
r.add_implicit_resolver("tag:custom", Regex::new(r"^X$").unwrap(), Some(&['X']));
assert_eq!(r.resolve(NodeKind::Scalar, "X", true), "tag:custom");
}
#[test]
fn add_implicit_resolver_with_none_first_uses_null_bucket() {
let mut r = BaseResolver::new();
r.add_implicit_resolver("tag:catchall", Regex::new(r"^ZZZ$").unwrap(), None);
assert_eq!(r.resolve(NodeKind::Scalar, "ZZZ", true), "tag:catchall");
}
#[test]
fn resolve_bool_with_lowercase_first_char_t() {
let r = BaseResolver::new();
assert_eq!(
r.resolve(NodeKind::Scalar, "true", true),
"tag:yaml.org,2002:bool"
);
}
#[test]
fn resolver_singleton_caches_instance() {
let r1 = resolver_singleton();
let r2 = resolver_singleton();
assert!(std::ptr::eq(r1, r2));
}
#[test]
fn resolver_error_implements_error_traits() {
let me = MarkedError::new(Some("ctx"), None, Some("prob"), None, None);
let e = ResolverError(me);
let _: &dyn std::error::Error = &e;
assert!(e.to_string().contains("ctx"));
}
#[test]
fn float_regex_rejects_plain_int() {
let r = BaseResolver::new();
let tag = r.resolve(NodeKind::Scalar, "42", true);
assert_eq!(tag, "tag:yaml.org,2002:int");
assert_ne!(tag, "tag:yaml.org,2002:float");
}
#[test]
fn int_regex_rejects_leading_zero_multi_digit() {
let r = BaseResolver::new();
assert_eq!(r.resolve(NodeKind::Scalar, "00", true), DEFAULT_SCALAR_TAG);
}
}