use std::any::Any;
use serde_json::{Map, Value};
use unicode_normalization::UnicodeNormalization;
use crate::{
deserializer::traits::CoercionContext,
error::{DeserializeError, ParseError, Result},
value::{FlexValue, Transformation},
};
#[derive(Debug, Clone)]
pub struct FieldMatcher {
pub expected: String,
pub allow_substring: bool,
}
impl FieldMatcher {
pub fn new(expected: &str) -> Self {
Self {
expected: expected.to_string(),
allow_substring: false,
}
}
pub fn with_substring_match(mut self) -> Self {
self.allow_substring = true;
self
}
pub fn find_in_object<'a>(
&self,
obj: &'a Map<String, Value>,
) -> Option<(&'a String, &'a Value)> {
let expected_camel = to_camel_case(&self.expected);
let expected_snake = to_snake_case(&self.expected);
if let Some((k, v)) = obj.iter().find(|(k, _)| k.as_str() == self.expected) {
return Some((k, v));
}
if let Some((k, v)) = obj.iter().find(|(k, _)| {
let key = k.as_str();
let key_camel = to_camel_case(key);
let key_snake = to_snake_case(key);
key == expected_camel || key == expected_snake ||
key_camel == self.expected || key_snake == self.expected ||
key_camel == expected_camel || key_snake == expected_snake
}) {
return Some((k, v));
}
let unaccented_expected = remove_accents(&self.expected);
if let Some((k, v)) = obj
.iter()
.find(|(k, _)| remove_accents(k) == unaccented_expected)
{
return Some((k, v));
}
let stripped_expected = strip_punctuation(&self.expected);
if let Some((k, v)) = obj
.iter()
.find(|(k, _)| strip_punctuation(k) == stripped_expected)
{
return Some((k, v));
}
let stripped_lower_expected = stripped_expected.to_lowercase();
if let Some((k, v)) = obj
.iter()
.find(|(k, _)| strip_punctuation(k).to_lowercase() == stripped_lower_expected)
{
return Some((k, v));
}
if self.allow_substring {
let lower_expected = self.expected.to_lowercase();
if let Some((k, v)) = obj.iter().find(|(k, _)| {
let lower_key = k.to_lowercase();
lower_key.contains(&lower_expected) || lower_expected.contains(&lower_key)
}) {
return Some((k, v));
}
}
None
}
pub fn matches(&self, key: &str) -> bool {
let mut temp_map = Map::new();
temp_map.insert(key.to_string(), Value::Null);
self.find_in_object(&temp_map).is_some()
}
}
pub fn to_camel_case(s: &str) -> String {
if !s.contains('_') {
let mut chars = s.chars();
if let Some(first) = chars.next() {
if first.is_lowercase() {
return s.to_string();
}
}
}
let mut result = String::with_capacity(s.len());
let mut capitalize_next = false;
let mut first_char = true;
for ch in s.chars() {
if ch == '_' {
capitalize_next = true;
} else if capitalize_next {
result.push(ch.to_ascii_uppercase());
capitalize_next = false;
first_char = false;
} else if first_char {
result.push(ch.to_ascii_lowercase());
first_char = false;
} else {
result.push(ch);
}
}
result
}
pub fn to_snake_case(s: &str) -> String {
let needs_conversion = s
.chars()
.any(|ch| ch.is_uppercase() || ch == '-' || ch == '.');
if !needs_conversion {
return s.to_string();
}
let mut result = String::with_capacity(s.len() + s.len() / 4);
for ch in s.chars() {
if ch.is_uppercase() {
if !result.is_empty() {
result.push('_');
}
result.push(ch.to_ascii_lowercase());
} else if ch == '-' || ch == '.' {
result.push('_');
} else {
result.push(ch);
}
}
result
}
pub fn apply_rename_all(s: &str, rule: &str) -> String {
match rule {
"snake_case" => to_snake_case(s),
"camelCase" => to_camel_case(s),
"PascalCase" => {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
}
"SCREAMING_SNAKE_CASE" => to_snake_case(s).to_uppercase(),
"kebab-case" => to_snake_case(s).replace('_', "-"),
_ => s.to_string(),
}
}
pub fn remove_accents(s: &str) -> String {
if s.is_ascii() {
return s.to_string();
}
let s = s
.replace('ß', "ss")
.replace('æ', "ae")
.replace('Æ', "AE")
.replace('ø', "o")
.replace('Ø', "O")
.replace('œ', "oe")
.replace('Œ', "OE");
s.nfkd()
.filter(|c| !unicode_normalization::char::is_combining_mark(*c))
.collect()
}
pub fn strip_punctuation(s: &str) -> String {
let needs_stripping = s
.chars()
.any(|c| !c.is_alphanumeric() && c != '-' && c != '_');
if !needs_stripping {
return s.to_string();
}
s.chars()
.filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
.collect()
}
#[derive(Debug, Clone)]
pub struct FieldDescriptor {
pub name: String,
pub type_name: &'static str,
pub is_optional: bool,
}
impl FieldDescriptor {
pub fn new(name: impl Into<String>, type_name: &'static str, is_optional: bool) -> Self {
Self {
name: name.into(),
type_name,
is_optional,
}
}
}
pub struct StructDeserializer {
fields: Vec<FieldDescriptor>,
transformations: Vec<Transformation>,
}
impl StructDeserializer {
pub fn new() -> Self {
Self {
fields: Vec::new(),
transformations: Vec::new(),
}
}
pub fn field(mut self, descriptor: FieldDescriptor) -> Self {
self.fields.push(descriptor);
self
}
pub fn try_deserialize<F>(
&self,
value: &FlexValue,
ctx: &mut CoercionContext,
type_name: &str,
mut deserialize_fn: F,
) -> Result<std::collections::HashMap<String, Box<dyn Any>>>
where
F: FnMut(&str, &FlexValue, &mut CoercionContext) -> Option<Box<dyn Any>>,
{
let obj = match &value.value {
Value::Object(obj) => obj,
_ => {
return Err(ParseError::DeserializeFailed(
DeserializeError::type_mismatch("object", "non-object"),
));
}
};
ctx.check_can_enter_strict(type_name, value)?;
let mut nested_ctx = ctx.with_visited_strict(type_name, value);
let mut result = std::collections::HashMap::new();
for field in &self.fields {
let value = obj.get(&field.name).ok_or_else(|| {
ParseError::DeserializeFailed(DeserializeError::missing_field(&field.name))
})?;
let flex_value = FlexValue::new(value.clone(), crate::value::Source::Direct);
let field_value = deserialize_fn(&field.name, &flex_value, &mut nested_ctx)
.ok_or_else(|| {
ParseError::DeserializeFailed(DeserializeError::type_mismatch(
field.type_name,
"value",
))
})?;
result.insert(field.name.clone(), field_value);
}
if obj.len() != self.fields.len() {
return Err(ParseError::DeserializeFailed(DeserializeError::Custom(
"Extra fields not allowed in strict mode".to_string(),
)));
}
Ok(result)
}
pub fn deserialize<F>(
&mut self,
value: &FlexValue,
ctx: &mut CoercionContext,
type_name: &str,
mut deserialize_fn: F,
) -> Result<std::collections::HashMap<String, Box<dyn Any>>>
where
F: FnMut(&str, &FlexValue, &mut CoercionContext, bool) -> Result<Box<dyn Any>>,
{
let obj = match &value.value {
Value::Object(obj) => obj,
Value::Array(arr) => {
if self.fields.len() == 1 {
return self.try_single_field_coercion(value, ctx, type_name, deserialize_fn);
}
return self.try_array_to_struct_coercion(arr, ctx, type_name, deserialize_fn);
}
_ => {
if self.fields.len() == 1 {
return self.try_single_field_coercion(value, ctx, type_name, deserialize_fn);
}
return Err(ParseError::DeserializeFailed(
DeserializeError::type_mismatch("object", "non-object"),
));
}
};
ctx.check_can_enter_lenient(type_name, value)?;
let mut nested_ctx = ctx.with_visited_lenient(type_name, value);
if let Some(result) = self.try_strict_match(obj, &mut nested_ctx, &mut deserialize_fn) {
return Ok(result);
}
self.try_lenient_match(obj, &mut nested_ctx, deserialize_fn)
}
fn try_strict_match<F>(
&self,
obj: &Map<String, Value>,
ctx: &mut CoercionContext,
deserialize_fn: &mut F,
) -> Option<std::collections::HashMap<String, Box<dyn Any>>>
where
F: FnMut(&str, &FlexValue, &mut CoercionContext, bool) -> Result<Box<dyn Any>>,
{
use crate::value::Source;
let mut result = std::collections::HashMap::new();
for field in &self.fields {
let value = obj.get(&field.name)?;
let flex_value = FlexValue::new(value.clone(), Source::Direct);
let field_value = deserialize_fn(&field.name, &flex_value, ctx, true).ok()?;
result.insert(field.name.clone(), field_value);
}
if obj.len() != self.fields.len() {
return None;
}
Some(result)
}
fn try_lenient_match<F>(
&mut self,
obj: &Map<String, Value>,
ctx: &mut CoercionContext,
mut deserialize_fn: F,
) -> Result<std::collections::HashMap<String, Box<dyn Any>>>
where
F: FnMut(&str, &FlexValue, &mut CoercionContext, bool) -> Result<Box<dyn Any>>,
{
use crate::value::Source;
let mut result = std::collections::HashMap::new();
let mut matched_keys = std::collections::HashSet::new();
for field in &self.fields {
let matcher = FieldMatcher::new(&field.name);
match matcher.find_in_object(obj) {
Some((actual_key, value)) => {
matched_keys.insert(actual_key.clone());
if actual_key != &field.name {
self.transformations
.push(Transformation::FieldNameCaseChanged {
from: actual_key.clone(),
to: field.name.clone(),
});
}
let flex_value = FlexValue::new(value.clone(), Source::Direct);
match deserialize_fn(&field.name, &flex_value, ctx, false) {
Ok(field_value) => {
result.insert(field.name.clone(), field_value);
}
Err(e) => {
if field.is_optional {
let transformation = Transformation::DefaultValueInserted {
field: field.name.clone(),
};
self.transformations.push(transformation.clone());
ctx.add_transformation(transformation);
continue;
} else {
return Err(e);
}
}
}
}
None => {
if field.is_optional {
let transformation = Transformation::DefaultValueInserted {
field: field.name.clone(),
};
self.transformations.push(transformation.clone());
ctx.add_transformation(transformation);
continue;
} else {
return Err(ParseError::DeserializeFailed(
DeserializeError::missing_field(&field.name),
));
}
}
}
}
for (key, _value) in obj.iter() {
if !matched_keys.contains(key) {
let transformation = Transformation::ExtraKey { key: key.clone() };
self.transformations.push(transformation.clone());
ctx.add_transformation(transformation);
}
}
Ok(result)
}
fn try_single_field_coercion<F>(
&mut self,
value: &FlexValue,
ctx: &mut CoercionContext,
_type_name: &str,
mut deserialize_fn: F,
) -> Result<std::collections::HashMap<String, Box<dyn Any>>>
where
F: FnMut(&str, &FlexValue, &mut CoercionContext, bool) -> Result<Box<dyn Any>>,
{
assert_eq!(
self.fields.len(),
1,
"Single field coercion requires exactly one field"
);
let field = &self.fields[0];
match deserialize_fn(&field.name, value, ctx, false) {
Ok(field_value) => {
let transformation = Transformation::ImpliedKey {
field: field.name.clone(),
};
self.transformations.push(transformation.clone());
ctx.add_transformation(transformation);
let mut result = std::collections::HashMap::new();
result.insert(field.name.clone(), field_value);
Ok(result)
}
Err(e) => {
if field.is_optional {
let transformation = Transformation::DefaultValueInserted {
field: field.name.clone(),
};
self.transformations.push(transformation.clone());
ctx.add_transformation(transformation);
Ok(std::collections::HashMap::new())
} else {
Err(e)
}
}
}
}
fn try_array_to_struct_coercion<F>(
&mut self,
arr: &[Value],
ctx: &mut CoercionContext,
_type_name: &str,
mut deserialize_fn: F,
) -> Result<std::collections::HashMap<String, Box<dyn Any>>>
where
F: FnMut(&str, &FlexValue, &mut CoercionContext, bool) -> Result<Box<dyn Any>>,
{
use crate::value::Source;
let required_count = self.fields.iter().filter(|f| !f.is_optional).count();
if arr.len() < required_count {
return Err(ParseError::DeserializeFailed(DeserializeError::Custom(
format!(
"Array has {} elements but struct requires {} fields",
arr.len(),
required_count
),
)));
}
let mut result = std::collections::HashMap::new();
for (index, field) in self.fields.iter().enumerate() {
if index >= arr.len() {
if field.is_optional {
let transformation = Transformation::DefaultValueInserted {
field: field.name.clone(),
};
self.transformations.push(transformation.clone());
ctx.add_transformation(transformation);
continue;
} else {
return Err(ParseError::DeserializeFailed(DeserializeError::Custom(
format!(
"Required field '{}' missing - array only has {} elements",
field.name,
arr.len()
),
)));
}
}
let element = &arr[index];
let flex_value = FlexValue::new(element.clone(), Source::Direct);
match deserialize_fn(&field.name, &flex_value, ctx, false) {
Ok(field_value) => {
let transformation = Transformation::FirstMatch {
index,
total: arr.len(),
};
self.transformations.push(transformation.clone());
ctx.add_transformation(transformation);
result.insert(field.name.clone(), field_value);
}
Err(e) => {
if field.is_optional {
let transformation = Transformation::DefaultButHadUnparseableValue {
field: field.name.clone(),
value: element.to_string(),
error: e.to_string(),
};
self.transformations.push(transformation.clone());
ctx.add_transformation(transformation);
} else {
return Err(e);
}
}
}
}
Ok(result)
}
pub fn transformations(&self) -> &[Transformation] {
&self.transformations
}
pub fn into_transformations(self) -> Vec<Transformation> {
self.transformations
}
}
impl Default for StructDeserializer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn test_remove_accents_basic() {
assert_eq!(remove_accents("café"), "cafe");
assert_eq!(remove_accents("naïve"), "naive");
assert_eq!(remove_accents("résumé"), "resume");
}
#[test]
fn test_remove_accents_german() {
assert_eq!(remove_accents("Straße"), "Strasse");
assert_eq!(remove_accents("Grün"), "Grun");
assert_eq!(remove_accents("Über"), "Uber");
}
#[test]
fn test_remove_accents_nordic() {
assert_eq!(remove_accents("æ"), "ae");
assert_eq!(remove_accents("Æ"), "AE");
assert_eq!(remove_accents("ø"), "o");
assert_eq!(remove_accents("Ø"), "O");
assert_eq!(remove_accents("København"), "Kobenhavn");
}
#[test]
fn test_remove_accents_french() {
assert_eq!(remove_accents("œ"), "oe");
assert_eq!(remove_accents("Œ"), "OE");
assert_eq!(remove_accents("cœur"), "coeur");
assert_eq!(remove_accents("œuvre"), "oeuvre");
}
#[test]
fn test_strip_punctuation() {
assert_eq!(strip_punctuation("user.name"), "username");
assert_eq!(strip_punctuation("first_name"), "first_name");
assert_eq!(strip_punctuation("user-id"), "user-id");
assert_eq!(strip_punctuation("email@address"), "emailaddress");
assert_eq!(strip_punctuation("hello, world!"), "helloworld");
}
#[test]
fn test_to_camel_case() {
assert_eq!(to_camel_case("user_name"), "userName");
assert_eq!(to_camel_case("first_name"), "firstName");
assert_eq!(to_camel_case("email_address"), "emailAddress");
assert_eq!(to_camel_case("a_b_c"), "aBC");
}
#[test]
fn test_to_snake_case() {
assert_eq!(to_snake_case("userName"), "user_name");
assert_eq!(to_snake_case("firstName"), "first_name");
assert_eq!(to_snake_case("emailAddress"), "email_address");
assert_eq!(to_snake_case("ABC"), "a_b_c");
}
#[test]
fn test_field_matcher_exact_match() {
let obj = json!({"user_name": "Alice"}).as_object().unwrap().clone();
let matcher = FieldMatcher::new("user_name");
let result = matcher.find_in_object(&obj);
assert!(result.is_some());
let (key, value) = result.unwrap();
assert_eq!(key, "user_name");
assert_eq!(value, &json!("Alice"));
}
#[test]
fn test_field_matcher_case_insensitive() {
let obj = json!({"UserName": "Alice"}).as_object().unwrap().clone();
let matcher = FieldMatcher::new("user_name");
let result = matcher.find_in_object(&obj);
assert!(result.is_some());
let (key, value) = result.unwrap();
assert_eq!(key, "UserName");
assert_eq!(value, &json!("Alice"));
}
#[test]
fn test_field_matcher_with_punctuation() {
let obj = json!({"user-name": "Alice"}).as_object().unwrap().clone();
let matcher = FieldMatcher::new("user_name");
let result = matcher.find_in_object(&obj);
assert!(result.is_some(), "kebab-case should match snake_case");
let (key, value) = result.unwrap();
assert_eq!(key, "user-name");
assert_eq!(value, &json!("Alice"));
let obj3 = json!({"user.name": "Charlie"}).as_object().unwrap().clone();
let result3 = matcher.find_in_object(&obj3);
assert!(result3.is_some(), "dot.notation should match snake_case");
let (key3, value3) = result3.unwrap();
assert_eq!(key3, "user.name");
assert_eq!(value3, &json!("Charlie"));
let obj2 = json!({"user_name": "Bob"}).as_object().unwrap().clone();
let result2 = matcher.find_in_object(&obj2);
assert!(result2.is_some());
}
#[test]
fn test_field_matcher_with_accents() {
let obj = json!({"café": "Espresso"}).as_object().unwrap().clone();
let matcher = FieldMatcher::new("cafe");
let result = matcher.find_in_object(&obj);
assert!(result.is_some());
let (key, value) = result.unwrap();
assert_eq!(key, "café");
assert_eq!(value, &json!("Espresso"));
}
#[test]
fn test_field_matcher_no_match() {
let obj = json!({"first_name": "Alice"}).as_object().unwrap().clone();
let matcher = FieldMatcher::new("last_name");
let result = matcher.find_in_object(&obj);
assert!(result.is_none());
}
#[test]
fn test_field_matcher_substring_disabled_by_default() {
let obj = json!({"user_name_extra": "Alice"})
.as_object()
.unwrap()
.clone();
let matcher = FieldMatcher::new("user_name");
let result = matcher.find_in_object(&obj);
assert!(result.is_none());
}
#[test]
fn test_field_matcher_substring_enabled() {
let obj = json!({"user_name_extra": "Alice"})
.as_object()
.unwrap()
.clone();
let matcher = FieldMatcher::new("user_name").with_substring_match();
let result = matcher.find_in_object(&obj);
assert!(result.is_some());
let (key, value) = result.unwrap();
assert_eq!(key, "user_name_extra");
assert_eq!(value, &json!("Alice"));
}
#[test]
fn test_matches_method() {
let matcher = FieldMatcher::new("user_name");
assert!(matcher.matches("user_name"));
assert!(matcher.matches("userName"));
assert!(matcher.matches("UserName"));
assert!(matcher.matches("USER_NAME")); assert!(matcher.matches("User_Name")); assert!(!matcher.matches("first_name"));
assert!(!matcher.matches("username"));
}
#[test]
fn test_struct_deserializer_strict_match() {
use crate::value::Source;
let obj = json!({"name": "Alice", "age": 30});
let value = FlexValue::new(obj, Source::Direct);
let mut ctx = CoercionContext::new();
let mut deserializer = StructDeserializer::new()
.field(FieldDescriptor::new("name", "String", false))
.field(FieldDescriptor::new("age", "i64", false));
let result =
deserializer.deserialize(&value, &mut ctx, "TestStruct", |name, val, _ctx, strict| {
if strict {
match (name, &val.value) {
("name", Value::String(s)) => Ok(Box::new(s.clone()) as Box<dyn Any>),
("age", Value::Number(n)) if n.is_i64() => {
Ok(Box::new(n.as_i64().unwrap()) as Box<dyn Any>)
}
_ => Err(ParseError::DeserializeFailed(
DeserializeError::type_mismatch("", ""),
)),
}
} else {
match name {
"name" => Ok(Box::new(val.value.to_string()) as Box<dyn Any>),
"age" => Ok(Box::new(42i64) as Box<dyn Any>),
_ => Err(ParseError::DeserializeFailed(
DeserializeError::type_mismatch("", ""),
)),
}
}
});
assert!(result.is_ok());
let fields = result.unwrap();
assert_eq!(fields.len(), 2);
assert!(fields.contains_key("name"));
assert!(fields.contains_key("age"));
}
#[test]
fn test_struct_deserializer_fuzzy_field_names() {
use crate::value::Source;
let obj = json!({"userName": "Alice", "emailAddress": "alice@example.com"});
let value = FlexValue::new(obj, Source::Direct);
let mut ctx = CoercionContext::new();
let mut deserializer = StructDeserializer::new()
.field(FieldDescriptor::new("user_name", "String", false))
.field(FieldDescriptor::new("email_address", "String", false));
let result =
deserializer.deserialize(&value, &mut ctx, "User", |_name, val, _ctx, _strict| {
if let Value::String(s) = &val.value {
Ok(Box::new(s.clone()) as Box<dyn Any>)
} else {
Err(ParseError::DeserializeFailed(
DeserializeError::type_mismatch("string", "other"),
))
}
});
assert!(result.is_ok());
let fields = result.unwrap();
assert!(fields.contains_key("user_name"));
assert!(fields.contains_key("email_address"));
let transformations = deserializer.transformations();
assert_eq!(transformations.len(), 2);
assert!(transformations.iter().any(
|t| matches!(t, Transformation::FieldNameCaseChanged { from, to }
if from == "userName" && to == "user_name")
));
assert!(transformations.iter().any(
|t| matches!(t, Transformation::FieldNameCaseChanged { from, to }
if from == "emailAddress" && to == "email_address")
));
}
#[test]
fn test_struct_deserializer_optional_field_missing() {
use crate::value::Source;
let obj = json!({"name": "Alice"});
let value = FlexValue::new(obj, Source::Direct);
let mut ctx = CoercionContext::new();
let mut deserializer = StructDeserializer::new()
.field(FieldDescriptor::new("name", "String", false))
.field(FieldDescriptor::new("age", "Option<i64>", true));
let result =
deserializer.deserialize(&value, &mut ctx, "User", |name, val, _ctx, _strict| {
match name {
"name" => {
if let Value::String(s) = &val.value {
Ok(Box::new(s.clone()) as Box<dyn Any>)
} else {
Err(ParseError::DeserializeFailed(
DeserializeError::type_mismatch("string", "other"),
))
}
}
"age" => {
unreachable!()
}
_ => Err(ParseError::DeserializeFailed(
DeserializeError::type_mismatch("", ""),
)),
}
});
assert!(result.is_ok());
let fields = result.unwrap();
assert_eq!(fields.len(), 1);
assert!(fields.contains_key("name"));
assert!(!fields.contains_key("age"));
let transformations = deserializer.transformations();
assert!(transformations.iter().any(
|t| matches!(t, Transformation::DefaultValueInserted { field }
if field == "age")
));
}
#[test]
fn test_struct_deserializer_required_field_missing() {
use crate::value::Source;
let obj = json!({"name": "Alice"});
let value = FlexValue::new(obj, Source::Direct);
let mut ctx = CoercionContext::new();
let mut deserializer = StructDeserializer::new()
.field(FieldDescriptor::new("name", "String", false))
.field(FieldDescriptor::new("age", "i64", false));
let result =
deserializer.deserialize(&value, &mut ctx, "User", |_name, _val, _ctx, _strict| {
Ok(Box::new(String::new()) as Box<dyn Any>)
});
assert!(result.is_err());
if let Err(ParseError::DeserializeFailed(DeserializeError::MissingField { field })) = result
{
assert_eq!(field, "age");
} else {
panic!("Expected MissingField error");
}
}
#[test]
fn test_struct_deserializer_extra_keys() {
use crate::value::Source;
let obj = json!({"name": "Alice", "age": 30, "extra_field": "ignored"});
let value = FlexValue::new(obj, Source::Direct);
let mut ctx = CoercionContext::new();
let mut deserializer = StructDeserializer::new()
.field(FieldDescriptor::new("name", "String", false))
.field(FieldDescriptor::new("age", "i64", false));
let result =
deserializer.deserialize(&value, &mut ctx, "User", |_name, val, _ctx, _strict| {
match &val.value {
Value::String(s) => Ok(Box::new(s.clone()) as Box<dyn Any>),
Value::Number(n) => Ok(Box::new(n.as_i64().unwrap_or(0)) as Box<dyn Any>),
_ => Err(ParseError::DeserializeFailed(
DeserializeError::type_mismatch("", ""),
)),
}
});
assert!(result.is_ok());
let transformations = deserializer.transformations();
assert!(transformations
.iter()
.any(|t| matches!(t, Transformation::ExtraKey { key }
if key == "extra_field")));
}
#[test]
fn test_struct_deserializer_single_field_implicit_key() {
use crate::value::Source;
let arr = json!(["a", "b", "c"]);
let value = FlexValue::new(arr, Source::Direct);
let mut ctx = CoercionContext::new();
let mut deserializer =
StructDeserializer::new().field(FieldDescriptor::new("items", "Vec<String>", false));
let result =
deserializer.deserialize(&value, &mut ctx, "Container", |name, val, _ctx, _strict| {
assert_eq!(name, "items");
if let Value::Array(_) = &val.value {
Ok(
Box::new(vec!["a".to_string(), "b".to_string(), "c".to_string()])
as Box<dyn Any>,
)
} else {
Err(ParseError::DeserializeFailed(
DeserializeError::type_mismatch("array", "other"),
))
}
});
assert!(result.is_ok());
let fields = result.unwrap();
assert_eq!(fields.len(), 1);
assert!(fields.contains_key("items"));
let transformations = deserializer.transformations();
assert!(transformations
.iter()
.any(|t| matches!(t, Transformation::ImpliedKey { field }
if field == "items")));
}
#[test]
fn test_struct_deserializer_circular_detection() {
use crate::value::Source;
let obj = json!({"name": "Node", "child": {"name": "Child"}});
let value = FlexValue::new(obj, Source::Direct);
let mut ctx = CoercionContext::new();
ctx = ctx.with_visited_lenient("Node", &value);
let mut deserializer =
StructDeserializer::new().field(FieldDescriptor::new("name", "String", false));
let result =
deserializer.deserialize(&value, &mut ctx, "Node", |_name, _val, _ctx, _strict| {
Ok(Box::new(String::new()) as Box<dyn Any>)
});
assert!(result.is_err());
if let Err(ParseError::DeserializeFailed(DeserializeError::CircularReference {
type_name,
})) = result
{
assert_eq!(type_name, "Node");
} else {
panic!("Expected CircularReference error, got: {:?}", result);
}
}
}