use regex::Regex;
use std::sync::OnceLock;
#[allow(non_snake_case)]
pub fn NON_PRINTABLE_RE() -> &'static Regex {
static R: OnceLock<Regex> = OnceLock::new();
R.get_or_init(|| {
Regex::new(r"[\x00-\x1F\x7F-\u{9F}]").unwrap()
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CheckResult {
pub proceed: bool,
pub hadproblem: bool,
}
impl CheckResult {
pub fn ok() -> Self {
Self {
proceed: true,
hadproblem: false,
}
}
pub fn failed() -> Self {
Self {
proceed: false,
hadproblem: true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SpecType {
Dict,
List,
Unicode,
Bool,
Float,
Null,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Cmp {
Lt,
Le,
Eq,
Ge,
Gt,
Ne,
}
pub struct Spec {
pub specs: Vec<Spec>,
pub keys: std::collections::HashMap<String, Spec>,
pub cmsg: String,
pub isoptional: bool,
pub did_type: bool,
pub allowed_types: Vec<SpecType>,
pub regex: Option<String>,
pub oneof: Option<Vec<String>>,
pub len_constraints: Vec<(Cmp, i64)>,
pub uspecs: Vec<(usize, usize)>,
pub ufailmsg: Option<String>,
pub cmp_constraint: Option<(Cmp, f64)>,
pub unsigned_flag: bool,
pub printable_flag: bool,
pub ident_flag: bool,
pub error_msg: Option<String>,
}
impl Default for Spec {
fn default() -> Self {
Self::new()
}
}
impl Spec {
pub fn new() -> Self {
Self {
specs: Vec::new(),
keys: std::collections::HashMap::new(),
cmsg: String::new(),
isoptional: false,
did_type: false,
allowed_types: Vec::new(),
regex: None,
oneof: None,
len_constraints: Vec::new(),
uspecs: Vec::new(),
ufailmsg: None,
cmp_constraint: None,
unsigned_flag: false,
printable_flag: false,
ident_flag: false,
error_msg: None,
}
}
pub fn update(mut self, key: impl Into<String>, spec: Spec) -> Self {
self.keys.insert(key.into(), spec);
if !self.keys.is_empty() && !self.did_type {
self.allowed_types.push(SpecType::Dict);
self.did_type = true;
}
self
}
pub fn optional(mut self) -> Self {
self.isoptional = true;
self
}
pub fn required(mut self) -> Self {
self.isoptional = false;
self
}
pub fn context_message(mut self, msg: impl Into<String>) -> Self {
let msg = msg.into();
self.cmsg = msg.clone();
for spec in self.specs.iter_mut() {
if spec.cmsg.is_empty() {
let taken = std::mem::take(spec);
*spec = taken.context_message(msg.clone());
}
}
self
}
pub fn printable(mut self) -> Self {
self.allowed_types.push(SpecType::Unicode);
self.printable_flag = true;
self
}
pub fn unsigned(mut self) -> Self {
self.allowed_types.push(SpecType::Float);
self.cmp_constraint = Some((Cmp::Ge, 0.0));
self.unsigned_flag = true;
self
}
pub fn ident(mut self) -> Self {
self.ident_flag = true;
self.regex = Some(r"^\w+(?::\w+)?$".to_string());
self
}
pub fn type_check(mut self, types: &[SpecType]) -> Self {
self.allowed_types.extend_from_slice(types);
self
}
pub fn regex(mut self, pattern: impl Into<String>) -> Self {
self.regex = Some(pattern.into());
self
}
pub fn re(self, pattern: impl Into<String>) -> Self {
self.regex(pattern)
}
pub fn oneof(mut self, values: &[&str]) -> Self {
self.oneof = Some(values.iter().map(|s| s.to_string()).collect());
self
}
pub fn error(mut self, msg: impl Into<String>) -> Self {
self.error_msg = Some(msg.into());
self
}
pub fn len(mut self, comparison: Cmp, value: i64) -> Self {
self.len_constraints.push((comparison, value));
self
}
pub fn cmp(mut self, comparison: Cmp, value: f64) -> Self {
self.cmp_constraint = Some((comparison, value));
self
}
pub fn copy(&self) -> Spec {
Spec {
specs: self.specs.iter().map(|s| s.copy()).collect(),
keys: self
.keys
.iter()
.map(|(k, v)| (k.clone(), v.copy()))
.collect(),
cmsg: self.cmsg.clone(),
isoptional: self.isoptional,
did_type: self.did_type,
allowed_types: self.allowed_types.clone(),
regex: self.regex.clone(),
oneof: self.oneof.clone(),
len_constraints: self.len_constraints.clone(),
uspecs: self.uspecs.clone(),
ufailmsg: self.ufailmsg.clone(),
cmp_constraint: self.cmp_constraint,
unsigned_flag: self.unsigned_flag,
printable_flag: self.printable_flag,
ident_flag: self.ident_flag,
error_msg: self.error_msg.clone(),
}
}
pub fn check_printable(value: &str) -> CheckResult {
if NON_PRINTABLE_RE().is_match(value) {
CheckResult::failed()
} else {
CheckResult::ok()
}
}
pub fn list(mut self, item_spec: Spec) -> Self {
self.allowed_types.push(SpecType::List);
self.specs.push(item_spec);
self
}
pub fn tuple(mut self, specs: Vec<Spec>) -> Self {
self.allowed_types.push(SpecType::List);
let max_len = specs.len();
let mut min_len = max_len;
for spec in specs.iter().rev() {
if spec.isoptional {
min_len -= 1;
} else {
break;
}
}
if max_len == min_len {
self.len_constraints.push((Cmp::Eq, max_len as i64));
} else {
if min_len > 0 {
self.len_constraints.push((Cmp::Ge, min_len as i64));
}
self.len_constraints.push((Cmp::Le, max_len as i64));
}
for spec in specs {
self.specs.push(spec);
}
self
}
pub fn either(mut self, specs: Vec<Spec>) -> Self {
for spec in specs {
self.specs.push(spec);
}
self
}
pub fn match_checks<F>(checks: &[F]) -> (bool, bool)
where
F: Fn() -> (bool, bool),
{
let mut hadproblem = false;
for check in checks {
let (proceed, chadproblem) = check();
if chadproblem {
hadproblem = true;
}
if !proceed {
return (false, hadproblem);
}
}
(true, hadproblem)
}
pub fn unknown_msg(mut self, msg: impl Into<String>) -> Self {
self.ufailmsg = Some(msg.into());
self
}
pub fn unknown_spec(mut self, key_spec: Spec, value_spec: Spec) -> Self {
self.specs.push(key_spec);
let keyfunc_id = self.specs.len() - 1;
self.specs.push(value_spec);
let spec_id = self.specs.len() - 1;
self.uspecs.push((keyfunc_id, spec_id));
self
}
pub fn check_func<F>(value: &str, func: F) -> (bool, bool)
where
F: FnOnce(&str) -> (bool, bool, bool),
{
let (proceed, _echo, hadproblem) = func(value);
(proceed, hadproblem)
}
pub fn check_tuple<F>(
&self,
values: &[serde_json::Value],
start: usize,
end: usize,
mut match_one: F,
) -> (bool, bool)
where
F: FnMut(usize, usize, &serde_json::Value) -> (bool, bool),
{
let mut hadproblem = false;
let n = end.min(self.specs.len()).saturating_sub(start);
let pairs = values.iter().zip(start..start + n).enumerate();
for (i, (item, spec_idx)) in pairs {
let (proceed, ihadproblem) = match_one(spec_idx, i, item);
if ihadproblem {
hadproblem = true;
}
if !proceed {
return (false, hadproblem);
}
}
(true, hadproblem)
}
pub fn func(mut self, name: impl Into<String>) -> Self {
self.error_msg = Some(name.into());
self
}
pub fn get(&self, key: &str) -> Option<&Spec> {
self.keys.get(key)
}
pub fn set(&mut self, key: impl Into<String>, spec: Spec) {
self.keys.insert(key.into(), spec);
}
pub fn _update(&mut self, other: &Spec) {
self.cmsg = other.cmsg.clone();
self.isoptional = other.isoptional;
self.did_type = other.did_type;
self.allowed_types = other.allowed_types.clone();
self.regex = other.regex.clone();
self.oneof = other.oneof.clone();
self.len_constraints = other.len_constraints.clone();
self.uspecs = other.uspecs.clone();
self.ufailmsg = other.ufailmsg.clone();
self.cmp_constraint = other.cmp_constraint;
self.unsigned_flag = other.unsigned_flag;
self.printable_flag = other.printable_flag;
self.ident_flag = other.ident_flag;
self.error_msg = other.error_msg.clone();
self.keys = other
.keys
.iter()
.map(|(k, v)| (k.clone(), v.copy()))
.collect();
self.specs = other.specs.iter().map(|s| s.copy()).collect();
}
pub fn check_list_walk<F>(values: &[serde_json::Value], mut item_match: F) -> (bool, bool)
where
F: FnMut(usize, &serde_json::Value) -> (bool, bool),
{
let mut hadproblem = false;
for (i, item) in values.iter().enumerate() {
let (proceed, ihadproblem) = item_match(i, item);
if ihadproblem {
hadproblem = true;
}
if !proceed {
return (false, hadproblem);
}
}
(true, hadproblem)
}
pub fn match_dispatch<TC, KM>(
&self,
value: &serde_json::Value,
mut match_top_checks: TC,
mut match_key: KM,
) -> (bool, bool)
where
TC: FnMut(&serde_json::Value) -> (bool, bool),
KM: FnMut(&str, &serde_json::Value) -> (bool, bool),
{
let (proceed, mut hadproblem) = match_top_checks(value);
if !proceed {
return (false, hadproblem);
}
let Some(map) = value.as_object() else {
return (true, hadproblem);
};
if self.keys.is_empty() {
return (true, hadproblem);
}
for (key, valspec) in &self.keys {
if let Some(val) = map.get(key) {
let (kproceed, khadproblem) = match_key(key, val);
if khadproblem {
hadproblem = true;
}
if !kproceed {
return (false, hadproblem);
}
} else if !valspec.isoptional {
hadproblem = true;
}
}
(true, hadproblem)
}
}
pub fn check_type(value: &serde_json::Value, types: &[SpecType]) -> CheckResult {
let actual = match value {
serde_json::Value::Object(_) => SpecType::Dict,
serde_json::Value::Array(_) => SpecType::List,
serde_json::Value::String(_) => SpecType::Unicode,
serde_json::Value::Bool(_) => SpecType::Bool,
serde_json::Value::Number(_) => SpecType::Float,
serde_json::Value::Null => SpecType::Null,
};
if types.contains(&actual) {
CheckResult::ok()
} else {
CheckResult::failed()
}
}
pub fn check_list<F>(items: &[serde_json::Value], mut item_check: F) -> CheckResult
where
F: FnMut(&serde_json::Value) -> CheckResult,
{
let mut proceed = true;
let mut had_problem = false;
for item in items {
let r = item_check(item);
proceed &= r.proceed;
had_problem |= r.hadproblem;
}
CheckResult {
proceed,
hadproblem: had_problem,
}
}
pub fn check_either<F>(specs_count: usize, mut check_one: F) -> CheckResult
where
F: FnMut(usize) -> CheckResult,
{
for i in 0..specs_count {
let r = check_one(i);
if !r.hadproblem {
return CheckResult::ok();
}
}
CheckResult::failed()
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn non_printable_re_matches_control_chars() {
assert!(NON_PRINTABLE_RE().is_match("\x07"));
assert!(NON_PRINTABLE_RE().is_match("\x1f"));
assert!(NON_PRINTABLE_RE().is_match("\t"));
assert!(NON_PRINTABLE_RE().is_match("\n"));
assert!(NON_PRINTABLE_RE().is_match("\x7f")); assert!(NON_PRINTABLE_RE().is_match("\u{0085}")); }
#[test]
fn non_printable_re_allows_printable() {
assert!(!NON_PRINTABLE_RE().is_match("hello"));
assert!(!NON_PRINTABLE_RE().is_match(""));
}
#[test]
fn check_result_ok_is_proceed_true_no_problem() {
let r = CheckResult::ok();
assert!(r.proceed);
assert!(!r.hadproblem);
}
#[test]
fn check_result_failed_is_proceed_false_with_problem() {
let r = CheckResult::failed();
assert!(!r.proceed);
assert!(r.hadproblem);
}
#[test]
fn spec_new_starts_empty() {
let s = Spec::new();
assert!(s.specs.is_empty());
assert!(s.keys.is_empty());
assert!(s.cmsg.is_empty());
assert!(!s.isoptional);
assert!(!s.did_type);
assert!(s.allowed_types.is_empty());
}
#[test]
fn spec_update_registers_key_spec() {
let inner = Spec::new().type_check(&[SpecType::Unicode]);
let s = Spec::new().update("foo", inner);
assert!(s.keys.contains_key("foo"));
}
#[test]
fn spec_optional_sets_isoptional() {
let s = Spec::new().optional();
assert!(s.isoptional);
}
#[test]
fn spec_required_clears_isoptional() {
let s = Spec::new().optional().required();
assert!(!s.isoptional);
}
#[test]
fn spec_context_message_stores_msg() {
let s = Spec::new().context_message("at {key}");
assert_eq!(s.cmsg, "at {key}");
}
#[test]
fn spec_printable_sets_flag() {
let s = Spec::new().printable();
assert!(s.printable_flag);
}
#[test]
fn spec_unsigned_sets_flag() {
let s = Spec::new().unsigned();
assert!(s.unsigned_flag);
}
#[test]
fn spec_ident_sets_flag_and_regex() {
let s = Spec::new().ident();
assert!(s.ident_flag);
assert_eq!(s.regex.as_deref(), Some(r"^\w+(?::\w+)?$"));
}
#[test]
fn spec_type_check_registers_allowed_types() {
let s = Spec::new().type_check(&[SpecType::Unicode, SpecType::Bool]);
assert!(!s.did_type);
assert_eq!(s.allowed_types.len(), 2);
assert!(s.allowed_types.contains(&SpecType::Unicode));
assert!(s.allowed_types.contains(&SpecType::Bool));
}
#[test]
fn spec_regex_stores_pattern() {
let s = Spec::new().regex(r"^\d+$");
assert_eq!(s.regex.as_deref(), Some(r"^\d+$"));
}
#[test]
fn spec_oneof_stores_values() {
let s = Spec::new().oneof(&["foo", "bar"]);
let v = s.oneof.unwrap();
assert_eq!(v, vec!["foo".to_string(), "bar".to_string()]);
}
#[test]
fn spec_error_stores_msg() {
let s = Spec::new().error("invalid value");
assert_eq!(s.error_msg.as_deref(), Some("invalid value"));
}
#[test]
fn spec_len_stores_constraint() {
let s = Spec::new().len(Cmp::Lt, 10);
assert_eq!(s.len_constraints, vec![(Cmp::Lt, 10)]);
}
#[test]
fn spec_cmp_stores_constraint() {
let s = Spec::new().cmp(Cmp::Ge, 0.0);
let c = s.cmp_constraint.unwrap();
assert_eq!(c.0, Cmp::Ge);
assert_eq!(c.1, 0.0);
}
#[test]
fn spec_copy_clones_all_fields() {
let s = Spec::new()
.type_check(&[SpecType::Unicode])
.regex(r"^foo$")
.optional()
.context_message("at {key}");
let c = s.copy();
assert_eq!(c.allowed_types, s.allowed_types);
assert_eq!(c.regex, s.regex);
assert_eq!(c.isoptional, s.isoptional);
assert_eq!(c.cmsg, s.cmsg);
}
#[test]
fn check_printable_accepts_normal_text() {
let r = Spec::check_printable("hello world");
assert!(r.proceed);
assert!(!r.hadproblem);
}
#[test]
fn check_printable_rejects_control_char() {
let r = Spec::check_printable("hello\x07world");
assert!(!r.proceed);
assert!(r.hadproblem);
}
#[test]
fn check_type_object_matches_dict() {
let r = check_type(&json!({"k": 1}), &[SpecType::Dict]);
assert!(r.proceed);
}
#[test]
fn check_type_array_matches_list() {
let r = check_type(&json!([1, 2]), &[SpecType::List]);
assert!(r.proceed);
}
#[test]
fn check_type_string_matches_unicode() {
let r = check_type(&json!("hi"), &[SpecType::Unicode]);
assert!(r.proceed);
}
#[test]
fn check_type_bool_matches_bool() {
let r = check_type(&json!(true), &[SpecType::Bool]);
assert!(r.proceed);
}
#[test]
fn check_type_number_matches_float() {
let r = check_type(&json!(42), &[SpecType::Float]);
assert!(r.proceed);
}
#[test]
fn check_type_null_matches_null() {
let r = check_type(&json!(null), &[SpecType::Null]);
assert!(r.proceed);
}
#[test]
fn check_type_mismatch_returns_failed() {
let r = check_type(&json!("hi"), &[SpecType::Bool]);
assert!(r.hadproblem);
}
#[test]
fn check_list_aggregates_results() {
let items = vec![json!(1), json!(2), json!(3)];
let r = check_list(&items, |_| CheckResult::ok());
assert!(r.proceed);
assert!(!r.hadproblem);
}
#[test]
fn check_list_any_failure_marks_had_problem() {
let items = vec![json!(1), json!(2)];
let mut called = 0;
let r = check_list(&items, |_| {
called += 1;
if called == 1 {
CheckResult::failed()
} else {
CheckResult::ok()
}
});
assert!(r.hadproblem);
assert!(!r.proceed);
}
#[test]
fn check_either_short_circuits_on_first_success() {
let r = check_either(3, |i| {
if i == 0 {
CheckResult::ok()
} else {
CheckResult::failed()
}
});
assert!(r.proceed);
}
#[test]
fn check_either_all_fail_returns_failed() {
let r = check_either(3, |_| CheckResult::failed());
assert!(r.hadproblem);
}
#[test]
fn cmp_enum_variants_match_python_operators() {
let ops = [Cmp::Lt, Cmp::Le, Cmp::Eq, Cmp::Ge, Cmp::Gt, Cmp::Ne];
assert_eq!(ops.len(), 6);
}
#[test]
fn list_pins_type_to_list_and_pushes_item_spec() {
let item_spec = Spec::new().type_check(&[SpecType::Unicode]);
let s = Spec::new().list(item_spec);
assert!(s.allowed_types.contains(&SpecType::List));
assert!(!s.did_type);
assert_eq!(s.specs.len(), 1);
}
#[test]
fn tuple_pins_eq_length_when_no_optionals() {
let specs = vec![
Spec::new().type_check(&[SpecType::Unicode]),
Spec::new().type_check(&[SpecType::Float]),
];
let s = Spec::new().tuple(specs);
assert_eq!(s.len_constraints, vec![(Cmp::Eq, 2)]);
assert_eq!(s.specs.len(), 2);
}
#[test]
fn tuple_uses_le_when_trailing_optionals_present() {
let specs = vec![
Spec::new().type_check(&[SpecType::Unicode]),
Spec::new().type_check(&[SpecType::Float]).optional(),
];
let s = Spec::new().tuple(specs);
assert_eq!(s.len_constraints, vec![(Cmp::Ge, 1), (Cmp::Le, 2)]);
}
#[test]
fn tuple_sets_type_to_list() {
let specs = vec![Spec::new().type_check(&[SpecType::Unicode])];
let s = Spec::new().tuple(specs);
assert!(s.allowed_types.contains(&SpecType::List));
assert!(!s.did_type);
}
#[test]
fn tuple_empty_specs_yields_eq_zero_length() {
let s = Spec::new().tuple(vec![]);
assert_eq!(s.len_constraints, vec![(Cmp::Eq, 0)]);
}
#[test]
fn either_pushes_all_variants_to_specs() {
let variants = vec![
Spec::new().type_check(&[SpecType::Unicode]),
Spec::new().type_check(&[SpecType::Float]),
Spec::new().type_check(&[SpecType::Bool]),
];
let s = Spec::new().either(variants);
assert_eq!(s.specs.len(), 3);
}
#[test]
fn either_empty_variants_leaves_specs_empty() {
let s = Spec::new().either(vec![]);
assert!(s.specs.is_empty());
}
#[test]
fn match_checks_all_pass_returns_true() {
let checks: Vec<Box<dyn Fn() -> (bool, bool)>> =
vec![Box::new(|| (true, false)), Box::new(|| (true, false))];
let r = Spec::match_checks(&checks);
assert_eq!(r, (true, false));
}
#[test]
fn match_checks_records_hadproblem_but_continues() {
let checks: Vec<Box<dyn Fn() -> (bool, bool)>> = vec![
Box::new(|| (true, true)), Box::new(|| (true, false)), ];
let r = Spec::match_checks(&checks);
assert_eq!(r, (true, true));
}
#[test]
fn match_checks_stops_early_on_no_proceed() {
let checks: Vec<Box<dyn Fn() -> (bool, bool)>> = vec![
Box::new(|| (false, true)), Box::new(|| panic!("should not run")),
];
let r = Spec::match_checks(&checks);
assert_eq!(r, (false, true));
}
#[test]
fn match_checks_empty_returns_ok() {
let checks: Vec<Box<dyn Fn() -> (bool, bool)>> = vec![];
let r = Spec::match_checks(&checks);
assert_eq!(r, (true, false));
}
#[test]
fn unknown_msg_returns_self_for_chaining() {
let s = Spec::new().unknown_msg("bad key");
assert!(s.specs.is_empty());
}
#[test]
fn unknown_spec_pushes_key_and_value_specs() {
let key_spec = Spec::new().type_check(&[SpecType::Unicode]);
let value_spec = Spec::new().type_check(&[SpecType::Float]);
let s = Spec::new().unknown_spec(key_spec, value_spec);
assert_eq!(s.specs.len(), 2);
}
#[test]
fn check_func_returns_proceed_hadproblem_pair() {
let (proceed, hadproblem) = Spec::check_func("hello", |v| {
assert_eq!(v, "hello");
(true, false, false)
});
assert!(proceed);
assert!(!hadproblem);
}
#[test]
fn check_func_propagates_hadproblem() {
let (proceed, hadproblem) = Spec::check_func("bad", |_| (true, true, true));
assert!(proceed);
assert!(hadproblem);
}
#[test]
fn check_func_stops_on_no_proceed() {
let (proceed, hadproblem) = Spec::check_func("v", |_| (false, false, true));
assert!(!proceed);
assert!(hadproblem);
}
#[test]
fn check_tuple_walks_each_item_with_corresponding_spec() {
let specs = vec![
Spec::new().type_check(&[SpecType::Unicode]),
Spec::new().type_check(&[SpecType::Float]),
];
let s = Spec::new().tuple(specs);
let values = vec![serde_json::json!("hello"), serde_json::json!(42)];
use std::cell::Cell;
let calls = Cell::new(0);
let (proceed, hadproblem) = s.check_tuple(&values, 0, 2, |spec_idx, item_idx, _item| {
calls.set(calls.get() + 1);
assert_eq!(spec_idx, item_idx);
(true, false)
});
assert!(proceed);
assert!(!hadproblem);
assert_eq!(calls.into_inner(), 2);
}
#[test]
fn check_tuple_early_exits_on_no_proceed() {
let specs = vec![
Spec::new().type_check(&[SpecType::Unicode]),
Spec::new().type_check(&[SpecType::Float]),
];
let s = Spec::new().tuple(specs);
let values = vec![serde_json::json!("hello"), serde_json::json!(42)];
use std::cell::Cell;
let calls = Cell::new(0);
let (proceed, hadproblem) = s.check_tuple(&values, 0, 2, |_, _, _| {
calls.set(calls.get() + 1);
(false, true)
});
assert!(!proceed);
assert!(hadproblem);
assert_eq!(calls.into_inner(), 1);
}
#[test]
fn check_tuple_records_hadproblem_but_continues_when_proceed_true() {
let specs = vec![
Spec::new().type_check(&[SpecType::Unicode]),
Spec::new().type_check(&[SpecType::Float]),
];
let s = Spec::new().tuple(specs);
let values = vec![serde_json::json!("a"), serde_json::json!("b")];
let (proceed, hadproblem) = s.check_tuple(&values, 0, 2, |_, i, _| {
if i == 0 {
(true, true)
} else {
(true, false)
}
});
assert!(proceed);
assert!(hadproblem);
}
#[test]
fn check_list_walk_walks_all_items() {
let values = vec![
serde_json::json!("a"),
serde_json::json!("b"),
serde_json::json!("c"),
];
use std::cell::Cell;
let count = Cell::new(0);
let (proceed, hadproblem) = Spec::check_list_walk(&values, |_, _| {
count.set(count.get() + 1);
(true, false)
});
assert!(proceed);
assert!(!hadproblem);
assert_eq!(count.into_inner(), 3);
}
#[test]
fn check_list_walk_early_exits_on_no_proceed() {
let values = vec![
serde_json::json!("a"),
serde_json::json!("b"),
serde_json::json!("c"),
];
use std::cell::Cell;
let count = Cell::new(0);
let (proceed, hadproblem) = Spec::check_list_walk(&values, |i, _| {
count.set(count.get() + 1);
if i == 1 {
(false, true)
} else {
(true, false)
}
});
assert!(!proceed);
assert!(hadproblem);
assert_eq!(count.into_inner(), 2);
}
#[test]
fn check_list_walk_records_hadproblem_but_continues() {
let values = vec![serde_json::json!("a"), serde_json::json!("b")];
let (proceed, hadproblem) = Spec::check_list_walk(&values, |_, _| (true, true));
assert!(proceed);
assert!(hadproblem);
}
#[test]
fn check_list_walk_empty_returns_ok() {
let values: Vec<serde_json::Value> = vec![];
let (proceed, hadproblem) = Spec::check_list_walk(&values, |_, _| (true, true));
assert!(proceed);
assert!(!hadproblem);
}
#[test]
fn match_dispatch_top_check_failure_returns_early() {
let s = Spec::new();
let value = serde_json::json!({});
let (proceed, hadproblem) =
s.match_dispatch(&value, |_| (false, true), |_, _| panic!("should not run"));
assert!(!proceed);
assert!(hadproblem);
}
#[test]
fn match_dispatch_no_keys_returns_top_check_result() {
let s = Spec::new();
let value = serde_json::json!({});
let (proceed, hadproblem) =
s.match_dispatch(&value, |_| (true, false), |_, _| (true, false));
assert!(proceed);
assert!(!hadproblem);
}
#[test]
fn match_dispatch_walks_registered_keys() {
let key_spec = Spec::new().type_check(&[SpecType::Unicode]);
let s = Spec::new().update("name", key_spec);
let value = serde_json::json!({"name": "value"});
use std::cell::Cell;
let calls = Cell::new(0);
let (proceed, _) = s.match_dispatch(
&value,
|_| (true, false),
|key, _| {
assert_eq!(key, "name");
calls.set(calls.get() + 1);
(true, false)
},
);
assert!(proceed);
assert_eq!(calls.into_inner(), 1);
}
#[test]
fn match_dispatch_missing_required_key_sets_hadproblem() {
let key_spec = Spec::new().type_check(&[SpecType::Unicode]);
let s = Spec::new().update("required_name", key_spec);
let value = serde_json::json!({"other_key": "value"});
let (proceed, hadproblem) =
s.match_dispatch(&value, |_| (true, false), |_, _| (true, false));
assert!(proceed);
assert!(hadproblem);
}
#[test]
fn match_dispatch_missing_optional_key_does_not_set_hadproblem() {
let key_spec = Spec::new().type_check(&[SpecType::Unicode]).optional();
let s = Spec::new().update("opt_name", key_spec);
let value = serde_json::json!({"other_key": "value"});
let (proceed, hadproblem) =
s.match_dispatch(&value, |_| (true, false), |_, _| (true, false));
assert!(proceed);
assert!(!hadproblem);
}
#[test]
fn match_dispatch_propagates_key_check_hadproblem() {
let key_spec = Spec::new().type_check(&[SpecType::Unicode]);
let s = Spec::new().update("name", key_spec);
let value = serde_json::json!({"name": "value"});
let (proceed, hadproblem) =
s.match_dispatch(&value, |_| (true, false), |_, _| (true, true));
assert!(proceed);
assert!(hadproblem);
}
#[test]
fn match_dispatch_early_exits_on_key_no_proceed() {
let key_spec = Spec::new().type_check(&[SpecType::Unicode]);
let s = Spec::new().update("a", key_spec).update("b", Spec::new());
let value = serde_json::json!({"a": "value", "b": "value"});
use std::cell::Cell;
let calls = Cell::new(0);
let (proceed, hadproblem) = s.match_dispatch(
&value,
|_| (true, false),
|_, _| {
calls.set(calls.get() + 1);
(false, true)
},
);
assert!(!proceed);
assert!(hadproblem);
assert_eq!(calls.into_inner(), 1);
}
#[test]
fn match_dispatch_non_map_value_short_circuits_after_top_check() {
let s = Spec::new().update("name", Spec::new());
let value = serde_json::json!("scalar");
let (proceed, _) = s.match_dispatch(
&value,
|_| (true, false),
|_, _| panic!("should not run for scalar"),
);
assert!(proceed);
}
#[test]
fn func_registers_name_via_error_msg_slot() {
let s = Spec::new().func("check_segment_function");
assert_eq!(s.error_msg.as_deref(), Some("check_segment_function"));
}
#[test]
fn get_retrieves_registered_key_spec() {
let item = Spec::new().type_check(&[SpecType::Unicode]);
let s = Spec::new().update("name", item);
let r = s.get("name");
assert!(r.is_some());
assert!(r.unwrap().allowed_types.contains(&SpecType::Unicode));
}
#[test]
fn get_missing_key_returns_none() {
let s = Spec::new();
assert!(s.get("nope").is_none());
}
#[test]
fn set_inserts_spec_under_key() {
let mut s = Spec::new();
let value_spec = Spec::new().type_check(&[SpecType::Float]);
s.set("count", value_spec);
assert!(s.keys.contains_key("count"));
}
#[test]
fn set_overwrites_existing_key() {
let mut s = Spec::new();
s.set("k", Spec::new().type_check(&[SpecType::Unicode]));
s.set("k", Spec::new().type_check(&[SpecType::Float]));
let r = s.get("k").unwrap();
assert!(r.allowed_types.contains(&SpecType::Float));
assert!(!r.allowed_types.contains(&SpecType::Unicode));
}
#[test]
fn _update_copies_all_constraint_fields() {
let source = Spec::new()
.type_check(&[SpecType::Unicode])
.optional()
.printable()
.unsigned()
.ident()
.context_message("x")
.oneof(&["a", "b"]);
let mut target = Spec::new();
target._update(&source);
assert_eq!(target.allowed_types, source.allowed_types);
assert_eq!(target.isoptional, source.isoptional);
assert_eq!(target.printable_flag, source.printable_flag);
assert_eq!(target.unsigned_flag, source.unsigned_flag);
assert_eq!(target.ident_flag, source.ident_flag);
assert_eq!(target.cmsg, source.cmsg);
}
#[test]
fn _update_deep_copies_nested_specs() {
let inner = Spec::new().type_check(&[SpecType::Unicode]);
let source = Spec::new().list(inner);
let mut target = Spec::new();
target._update(&source);
assert_eq!(target.specs.len(), 1);
assert!(target.specs[0].allowed_types.contains(&SpecType::Unicode));
}
#[test]
fn _update_copies_keys_map() {
let nested = Spec::new().type_check(&[SpecType::Float]);
let source = Spec::new().update("count", nested);
let mut target = Spec::new();
target._update(&source);
assert!(target.keys.contains_key("count"));
}
}