diff --git a/engine/baml-lib/jsonish/src/deserializer/coercer/array_helper.rs b/engine/baml-lib/jsonish/src/deserializer/coercer/array_helper.rs index e64755776..3802e8432 100644 --- a/engine/baml-lib/jsonish/src/deserializer/coercer/array_helper.rs +++ b/engine/baml-lib/jsonish/src/deserializer/coercer/array_helper.rs @@ -44,7 +44,7 @@ pub(super) fn pick_best( std::cmp::Ordering::Greater => std::cmp::Ordering::Greater, }); - log::warn!( + log::trace!( "Picking {} from {:?} items. Picked({:?}):\n{}", target, res_index, diff --git a/engine/baml-lib/jsonish/src/deserializer/coercer/coerce_array.rs b/engine/baml-lib/jsonish/src/deserializer/coercer/coerce_array.rs index ff06cbdcf..3075649cf 100644 --- a/engine/baml-lib/jsonish/src/deserializer/coercer/coerce_array.rs +++ b/engine/baml-lib/jsonish/src/deserializer/coercer/coerce_array.rs @@ -15,6 +15,13 @@ pub(super) fn coerce_array( ) -> Result { assert!(matches!(list_target, FieldType::List(_))); + log::debug!( + "scope: {scope} :: coercing to: {name} (current: {current})", + name = list_target.to_string(), + scope = ctx.display_scope(), + current = value.map(|v| v.r#type()).unwrap_or("".into()) + ); + let inner = match list_target { FieldType::List(inner) => inner, _ => unreachable!(), @@ -34,7 +41,7 @@ pub(super) fn coerce_array( } Some(v) => { flags.add_flag(Flag::SingleToArray); - match inner.coerce(&ctx.enter_scope("0"), inner, Some(v)) { + match inner.coerce(&ctx.enter_scope(""), inner, Some(v)) { Ok(v) => items.push(v), Err(e) => flags.add_flag(Flag::ArrayItemParseError(0, e)), } diff --git a/engine/baml-lib/jsonish/src/deserializer/coercer/coerce_optional.rs b/engine/baml-lib/jsonish/src/deserializer/coercer/coerce_optional.rs index 226c8ef3c..76778913b 100644 --- a/engine/baml-lib/jsonish/src/deserializer/coercer/coerce_optional.rs +++ b/engine/baml-lib/jsonish/src/deserializer/coercer/coerce_optional.rs @@ -14,6 +14,13 @@ pub(super) fn coerce_optional( value: Option<&crate::jsonish::Value>, ) -> Result { assert!(matches!(optional_target, FieldType::Optional(_))); + log::debug!( + "scope: {scope} :: coercing to: {name} (current: {current})", + name = optional_target.to_string(), + scope = ctx.display_scope(), + current = value.map(|v| v.r#type()).unwrap_or("".into()) + ); + let inner = match optional_target { FieldType::Optional(inner) => inner, _ => unreachable!(), diff --git a/engine/baml-lib/jsonish/src/deserializer/coercer/coerce_primitive.rs b/engine/baml-lib/jsonish/src/deserializer/coercer/coerce_primitive.rs index 4c4fe310f..5b55cc416 100644 --- a/engine/baml-lib/jsonish/src/deserializer/coercer/coerce_primitive.rs +++ b/engine/baml-lib/jsonish/src/deserializer/coercer/coerce_primitive.rs @@ -17,6 +17,13 @@ impl TypeCoercer for TypeValue { // Parsed from JSONish value: Option<&crate::jsonish::Value>, ) -> Result { + log::debug!( + "scope: {scope} :: coercing to: {name} (current: {current})", + name = target.to_string(), + scope = ctx.display_scope(), + current = value.map(|v| v.r#type()).unwrap_or("".into()) + ); + match self { TypeValue::String => coerce_string(ctx, target, value), TypeValue::Int => coerce_int(ctx, target, value), diff --git a/engine/baml-lib/jsonish/src/deserializer/coercer/coerce_union.rs b/engine/baml-lib/jsonish/src/deserializer/coercer/coerce_union.rs index 6b649df9b..472a25da1 100644 --- a/engine/baml-lib/jsonish/src/deserializer/coercer/coerce_union.rs +++ b/engine/baml-lib/jsonish/src/deserializer/coercer/coerce_union.rs @@ -11,6 +11,13 @@ pub(super) fn coerce_union( value: Option<&crate::jsonish::Value>, ) -> Result { assert!(matches!(union_target, FieldType::Union(_))); + log::debug!( + "scope: {scope} :: coercing to: {name} (current: {current})", + name = union_target.to_string(), + scope = ctx.display_scope(), + current = value.map(|v| v.r#type()).unwrap_or("".into()) + ); + let options = match union_target { FieldType::Union(options) => options, _ => unreachable!(), diff --git a/engine/baml-lib/jsonish/src/deserializer/coercer/field_type.rs b/engine/baml-lib/jsonish/src/deserializer/coercer/field_type.rs index b68f92b29..cc418ef18 100644 --- a/engine/baml-lib/jsonish/src/deserializer/coercer/field_type.rs +++ b/engine/baml-lib/jsonish/src/deserializer/coercer/field_type.rs @@ -22,6 +22,12 @@ impl TypeCoercer for FieldType { ) -> Result { match value { Some(crate::jsonish::Value::AnyOf(candidates, primitive)) => { + log::debug!( + "scope: {scope} :: coercing to: {name} (current: {current})", + name = target.to_string(), + scope = ctx.display_scope(), + current = value.map(|v| v.r#type()).unwrap_or("".into()) + ); if matches!(target, FieldType::Primitive(TypeValue::String)) { self.coerce( ctx, @@ -38,6 +44,12 @@ impl TypeCoercer for FieldType { } } Some(crate::jsonish::Value::Markdown(_t, v)) => { + log::debug!( + "scope: {scope} :: coercing to: {name} (current: {current})", + name = target.to_string(), + scope = ctx.display_scope(), + current = value.map(|v| v.r#type()).unwrap_or("".into()) + ); self.coerce(ctx, target, Some(v)).and_then(|mut v| { v.add_flag(Flag::ObjectFromMarkdown( if matches!(target, FieldType::Primitive(TypeValue::String)) { @@ -51,6 +63,12 @@ impl TypeCoercer for FieldType { }) } Some(crate::jsonish::Value::FixedJson(v, fixes)) => { + log::debug!( + "scope: {scope} :: coercing to: {name} (current: {current})", + name = target.to_string(), + scope = ctx.display_scope(), + current = value.map(|v| v.r#type()).unwrap_or("".into()) + ); let mut v = self.coerce(ctx, target, Some(v))?; v.add_flag(Flag::ObjectFromFixedJson(fixes.to_vec())); Ok(v) diff --git a/engine/baml-lib/jsonish/src/deserializer/coercer/ir_ref/coerce_class.rs b/engine/baml-lib/jsonish/src/deserializer/coercer/ir_ref/coerce_class.rs index e7888261a..30df21416 100644 --- a/engine/baml-lib/jsonish/src/deserializer/coercer/ir_ref/coerce_class.rs +++ b/engine/baml-lib/jsonish/src/deserializer/coercer/ir_ref/coerce_class.rs @@ -26,6 +26,12 @@ impl TypeCoercer for Class { target: &FieldType, value: Option<&crate::jsonish::Value>, ) -> Result { + log::debug!( + "scope: {scope} :: coercing to: {name} (current: {current})", + name = self.name.real_name(), + scope = ctx.display_scope(), + current = value.map(|v| v.r#type()).unwrap_or("".into()) + ); let (optional, required): (Vec<_>, Vec<_>) = self.fields.iter().partition(|f| f.1.is_optional()); let mut optional_values = optional @@ -40,70 +46,68 @@ impl TypeCoercer for Class { let mut completed_cls = Vec::new(); - match self.fields.len() { - 0 => {} - 1 => { - // Special case for single fields (we may want to consider creating the kv manually) - let field = &self.fields[0]; - let parsed_field = - parse_field((self, target), field, ctx, value, &mut completed_cls, false); - - update_map( - &mut required_values, - &mut optional_values, - field, - parsed_field, - ); + // There are a few possible approaches here: + match value { + None => { + // Do nothing } - _ => { - match value { - None | Some(crate::jsonish::Value::Null) => { - // We have multiple fields, but no value to parse + Some(crate::jsonish::Value::Object(obj)) => { + // match keys, if that fails, then do something fancy later. + obj.iter().for_each(|(key, v)| { + if let Some(field) = self + .fields + .iter() + .find(|(name, ..)| name.rendered_name().trim() == key) + { + let scope = ctx.enter_scope(field.0.real_name()); + let parsed = field.1.coerce(&scope, &field.1, Some(v)); + update_map(&mut required_values, &mut optional_values, field, parsed); + } else { + flags.add_flag(Flag::ExtraKey(key.clone(), v.clone())); } - Some(crate::jsonish::Value::Array(items)) => { - // Coerce the each item into the class - if let Ok(option1) = array_helper::coerce_array_to_singular( - ctx, - target, - &items.iter().collect::>(), - &|value| self.coerce(ctx, target, Some(value)), - ) { - completed_cls.push(Ok(option1)); + }); + } + Some(crate::jsonish::Value::Array(items)) => { + if self.fields.len() == 1 { + let field = &self.fields[0]; + let scope = ctx.enter_scope(&format!("", field.0.real_name())); + let parsed = match field.1.coerce(&scope, &field.1, value) { + Ok(mut v) => { + v.add_flag(Flag::ImpliedKey(field.0.real_name().into())); + Ok(v) } - } - Some(crate::jsonish::Value::Object(obj)) => { - obj.iter().for_each(|(key, v)| { - if let Some(field) = self - .fields - .iter() - .find(|(name, ..)| name.rendered_name().trim() == key) - { - let parsed_field = parse_field( - (self, target), - field, - ctx, - Some(v), - &mut completed_cls, - true, - ); - update_map( - &mut required_values, - &mut optional_values, - field, - parsed_field, - ); - } else { - flags.add_flag(Flag::ExtraKey(key.clone(), v.clone())); - } - }); - } - _ => {} + Err(e) => Err(e), + }; + update_map(&mut required_values, &mut optional_values, field, parsed); + } + + // Coerce the each item into the class if possible + if let Ok(option1) = array_helper::coerce_array_to_singular( + ctx, + target, + &items.iter().collect::>(), + &|value| self.coerce(ctx, target, Some(value)), + ) { + completed_cls.push(Ok(option1)); + } + } + Some(x) => { + // If the class has a single field, then we can try to coerce it directly + if self.fields.len() == 1 { + let field = &self.fields[0]; + let scope = ctx.enter_scope(&format!("", field.0.real_name())); + let parsed = match field.1.coerce(&scope, &field.1, Some(x)) { + Ok(mut v) => { + v.add_flag(Flag::ImpliedKey(field.0.real_name().into())); + Ok(v) + } + Err(e) => Err(e), + }; + update_map(&mut required_values, &mut optional_values, field, parsed); } } } - // Now try and assemble the class. - // Check what we have / what we need { self.fields.iter().for_each(|(field_name, t, ..)| { @@ -112,7 +116,7 @@ impl TypeCoercer for Class { let next = match v { Some(Ok(_)) => None, Some(Err(e)) => { - log::info!( + log::trace!( "Error in optional field {}: {}", field_name.real_name(), e @@ -164,9 +168,9 @@ impl TypeCoercer for Class { } }); - log::info!("---"); + log::trace!("---"); for (k, v) in optional_values.iter() { - log::info!( + log::trace!( " Optional field: {} = ({} + {})", k, v.is_none(), @@ -174,14 +178,14 @@ impl TypeCoercer for Class { ); } for (k, v) in required_values.iter() { - log::info!( + log::trace!( " Required field: {} = ({} + {})", k, v.is_none(), v.as_ref().map(|v| v.is_ok()).unwrap_or(false) ); } - log::info!("----"); + log::trace!("----"); let missing_required_fields = required_values .iter() @@ -190,7 +194,7 @@ impl TypeCoercer for Class { .collect::>(); if !missing_required_fields.is_empty() { - log::info!( + log::trace!( "Missing required fields: {:?} in {:?}", missing_required_fields, value @@ -243,105 +247,12 @@ impl TypeCoercer for Class { } } - log::debug!("Completed class: {:#?}", completed_cls); + log::trace!("Completed class: {:#?}", completed_cls); array_helper::pick_best(ctx, target, &completed_cls) } } -fn parse_field<'a>( - (cls, cls_target): (&'a Class, &FieldType), - (field_name, t, ..): &'a FieldValue, - ctx: &ParsingContext, - value: Option<&crate::jsonish::Value>, - completed_cls: &mut Vec>, - in_key: bool, -) -> Result { - log::info!("Parsing field: {} from {:?}", field_name.real_name(), value); - - match value { - Some(crate::jsonish::Value::Array(items)) => { - // This could be either the case that: - // - multiple candidates for that class - // - multiple values for the field - // - the field itself is mutliple value - - // Coerce the each item into the class - if let Ok(option1) = array_helper::coerce_array_to_singular( - ctx, - cls_target, - &items.iter().collect::>(), - &|value| cls.coerce(ctx, cls_target, Some(value)), - ) { - completed_cls.push(Ok(option1)); - } - - let field_scope = ctx.enter_scope(field_name.real_name()); - // Coerce the each item into the field - let option2 = array_helper::coerce_array_to_singular( - &field_scope, - t, - &items.iter().collect::>(), - &|value| t.coerce(&field_scope, t, Some(value)), - ); - - // Coerce the array to the field - let option3 = t.coerce(&ctx.enter_scope(field_name.real_name()), t, value); - - match array_helper::pick_best(&field_scope, t, &[option2, option3]) { - Ok(mut v) => { - if !in_key { - v.add_flag(Flag::ImpliedKey(field_name.real_name().into())); - } - Ok(v) - } - Err(e) => Err(e), - } - } - Some(crate::jsonish::Value::Object(obj)) => { - let field_scope = ctx.enter_scope(field_name.real_name()); - let valid_keys = [field_name.rendered_name()]; - - // Coerce each matching key into the field - let mut candidates = valid_keys - .iter() - .filter_map(|&key| { - obj.get(key) - .map(|value| t.coerce(&field_scope, t, Some(value))) - }) - .collect::>(); - - if obj.is_empty() && t.is_optional() { - // If the object is empty, and the field is optional, then we can just return null - candidates.push(Ok(BamlValueWithFlags::Null( - DeserializerConditions::new().with_flag(Flag::OptionalDefaultFromNoValue), - ))); - } - - // Also try to implicitly coerce the object into the field - let option2 = match t.coerce(&field_scope, t, value) { - Ok(mut v) => { - v.add_flag(Flag::ImpliedKey(field_name.real_name().into())); - Ok(v) - } - Err(e) => Err(e), - }; - - candidates.push(option2); - array_helper::pick_best(&field_scope, t, &candidates) - } - v => match t.coerce(&ctx.enter_scope(field_name.real_name()), t, v) { - Ok(mut v) => { - if !in_key { - v.add_flag(Flag::ImpliedKey(field_name.real_name().into())); - } - Ok(v) - } - Err(e) => Err(e), - }, - } -} - fn update_map<'a>( required_values: &'a mut HashMap>>, optional_values: &'a mut HashMap>>, @@ -358,13 +269,13 @@ fn update_map<'a>( match map.get(key) { Some(Some(_)) => { // DO NOTHING (keep first value) - log::debug!("Duplicate field: {}", key); + log::trace!("Duplicate field: {}", key); } Some(None) => { map.insert(key.into(), Some(value)); } None => { - log::debug!("Field not found: {}", key); + log::trace!("Field not found: {}", key); } } } diff --git a/engine/baml-lib/jsonish/src/deserializer/coercer/ir_ref/coerce_enum.rs b/engine/baml-lib/jsonish/src/deserializer/coercer/ir_ref/coerce_enum.rs index acb59ea14..97d178c16 100644 --- a/engine/baml-lib/jsonish/src/deserializer/coercer/ir_ref/coerce_enum.rs +++ b/engine/baml-lib/jsonish/src/deserializer/coercer/ir_ref/coerce_enum.rs @@ -36,6 +36,12 @@ impl TypeCoercer for Enum { target: &FieldType, value: Option<&crate::jsonish::Value>, ) -> Result { + log::debug!( + "scope: {scope} :: coercing to: {name} (current: {current})", + name = self.name.real_name(), + scope = ctx.display_scope(), + current = value.map(|v| v.r#type()).unwrap_or("".into()) + ); let value = match value { None | Some(crate::jsonish::Value::Null) => { // If the value is None, we can't parse it. diff --git a/engine/baml-lib/jsonish/src/deserializer/coercer/mod.rs b/engine/baml-lib/jsonish/src/deserializer/coercer/mod.rs index 82c83ca16..bc769d1a7 100644 --- a/engine/baml-lib/jsonish/src/deserializer/coercer/mod.rs +++ b/engine/baml-lib/jsonish/src/deserializer/coercer/mod.rs @@ -7,7 +7,7 @@ mod field_type; mod ir_ref; use anyhow::Result; use internal_baml_jinja::types::OutputFormatContent; -use std::collections::HashMap; +use std::{collections::HashMap, fmt::Display}; use internal_baml_core::ir::{repr::IntermediateRepr, FieldType}; @@ -20,6 +20,13 @@ pub struct ParsingContext<'a> { } impl ParsingContext<'_> { + pub fn display_scope(&self) -> String { + if self.scope.is_empty() { + return "".to_string(); + } + self.scope.join(".") + } + pub(crate) fn new<'a>(of: &'a OutputFormatContent, allow_partials: bool) -> ParsingContext<'a> { ParsingContext { scope: Vec::new(), @@ -133,15 +140,13 @@ impl ParsingContext<'_> { } } - pub(crate) fn error_unexpected_type( + pub(crate) fn error_unexpected_type( &self, target: &FieldType, - got: &crate::jsonish::Value, + got: &T, ) -> ParsingError { - let type_of = got.r#type(); - ParsingError { - reason: format!("Expected {}, got {:#?}.\n{}", target, type_of, got), + reason: format!("Expected {}, got {}.\n{:#?}", target, got, got), scope: self.scope.clone(), } } diff --git a/engine/baml-lib/jsonish/src/deserializer/deserialize_flags.rs b/engine/baml-lib/jsonish/src/deserializer/deserialize_flags.rs index e9a3156e0..2a8695e19 100644 --- a/engine/baml-lib/jsonish/src/deserializer/deserialize_flags.rs +++ b/engine/baml-lib/jsonish/src/deserializer/deserialize_flags.rs @@ -2,6 +2,7 @@ use super::{coercer::ParsingError, types::BamlValueWithFlags}; #[derive(Debug, Clone)] pub enum Flag { + // SingleFromMultiple, ObjectFromMarkdown(i32), ObjectFromFixedJson(Vec), @@ -181,3 +182,9 @@ impl Default for DeserializerConditions { Self::new() } } + +impl From for DeserializerConditions { + fn from(flag: Flag) -> Self { + DeserializerConditions::new().with_flag(flag) + } +} diff --git a/engine/baml-lib/jsonish/src/deserializer/mod.rs b/engine/baml-lib/jsonish/src/deserializer/mod.rs index 68952a36a..431f9ef7f 100644 --- a/engine/baml-lib/jsonish/src/deserializer/mod.rs +++ b/engine/baml-lib/jsonish/src/deserializer/mod.rs @@ -1,4 +1,5 @@ pub mod coercer; mod deserialize_flags; +// pub mod schema; mod score; pub mod types; diff --git a/engine/baml-lib/jsonish/src/deserializer/types.rs b/engine/baml-lib/jsonish/src/deserializer/types.rs index 8139c5d9b..9ffb6e6b8 100644 --- a/engine/baml-lib/jsonish/src/deserializer/types.rs +++ b/engine/baml-lib/jsonish/src/deserializer/types.rs @@ -175,7 +175,7 @@ impl BamlValueWithFlags { .into_iter() .collect::>() .join(" | "); - format!("List[{inner}]") + format!("List[{}:{inner}]", i.len()) } BamlValueWithFlags::Map(_, _) => "Map".to_string(), BamlValueWithFlags::Enum(n, _) => format!("Enum {n}"), diff --git a/engine/baml-lib/jsonish/src/jsonish/value.rs b/engine/baml-lib/jsonish/src/jsonish/value.rs index 16abca7eb..375663e76 100644 --- a/engine/baml-lib/jsonish/src/jsonish/value.rs +++ b/engine/baml-lib/jsonish/src/jsonish/value.rs @@ -1,3 +1,5 @@ +use std::collections::HashSet; + use baml_types::BamlMap; #[derive(Debug, Clone, PartialEq, Eq)] @@ -31,8 +33,27 @@ impl Value { Value::Number(_) => "Number".to_string(), Value::Boolean(_) => "Boolean".to_string(), Value::Null => "Null".to_string(), - Value::Object(_k) => "Object".to_string(), - Value::Array(_i) => "Array".to_string(), + Value::Object(k) => { + let mut s = "Object{".to_string(); + for (key, value) in k.iter() { + s.push_str(&format!("{}: {}, ", key, value.r#type())); + } + s.push('}'); + s + } + Value::Array(i) => { + let mut s = "Array[".to_string(); + let items = i + .iter() + .map(|v| v.r#type()) + .collect::>() + .into_iter() + .collect::>() + .join(" | "); + s.push_str(&items); + s.push(']'); + s + } Value::Markdown(tag, item) => { format!("Markdown:{} - {}", tag, item.r#type()) } diff --git a/engine/baml-lib/jsonish/src/lib.rs b/engine/baml-lib/jsonish/src/lib.rs index c4bfe80e3..a44404590 100644 --- a/engine/baml-lib/jsonish/src/lib.rs +++ b/engine/baml-lib/jsonish/src/lib.rs @@ -24,10 +24,20 @@ pub fn from_str( // When the schema is just a string, i should really just return the raw_string w/o parsing it. let value = jsonish::parse(raw_string, jsonish::ParseOptions::default())?; + // let schema = deserializer::schema::from_jsonish_value(&value, None); - log::info!("Parsed value: {:?}", value); - + // Pick the schema that is the most specific. + // log::info!("Parsed: {}", schema); let ctx = ParsingContext::new(of, allow_partials); + // let res = schema.cast_to(target); + // log::info!("Casted: {:?}", res); + + // match res { + // Ok(v) => Ok(v), + // Err(e) => anyhow::bail!("Failed to cast value: {}", e), + // } + + // Determine the best way to get the desired schema from the parsed schema. // Lets try to now coerce the value into the expected schema. match target.coerce(&ctx, target, Some(&value)) { diff --git a/engine/baml-lib/jsonish/src/tests/mod.rs b/engine/baml-lib/jsonish/src/tests/mod.rs index 0200b725b..16af404bb 100644 --- a/engine/baml-lib/jsonish/src/tests/mod.rs +++ b/engine/baml-lib/jsonish/src/tests/mod.rs @@ -323,11 +323,11 @@ test_deserializer!( const FOO_FILE: &str = r#" class Foo { - id string + id string? } "#; -// This fails becaus +// This fails because we cannot find the inner json blob test_deserializer!( test_string_from_string23, FOO_FILE, @@ -341,7 +341,7 @@ test_deserializer!( "#, FieldType::Class("Foo".to_string()), - json!({"id": r#"{}"# }) + json!({"id": null }) ); // also fails -- if you are in an object and you are casting to a string, dont do that. @@ -362,7 +362,6 @@ test_deserializer!( json!({"id": r#"{{hi} there"# }) ); - const EXAMPLE_FILE: &str = r##" class Score { year int @description(#" @@ -404,7 +403,7 @@ class Score { // wordCounts WordCount[] } -"##; +"##; test_deserializer!( test_string_from_string25, @@ -486,76 +485,148 @@ test_deserializer!( "#, FieldType::Class("BookAnalysis".to_string()), json!({ - "bookNames": ["brave new world", "the lord of the rings", "three body problem", "stormlight archive"], - "popularityOverTime": [ - { - "bookName": "brave new world", - "scores": [ - { - "year": 1932, - "score": 65 - }, - { - "year": 2000, - "score": 80 - }, - { - "year": 2021, - "score": 70 - } - ] - }, - { - "bookName": "the lord of the rings", - "scores": [ - { - "year": 1954, - "score": 75 - }, - { - "year": 2001, - "score": 95 - }, - { - "year": 2021, - "score": 90 - } - ] - }, - { - "bookName": "three body problem", - "scores": [ - { - "year": 2008, - "score": 60 - }, - { - "year": 2014, - "score": 79 - }, - { - "year": 2021, - "score": 85 - } - ] - }, - { - "bookName": "stormlight archive", - "scores": [ - { - "year": 2010, - "score": 76 - }, - { - "year": 2020, - "score": 85 - }, - { - "year": 2021, - "score": 81 - } - ] - } - ] - }) -); \ No newline at end of file + "bookNames": ["brave new world", "the lord of the rings", "three body problem", "stormlight archive"], + "popularityOverTime": [ + { + "bookName": "brave new world", + "scores": [ + { + "year": 1932, + "score": 65 + }, + { + "year": 2000, + "score": 80 + }, + { + "year": 2021, + "score": 70 + } + ] + }, + { + "bookName": "the lord of the rings", + "scores": [ + { + "year": 1954, + "score": 75 + }, + { + "year": 2001, + "score": 95 + }, + { + "year": 2021, + "score": 90 + } + ] + }, + { + "bookName": "three body problem", + "scores": [ + { + "year": 2008, + "score": 60 + }, + { + "year": 2014, + "score": 79 + }, + { + "year": 2021, + "score": 85 + } + ] + }, + { + "bookName": "stormlight archive", + "scores": [ + { + "year": 2010, + "score": 76 + }, + { + "year": 2020, + "score": 85 + }, + { + "year": 2021, + "score": 81 + } + ] + } + ] + }) +); + +test_deserializer!( + test_string_from_string26, + EXAMPLE_FILE, + r#" + { + "bookNames": ["brave new world", "the lord of the rings"], + "popularityData": [ + { + "bookName": "brave new world", + "scores": [ + { + "year": 1932, + "score": 65 + } + ] + }, + { + "bookName": "the lord of the rings", + "scores": [ + { + "year": 1954, + "score": 75 + } + ] + }, + { + "bookName": "the lord of the rings", + "scores": [ + { + "year": 1954, + "score": 75 + } + ] + } + ] + } + "#, + FieldType::Class("BookAnalysis".to_string()), + json!({ + "bookNames": ["brave new world", "the lord of the rings"], + "popularityOverTime": [ + { + "bookName": "brave new world", + "scores": [ + { + "year": 1932, + "score": 65 + } + ] + }, + { + "bookName": "the lord of the rings", + "scores": [ + { + "year": 1954, + "score": 75 + } + ] + }, + { + "bookName": "the lord of the rings", + "scores": [ + { + "year": 1954, + "score": 75 + } + ] + } + ] + }) +);