use crate::types::ExpectedType;
use crate::value::Value;
#[derive(Debug, Clone)]
pub struct CoercionConfig {
pub level: CoercionLevel,
pub allow_string_to_number: bool,
pub allow_lossy_float_to_int: bool,
pub bool_true_values: Vec<String>,
pub bool_false_values: Vec<String>,
pub null_as_default: bool,
}
impl Default for CoercionConfig {
fn default() -> Self {
Self {
level: CoercionLevel::Strict,
allow_string_to_number: true,
allow_lossy_float_to_int: false,
bool_true_values: vec!["true".into(), "yes".into(), "1".into()],
bool_false_values: vec!["false".into(), "no".into(), "0".into()],
null_as_default: false,
}
}
}
impl CoercionConfig {
pub fn none() -> Self {
Self {
level: CoercionLevel::None,
..Default::default()
}
}
pub fn strict() -> Self {
Self {
level: CoercionLevel::Strict,
..Default::default()
}
}
pub fn standard() -> Self {
Self {
level: CoercionLevel::Standard,
allow_string_to_number: true,
allow_lossy_float_to_int: false,
bool_true_values: vec!["true".into()],
bool_false_values: vec!["false".into()],
null_as_default: false,
}
}
pub fn permissive() -> Self {
Self {
level: CoercionLevel::Permissive,
allow_string_to_number: true,
allow_lossy_float_to_int: true,
null_as_default: true,
..Default::default()
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum CoercionMode {
#[default]
Strict,
Lenient,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CoercionLevel {
None,
#[default]
Strict,
Standard,
Permissive,
}
#[derive(Debug, Clone, PartialEq)]
pub enum CoercionResult {
Matched(Value),
Coerced(Value),
Failed {
value: Value,
expected: ExpectedType,
reason: String,
},
}
impl CoercionResult {
pub fn is_ok(&self) -> bool {
matches!(
self,
CoercionResult::Matched(_) | CoercionResult::Coerced(_)
)
}
pub fn is_err(&self) -> bool {
matches!(self, CoercionResult::Failed { .. })
}
pub fn value(self) -> Option<Value> {
match self {
CoercionResult::Matched(v) | CoercionResult::Coerced(v) => Some(v),
CoercionResult::Failed { .. } => None,
}
}
pub fn value_ref(&self) -> Option<&Value> {
match self {
CoercionResult::Matched(v) | CoercionResult::Coerced(v) => Some(v),
CoercionResult::Failed { .. } => None,
}
}
}
pub fn coerce_with_config(
value: Value,
expected: &ExpectedType,
config: &CoercionConfig,
) -> CoercionResult {
if expected.matches(&value) {
return CoercionResult::Matched(value);
}
if config.level == CoercionLevel::None {
return CoercionResult::Failed {
expected: expected.clone(),
reason: format!(
"cannot coerce {} to {} (coercion disabled)",
describe_value_type(&value),
expected.describe()
),
value,
};
}
match expected {
ExpectedType::Any => CoercionResult::Matched(value),
ExpectedType::Float => coerce_to_float_with_config(value, config),
ExpectedType::Int => coerce_to_int_with_config(value, config),
ExpectedType::Bool => coerce_to_bool_with_config(value, config),
ExpectedType::String => coerce_to_string_with_config(value, config),
ExpectedType::Numeric => coerce_to_numeric_with_config(value, config),
ExpectedType::Union(types) => coerce_to_union_with_config(value, types, config),
ExpectedType::Null => {
if matches!(value, Value::Null) {
CoercionResult::Matched(value)
} else {
CoercionResult::Failed {
expected: expected.clone(),
reason: format!("cannot coerce {} to Null", describe_value_type(&value)),
value,
}
}
}
ExpectedType::List(element_type) => {
if let Value::List(items) = &value {
let mut coerced_items = Vec::with_capacity(items.len());
for item in items.iter() {
match coerce_with_config(item.clone(), element_type, config) {
CoercionResult::Matched(v) | CoercionResult::Coerced(v) => {
coerced_items.push(v);
}
CoercionResult::Failed { reason, .. } => {
return CoercionResult::Failed {
expected: expected.clone(),
reason: format!("list element coercion failed: {}", reason),
value,
};
}
}
}
CoercionResult::Coerced(Value::List(Box::new(coerced_items)))
} else {
CoercionResult::Failed {
expected: expected.clone(),
reason: format!("cannot coerce {} to List", describe_value_type(&value)),
value,
}
}
}
ExpectedType::Tensor { .. } | ExpectedType::Reference { .. } | ExpectedType::Expression => {
CoercionResult::Failed {
expected: expected.clone(),
reason: format!(
"cannot coerce {} to {}",
describe_value_type(&value),
expected.describe()
),
value,
}
}
}
}
pub fn coerce(value: Value, expected: &ExpectedType, mode: CoercionMode) -> CoercionResult {
let config = match mode {
CoercionMode::Strict => CoercionConfig::strict(),
CoercionMode::Lenient => CoercionConfig::standard(),
};
coerce_with_config(value, expected, &config)
}
fn coerce_to_float_with_config(value: Value, config: &CoercionConfig) -> CoercionResult {
match value {
Value::Null if config.null_as_default && config.level == CoercionLevel::Permissive => {
CoercionResult::Coerced(Value::Float(0.0))
}
Value::Int(i) if config.level != CoercionLevel::None => {
CoercionResult::Coerced(Value::Float(i as f64))
}
Value::String(ref s)
if config.allow_string_to_number
&& matches!(
config.level,
CoercionLevel::Standard | CoercionLevel::Permissive
) =>
{
match s.trim().parse::<f64>() {
Ok(f) if f.is_finite() => CoercionResult::Coerced(Value::Float(f)),
_ => CoercionResult::Failed {
expected: ExpectedType::Float,
reason: format!("cannot parse '{}' as float", s),
value,
},
}
}
_ => CoercionResult::Failed {
expected: ExpectedType::Float,
reason: format!("cannot coerce {} to Float", describe_value_type(&value)),
value,
},
}
}
fn coerce_to_int_with_config(value: Value, config: &CoercionConfig) -> CoercionResult {
match value {
Value::Null if config.null_as_default && config.level == CoercionLevel::Permissive => {
CoercionResult::Coerced(Value::Int(0))
}
Value::Float(f)
if config.allow_lossy_float_to_int && config.level == CoercionLevel::Permissive =>
{
if f.is_finite() {
CoercionResult::Coerced(Value::Int(f.trunc() as i64))
} else {
CoercionResult::Failed {
expected: ExpectedType::Int,
reason: format!("cannot convert non-finite float {} to Int", f),
value,
}
}
}
Value::String(ref s)
if config.allow_string_to_number
&& matches!(
config.level,
CoercionLevel::Standard | CoercionLevel::Permissive
) =>
{
match s.trim().parse::<i64>() {
Ok(i) => CoercionResult::Coerced(Value::Int(i)),
Err(_) => CoercionResult::Failed {
expected: ExpectedType::Int,
reason: format!("cannot parse '{}' as integer", s),
value,
},
}
}
_ => CoercionResult::Failed {
expected: ExpectedType::Int,
reason: format!("cannot coerce {} to Int", describe_value_type(&value)),
value,
},
}
}
fn coerce_to_bool_with_config(value: Value, config: &CoercionConfig) -> CoercionResult {
match value {
Value::Null if config.null_as_default && config.level == CoercionLevel::Permissive => {
CoercionResult::Coerced(Value::Bool(false))
}
Value::String(ref s)
if matches!(
config.level,
CoercionLevel::Standard | CoercionLevel::Permissive
) =>
{
let trimmed = s.trim();
if config.bool_true_values.iter().any(|v| v == trimmed) {
return CoercionResult::Coerced(Value::Bool(true));
}
if config.bool_false_values.iter().any(|v| v == trimmed) {
return CoercionResult::Coerced(Value::Bool(false));
}
CoercionResult::Failed {
expected: ExpectedType::Bool,
reason: format!(
"cannot parse '{}' as boolean (expected one of: {}, {})",
s,
config.bool_true_values.join(", "),
config.bool_false_values.join(", ")
),
value,
}
}
_ => CoercionResult::Failed {
expected: ExpectedType::Bool,
reason: format!("cannot coerce {} to Bool", describe_value_type(&value)),
value,
},
}
}
fn coerce_to_string_with_config(value: Value, config: &CoercionConfig) -> CoercionResult {
if matches!(value, Value::Null)
&& config.null_as_default
&& config.level == CoercionLevel::Permissive
{
return CoercionResult::Coerced(Value::String("".into()));
}
if matches!(
config.level,
CoercionLevel::Standard | CoercionLevel::Permissive
) {
let s: Box<str> = match &value {
Value::Null => "~".into(),
Value::Bool(b) => b.to_string().into_boxed_str(),
Value::Int(i) => i.to_string().into_boxed_str(),
Value::Float(f) => f.to_string().into_boxed_str(),
Value::String(s) => return CoercionResult::Matched(Value::String(s.clone())),
Value::Reference(r) => r.to_ref_string().into_boxed_str(),
Value::Expression(_) | Value::Tensor(_) | Value::List(_) => {
return CoercionResult::Failed {
expected: ExpectedType::String,
reason: format!("cannot coerce {} to String", describe_value_type(&value)),
value,
}
}
};
CoercionResult::Coerced(Value::String(s))
} else {
CoercionResult::Failed {
expected: ExpectedType::String,
reason: format!(
"cannot coerce {} to String (coercion level too strict)",
describe_value_type(&value)
),
value,
}
}
}
fn coerce_to_numeric_with_config(value: Value, config: &CoercionConfig) -> CoercionResult {
match &value {
Value::Null if config.null_as_default && config.level == CoercionLevel::Permissive => {
CoercionResult::Coerced(Value::Int(0))
}
Value::Int(_) | Value::Float(_) => CoercionResult::Matched(value),
Value::String(s)
if config.allow_string_to_number
&& matches!(
config.level,
CoercionLevel::Standard | CoercionLevel::Permissive
) =>
{
let trimmed = s.trim();
if let Ok(i) = trimmed.parse::<i64>() {
CoercionResult::Coerced(Value::Int(i))
} else if let Ok(f) = trimmed.parse::<f64>() {
if f.is_finite() {
CoercionResult::Coerced(Value::Float(f))
} else {
CoercionResult::Failed {
expected: ExpectedType::Numeric,
reason: format!("'{}' is not a finite number", s),
value,
}
}
} else {
CoercionResult::Failed {
expected: ExpectedType::Numeric,
reason: format!("cannot parse '{}' as number", s),
value,
}
}
}
_ => CoercionResult::Failed {
expected: ExpectedType::Numeric,
reason: format!("cannot coerce {} to Numeric", describe_value_type(&value)),
value,
},
}
}
fn coerce_to_union_with_config(
value: Value,
types: &[ExpectedType],
config: &CoercionConfig,
) -> CoercionResult {
for expected in types {
if expected.matches(&value) {
return CoercionResult::Matched(value);
}
}
for expected in types {
match coerce_with_config(value.clone(), expected, config) {
result @ CoercionResult::Coerced(_) => return result,
_ => continue,
}
}
let type_names: Vec<String> = types.iter().map(|t| t.describe()).collect();
CoercionResult::Failed {
expected: ExpectedType::Union(types.to_vec()),
reason: format!(
"cannot coerce {} to any of: {}",
describe_value_type(&value),
type_names.join(", ")
),
value,
}
}
fn describe_value_type(value: &Value) -> &'static str {
match value {
Value::Null => "Null",
Value::Bool(_) => "Bool",
Value::Int(_) => "Int",
Value::Float(_) => "Float",
Value::String(_) => "String",
Value::Reference(_) => "Reference",
Value::Expression(_) => "Expression",
Value::Tensor(_) => "Tensor",
Value::List(_) => "List",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_coercion_mode_default() {
assert_eq!(CoercionMode::default(), CoercionMode::Strict);
}
#[test]
fn test_result_matched_is_ok() {
let result = CoercionResult::Matched(Value::Int(42));
assert!(result.is_ok());
assert!(!result.is_err());
}
#[test]
fn test_result_coerced_is_ok() {
let result = CoercionResult::Coerced(Value::Float(42.0));
assert!(result.is_ok());
assert!(!result.is_err());
}
#[test]
fn test_result_failed_is_err() {
let result = CoercionResult::Failed {
value: Value::String("test".to_string().into()),
expected: ExpectedType::Int,
reason: "test".to_string(),
};
assert!(result.is_err());
assert!(!result.is_ok());
}
#[test]
fn test_result_value() {
let result = CoercionResult::Coerced(Value::Int(42));
assert_eq!(result.value(), Some(Value::Int(42)));
let result = CoercionResult::Failed {
value: Value::String("test".to_string().into()),
expected: ExpectedType::Int,
reason: "test".to_string(),
};
assert!(result.value().is_none());
}
#[test]
fn test_result_value_ref() {
let result = CoercionResult::Matched(Value::Int(42));
assert_eq!(result.value_ref(), Some(&Value::Int(42)));
}
#[test]
fn test_int_to_float_strict() {
let result = coerce(Value::Int(42), &ExpectedType::Float, CoercionMode::Strict);
assert!(
matches!(result, CoercionResult::Coerced(Value::Float(f)) if (f - 42.0).abs() < 0.001)
);
}
#[test]
fn test_int_to_float_negative() {
let result = coerce(Value::Int(-100), &ExpectedType::Float, CoercionMode::Strict);
assert!(
matches!(result, CoercionResult::Coerced(Value::Float(f)) if (f + 100.0).abs() < 0.001)
);
}
#[test]
fn test_string_to_int_lenient() {
let result = coerce(
Value::String("42".to_string().into()),
&ExpectedType::Int,
CoercionMode::Lenient,
);
assert!(matches!(result, CoercionResult::Coerced(Value::Int(42))));
}
#[test]
fn test_string_to_int_with_whitespace() {
let result = coerce(
Value::String(" 42 ".to_string().into()),
&ExpectedType::Int,
CoercionMode::Lenient,
);
assert!(matches!(result, CoercionResult::Coerced(Value::Int(42))));
}
#[test]
fn test_string_to_int_negative() {
let result = coerce(
Value::String("-100".to_string().into()),
&ExpectedType::Int,
CoercionMode::Lenient,
);
assert!(matches!(result, CoercionResult::Coerced(Value::Int(-100))));
}
#[test]
fn test_string_to_int_strict_fails() {
let result = coerce(
Value::String("42".to_string().into()),
&ExpectedType::Int,
CoercionMode::Strict,
);
assert!(result.is_err());
}
#[test]
fn test_string_to_int_invalid() {
let result = coerce(
Value::String("not_a_number".to_string().into()),
&ExpectedType::Int,
CoercionMode::Lenient,
);
assert!(result.is_err());
}
#[test]
fn test_string_to_float_lenient() {
let result = coerce(
Value::String("3.25".to_string().into()),
&ExpectedType::Float,
CoercionMode::Lenient,
);
assert!(
matches!(result, CoercionResult::Coerced(Value::Float(f)) if (f - 3.25).abs() < 0.001)
);
}
#[test]
fn test_string_to_float_integer_string() {
let result = coerce(
Value::String("42".to_string().into()),
&ExpectedType::Float,
CoercionMode::Lenient,
);
assert!(
matches!(result, CoercionResult::Coerced(Value::Float(f)) if (f - 42.0).abs() < 0.001)
);
}
#[test]
fn test_string_to_float_strict_fails() {
let result = coerce(
Value::String("3.25".to_string().into()),
&ExpectedType::Float,
CoercionMode::Strict,
);
assert!(result.is_err());
}
#[test]
fn test_string_to_bool_true() {
let result = coerce(
Value::String("true".to_string().into()),
&ExpectedType::Bool,
CoercionMode::Lenient,
);
assert!(matches!(result, CoercionResult::Coerced(Value::Bool(true))));
}
#[test]
fn test_string_to_bool_false() {
let result = coerce(
Value::String("false".to_string().into()),
&ExpectedType::Bool,
CoercionMode::Lenient,
);
assert!(matches!(
result,
CoercionResult::Coerced(Value::Bool(false))
));
}
#[test]
fn test_string_to_bool_with_whitespace() {
let result = coerce(
Value::String(" true ".to_string().into()),
&ExpectedType::Bool,
CoercionMode::Lenient,
);
assert!(matches!(result, CoercionResult::Coerced(Value::Bool(true))));
}
#[test]
fn test_string_to_bool_invalid() {
let result = coerce(
Value::String("maybe".to_string().into()),
&ExpectedType::Bool,
CoercionMode::Lenient,
);
assert!(result.is_err());
}
#[test]
fn test_string_to_bool_strict_fails() {
let result = coerce(
Value::String("true".to_string().into()),
&ExpectedType::Bool,
CoercionMode::Strict,
);
assert!(result.is_err());
}
#[test]
fn test_any_matches_everything() {
let result = coerce(Value::Int(42), &ExpectedType::Any, CoercionMode::Strict);
assert!(matches!(result, CoercionResult::Matched(Value::Int(42))));
let result = coerce(
Value::String("test".to_string().into()),
&ExpectedType::Any,
CoercionMode::Strict,
);
assert!(result.is_ok());
}
#[test]
fn test_numeric_int_matches() {
let result = coerce(Value::Int(42), &ExpectedType::Numeric, CoercionMode::Strict);
assert!(matches!(result, CoercionResult::Matched(Value::Int(42))));
}
#[test]
fn test_numeric_float_matches() {
let result = coerce(
Value::Float(3.25),
&ExpectedType::Numeric,
CoercionMode::Strict,
);
assert!(result.is_ok());
}
#[test]
fn test_string_to_numeric_lenient() {
let result = coerce(
Value::String("42".to_string().into()),
&ExpectedType::Numeric,
CoercionMode::Lenient,
);
assert!(matches!(result, CoercionResult::Coerced(Value::Int(42))));
let result = coerce(
Value::String("3.25".to_string().into()),
&ExpectedType::Numeric,
CoercionMode::Lenient,
);
assert!(
matches!(result, CoercionResult::Coerced(Value::Float(f)) if (f - 3.25).abs() < 0.001)
);
}
#[test]
fn test_union_exact_match() {
let union = ExpectedType::Union(vec![ExpectedType::Int, ExpectedType::String]);
let result = coerce(Value::Int(42), &union, CoercionMode::Strict);
assert!(matches!(result, CoercionResult::Matched(Value::Int(42))));
}
#[test]
fn test_union_coercion() {
let union = ExpectedType::Union(vec![ExpectedType::Float, ExpectedType::String]);
let result = coerce(Value::Int(42), &union, CoercionMode::Strict);
assert!(matches!(result, CoercionResult::Coerced(Value::Float(_))));
}
#[test]
fn test_union_no_match() {
let union = ExpectedType::Union(vec![ExpectedType::Int, ExpectedType::Bool]);
let result = coerce(
Value::String("test".to_string().into()),
&union,
CoercionMode::Strict,
);
assert!(result.is_err());
}
#[test]
fn test_null_no_coercion() {
let result = coerce(Value::Int(0), &ExpectedType::Null, CoercionMode::Lenient);
assert!(result.is_err());
}
#[test]
fn test_expression_no_coercion() {
let result = coerce(
Value::String("$(now())".to_string().into()),
&ExpectedType::Expression,
CoercionMode::Lenient,
);
assert!(result.is_err());
}
#[test]
fn test_int_to_string_lenient() {
let result = coerce(Value::Int(42), &ExpectedType::String, CoercionMode::Lenient);
assert!(matches!(result, CoercionResult::Coerced(Value::String(s)) if s.as_ref() == "42"));
}
#[test]
fn test_bool_to_string_lenient() {
let result = coerce(
Value::Bool(true),
&ExpectedType::String,
CoercionMode::Lenient,
);
assert!(
matches!(result, CoercionResult::Coerced(Value::String(s)) if s.as_ref() == "true")
);
}
#[test]
fn test_to_string_strict_fails() {
let result = coerce(Value::Int(42), &ExpectedType::String, CoercionMode::Strict);
assert!(result.is_err());
}
#[test]
fn test_int_matches_int() {
let result = coerce(Value::Int(42), &ExpectedType::Int, CoercionMode::Strict);
assert!(matches!(result, CoercionResult::Matched(Value::Int(42))));
}
#[test]
fn test_string_matches_string() {
let result = coerce(
Value::String("test".to_string().into()),
&ExpectedType::String,
CoercionMode::Strict,
);
assert!(result.is_ok());
}
#[test]
fn test_coercion_level_default() {
assert_eq!(CoercionLevel::default(), CoercionLevel::Strict);
}
#[test]
fn test_coercion_config_default() {
let config = CoercionConfig::default();
assert_eq!(config.level, CoercionLevel::Strict);
assert!(config.allow_string_to_number);
assert!(!config.allow_lossy_float_to_int);
assert!(!config.null_as_default);
assert_eq!(config.bool_true_values, vec!["true", "yes", "1"]);
assert_eq!(config.bool_false_values, vec!["false", "no", "0"]);
}
#[test]
fn test_coercion_config_none() {
let config = CoercionConfig::none();
assert_eq!(config.level, CoercionLevel::None);
}
#[test]
fn test_coercion_config_strict() {
let config = CoercionConfig::strict();
assert_eq!(config.level, CoercionLevel::Strict);
}
#[test]
fn test_coercion_config_standard() {
let config = CoercionConfig::standard();
assert_eq!(config.level, CoercionLevel::Standard);
assert!(config.allow_string_to_number);
}
#[test]
fn test_coercion_config_permissive() {
let config = CoercionConfig::permissive();
assert_eq!(config.level, CoercionLevel::Permissive);
assert!(config.allow_string_to_number);
assert!(config.allow_lossy_float_to_int);
assert!(config.null_as_default);
}
#[test]
fn test_none_level_no_coercion() {
let config = CoercionConfig::none();
let result = coerce_with_config(Value::Int(42), &ExpectedType::Float, &config);
assert!(result.is_err());
}
#[test]
fn test_none_level_exact_match_ok() {
let config = CoercionConfig::none();
let result = coerce_with_config(Value::Int(42), &ExpectedType::Int, &config);
assert!(matches!(result, CoercionResult::Matched(_)));
}
#[test]
fn test_strict_int_to_float() {
let config = CoercionConfig::strict();
let result = coerce_with_config(Value::Int(42), &ExpectedType::Float, &config);
assert!(
matches!(result, CoercionResult::Coerced(Value::Float(f)) if (f - 42.0).abs() < 0.001)
);
}
#[test]
fn test_strict_no_string_parsing() {
let config = CoercionConfig::strict();
let result = coerce_with_config(
Value::String("42".to_string().into()),
&ExpectedType::Int,
&config,
);
assert!(result.is_err());
}
#[test]
fn test_standard_string_to_int() {
let config = CoercionConfig::standard();
let result = coerce_with_config(
Value::String("42".to_string().into()),
&ExpectedType::Int,
&config,
);
assert!(matches!(result, CoercionResult::Coerced(Value::Int(42))));
}
#[test]
fn test_standard_string_to_float() {
let config = CoercionConfig::standard();
let result = coerce_with_config(
Value::String("2.5".to_string().into()),
&ExpectedType::Float,
&config,
);
assert!(
matches!(result, CoercionResult::Coerced(Value::Float(f)) if (f - 2.5).abs() < 0.001)
);
}
#[test]
fn test_standard_string_to_bool() {
let config = CoercionConfig::standard();
let result = coerce_with_config(
Value::String("true".to_string().into()),
&ExpectedType::Bool,
&config,
);
assert!(matches!(result, CoercionResult::Coerced(Value::Bool(true))));
}
#[test]
fn test_standard_to_string() {
let config = CoercionConfig::standard();
let result = coerce_with_config(Value::Int(42), &ExpectedType::String, &config);
assert!(matches!(result, CoercionResult::Coerced(Value::String(s)) if s.as_ref() == "42"));
}
#[test]
fn test_standard_no_float_to_int() {
let config = CoercionConfig::standard();
let result = coerce_with_config(Value::Float(2.5), &ExpectedType::Int, &config);
assert!(result.is_err());
}
#[test]
fn test_permissive_float_to_int() {
let config = CoercionConfig::permissive();
let result = coerce_with_config(Value::Float(2.5), &ExpectedType::Int, &config);
assert!(matches!(result, CoercionResult::Coerced(Value::Int(2))));
}
#[test]
fn test_permissive_float_to_int_negative() {
let config = CoercionConfig::permissive();
let result = coerce_with_config(Value::Float(-3.9), &ExpectedType::Int, &config);
assert!(matches!(result, CoercionResult::Coerced(Value::Int(-3))));
}
#[test]
fn test_permissive_null_to_int() {
let config = CoercionConfig::permissive();
let result = coerce_with_config(Value::Null, &ExpectedType::Int, &config);
assert!(matches!(result, CoercionResult::Coerced(Value::Int(0))));
}
#[test]
fn test_permissive_null_to_float() {
let config = CoercionConfig::permissive();
let result = coerce_with_config(Value::Null, &ExpectedType::Float, &config);
assert!(matches!(result, CoercionResult::Coerced(Value::Float(f)) if f.abs() < 0.001));
}
#[test]
fn test_permissive_null_to_bool() {
let config = CoercionConfig::permissive();
let result = coerce_with_config(Value::Null, &ExpectedType::Bool, &config);
assert!(matches!(
result,
CoercionResult::Coerced(Value::Bool(false))
));
}
#[test]
fn test_permissive_null_to_string() {
let config = CoercionConfig::permissive();
let result = coerce_with_config(Value::Null, &ExpectedType::String, &config);
assert!(matches!(result, CoercionResult::Coerced(Value::String(s)) if s.as_ref() == ""));
}
#[test]
fn test_permissive_infinity_to_int_fails() {
let config = CoercionConfig::permissive();
let result = coerce_with_config(Value::Float(f64::INFINITY), &ExpectedType::Int, &config);
assert!(result.is_err());
}
#[test]
fn test_custom_bool_true_values() {
let config = CoercionConfig {
level: CoercionLevel::Standard,
bool_true_values: vec!["yes".into(), "on".into(), "enabled".into()],
bool_false_values: vec!["no".into(), "off".into(), "disabled".into()],
..Default::default()
};
let result = coerce_with_config(
Value::String("yes".to_string().into()),
&ExpectedType::Bool,
&config,
);
assert!(matches!(result, CoercionResult::Coerced(Value::Bool(true))));
let result = coerce_with_config(
Value::String("on".to_string().into()),
&ExpectedType::Bool,
&config,
);
assert!(matches!(result, CoercionResult::Coerced(Value::Bool(true))));
let result = coerce_with_config(
Value::String("enabled".to_string().into()),
&ExpectedType::Bool,
&config,
);
assert!(matches!(result, CoercionResult::Coerced(Value::Bool(true))));
}
#[test]
fn test_custom_bool_false_values() {
let config = CoercionConfig {
level: CoercionLevel::Standard,
bool_true_values: vec!["yes".into()],
bool_false_values: vec!["no".into(), "nope".into()],
..Default::default()
};
let result = coerce_with_config(
Value::String("no".to_string().into()),
&ExpectedType::Bool,
&config,
);
assert!(matches!(
result,
CoercionResult::Coerced(Value::Bool(false))
));
let result = coerce_with_config(
Value::String("nope".to_string().into()),
&ExpectedType::Bool,
&config,
);
assert!(matches!(
result,
CoercionResult::Coerced(Value::Bool(false))
));
}
#[test]
fn test_custom_bool_unrecognized_value() {
let config = CoercionConfig {
level: CoercionLevel::Standard,
bool_true_values: vec!["yes".into()],
bool_false_values: vec!["no".into()],
..Default::default()
};
let result = coerce_with_config(
Value::String("maybe".to_string().into()),
&ExpectedType::Bool,
&config,
);
assert!(result.is_err());
}
#[test]
fn test_disable_string_to_number() {
let config = CoercionConfig {
level: CoercionLevel::Standard,
allow_string_to_number: false,
..Default::default()
};
let result = coerce_with_config(
Value::String("42".to_string().into()),
&ExpectedType::Int,
&config,
);
assert!(result.is_err());
let result = coerce_with_config(
Value::String("3.14".to_string().into()),
&ExpectedType::Float,
&config,
);
assert!(result.is_err());
}
#[test]
fn test_numeric_null_to_default() {
let config = CoercionConfig::permissive();
let result = coerce_with_config(Value::Null, &ExpectedType::Numeric, &config);
assert!(matches!(result, CoercionResult::Coerced(Value::Int(0))));
}
#[test]
fn test_numeric_string_to_int() {
let config = CoercionConfig::standard();
let result = coerce_with_config(
Value::String("42".to_string().into()),
&ExpectedType::Numeric,
&config,
);
assert!(matches!(result, CoercionResult::Coerced(Value::Int(42))));
}
#[test]
fn test_numeric_string_to_float() {
let config = CoercionConfig::standard();
let result = coerce_with_config(
Value::String("2.5".to_string().into()),
&ExpectedType::Numeric,
&config,
);
assert!(
matches!(result, CoercionResult::Coerced(Value::Float(f)) if (f - 2.5).abs() < 0.001)
);
}
#[test]
fn test_union_with_permissive_config() {
let union = ExpectedType::Union(vec![ExpectedType::Int, ExpectedType::String]);
let config = CoercionConfig::permissive();
let result = coerce_with_config(Value::Float(42.7), &union, &config);
assert!(matches!(result, CoercionResult::Coerced(Value::Int(42))));
}
#[test]
fn test_union_with_standard_config() {
let union = ExpectedType::Union(vec![ExpectedType::Int, ExpectedType::Bool]);
let config = CoercionConfig::standard();
let result = coerce_with_config(Value::String("42".to_string().into()), &union, &config);
assert!(matches!(result, CoercionResult::Coerced(Value::Int(42))));
}
#[test]
fn test_empty_string_to_int() {
let config = CoercionConfig::standard();
let result = coerce_with_config(
Value::String("".to_string().into()),
&ExpectedType::Int,
&config,
);
assert!(result.is_err());
}
#[test]
fn test_whitespace_only_string_to_int() {
let config = CoercionConfig::standard();
let result = coerce_with_config(
Value::String(" ".to_string().into()),
&ExpectedType::Int,
&config,
);
assert!(result.is_err());
}
#[test]
fn test_nan_to_int_fails() {
let config = CoercionConfig::permissive();
let result = coerce_with_config(Value::Float(f64::NAN), &ExpectedType::Int, &config);
assert!(result.is_err());
}
#[test]
fn test_reference_to_string() {
use crate::value::Reference;
let config = CoercionConfig::standard();
let ref_val = Value::Reference(Reference::qualified("User", "123"));
let result = coerce_with_config(ref_val, &ExpectedType::String, &config);
assert!(
matches!(result, CoercionResult::Coerced(Value::String(s)) if s.as_ref() == "@User:123")
);
}
#[test]
fn test_coerce_mode_to_config_conversion() {
let result = coerce(Value::Int(42), &ExpectedType::Float, CoercionMode::Strict);
assert!(result.is_ok());
let result = coerce(
Value::String("42".to_string().into()),
&ExpectedType::Int,
CoercionMode::Lenient,
);
assert!(result.is_ok());
}
}