Last active
December 4, 2025 15:06
-
-
Save tobz/ad9907fd6cf2a001717cc6311392fdd0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| use std::{marker::PhantomData, u8}; | |
| use facet::{ | |
| EnumType, Facet, Field, NumericType, PointerType, PrimitiveType, SequenceType, Shape, ShapeLayout, StructType, | |
| TextualType, Type, UserType, | |
| }; | |
| use facet_reflect::Partial; | |
| use facet_value::{DestructuredRef, Value}; | |
| use saluki_error::{generic_error, ErrorContext, GenericError}; | |
| macro_rules! oversized_numeric { | |
| ($ty:ty, $value:expr) => { | |
| generic_error!( | |
| "Value {} is too large for type '{}' ({}-{}).", | |
| $value, | |
| stringify!($ty), | |
| <$ty>::MIN, | |
| <$ty>::MAX | |
| ) | |
| }; | |
| } | |
| pub struct Deserializer<'a, T> | |
| where | |
| T: Facet<'a> + 'a, | |
| { | |
| partial: Option<Partial<'a>>, | |
| _type: PhantomData<T>, | |
| } | |
| impl<'a, T> Deserializer<'a, T> | |
| where | |
| T: Facet<'a> + 'a, | |
| { | |
| pub fn new() -> Result<Self, GenericError> { | |
| let partial = Partial::alloc::<T>().error_context("Failed to allocate storage for deserializing object.")?; | |
| Ok(Self { | |
| partial: Some(partial), | |
| _type: PhantomData, | |
| }) | |
| } | |
| pub fn apply_value(&mut self, value: &'a Value) -> Result<(), GenericError> { | |
| match self.partial.take() { | |
| Some(partial) => { | |
| let new_partial = apply_value_to_partial(partial, value)?; | |
| self.partial = Some(new_partial); | |
| Ok(()) | |
| } | |
| None => Err(generic_error!( | |
| "Deserializer in non-deterministic state due to prior failure.", | |
| )), | |
| } | |
| } | |
| pub fn build(mut self) -> Result<T, GenericError> { | |
| match self.partial.take() { | |
| Some(partial) => partial | |
| .build() | |
| .error_context("Failed to build object from partial.")? | |
| .materialize() | |
| .error_context("Failed to materialize object to specified type."), | |
| None => Err(generic_error!( | |
| "Deserializer in non-deterministic state due to prior failure.", | |
| )), | |
| } | |
| } | |
| } | |
| fn apply_value_to_partial<'a>(partial: Partial<'a>, value: &'a Value) -> Result<Partial<'a>, GenericError> { | |
| debug_log(&partial, "apply_value_to_partial"); | |
| match partial.shape().ty { | |
| Type::Primitive(pt) => apply_primitive_value_to_partial(pt, partial, value), | |
| Type::Sequence(st) => apply_sequence_value_to_partial(st, partial, value), | |
| Type::User(ut) => apply_user_value_to_partial(ut, partial, value), | |
| Type::Pointer(pt) => apply_pointer_value_to_partial(pt, partial, value), | |
| } | |
| } | |
| fn apply_primitive_value_to_partial<'a>( | |
| pt: PrimitiveType, partial: Partial<'a>, value: &'a Value, | |
| ) -> Result<Partial<'a>, GenericError> { | |
| debug_log(&partial, "apply_primitive_value_to_partial"); | |
| match pt { | |
| PrimitiveType::Boolean => value | |
| .as_bool() | |
| .ok_or_else(|| type_mismatch("boolean", value)) | |
| .and_then(|value| set_partial_value(partial, value)), | |
| PrimitiveType::Numeric(nt) => apply_numeric_value_to_partial(nt, partial, value), | |
| PrimitiveType::Textual(tt) => apply_textual_value_to_partial(tt, partial, value), | |
| // TODO: Add frame path to error. | |
| PrimitiveType::Never => Err(generic_error!("Never type cannot be deserialized.")), | |
| } | |
| } | |
| fn apply_numeric_value_to_partial<'a>( | |
| nt: NumericType, partial: Partial<'a>, value: &Value, | |
| ) -> Result<Partial<'a>, GenericError> { | |
| debug_log(&partial, "apply_numeric_value_to_partial"); | |
| match nt { | |
| NumericType::Integer { signed } => { | |
| if signed { | |
| assign_signed_integer_value_to_partial(partial, value) | |
| } else { | |
| assign_unsigned_integer_value_to_partial(partial, value) | |
| } | |
| } | |
| NumericType::Float => assign_float_value_to_partial(partial, value), | |
| } | |
| } | |
| fn assign_signed_integer_value_to_partial<'a>( | |
| partial: Partial<'a>, value: &Value, | |
| ) -> Result<Partial<'a>, GenericError> { | |
| debug_log(&partial, "apply_signed_integer_value_to_partial"); | |
| let int_value = value | |
| .as_number() | |
| .and_then(|n| n.to_i64()) | |
| .ok_or_else(|| type_mismatch("int", value))?; | |
| match get_shape_bitwidth(partial.shape()) { | |
| i8::BITS => { | |
| let value = i8::try_from(int_value).map_err(|_| oversized_numeric!(i8, int_value))?; | |
| set_partial_value(partial, value) | |
| } | |
| i16::BITS => { | |
| let value = i16::try_from(int_value).map_err(|_| oversized_numeric!(i16, int_value))?; | |
| set_partial_value(partial, value) | |
| } | |
| i32::BITS => { | |
| let value = i32::try_from(int_value).map_err(|_| oversized_numeric!(i32, int_value))?; | |
| set_partial_value(partial, value) | |
| } | |
| i64::BITS => set_partial_value(partial, int_value), | |
| _ => Err(generic_error!("Unhandled signed integer type '{}'.", partial.shape())), | |
| } | |
| } | |
| fn assign_unsigned_integer_value_to_partial<'a>( | |
| partial: Partial<'a>, value: &Value, | |
| ) -> Result<Partial<'a>, GenericError> { | |
| debug_log(&partial, "apply_unsigned_integer_value_to_partial"); | |
| let uint_value = value | |
| .as_number() | |
| .and_then(|n| n.to_u64()) | |
| .ok_or_else(|| type_mismatch("uint", value))?; | |
| match get_shape_bitwidth(partial.shape()) { | |
| u8::BITS => { | |
| let value = u8::try_from(uint_value).map_err(|_| oversized_numeric!(u8, uint_value))?; | |
| set_partial_value(partial, value) | |
| } | |
| u16::BITS => { | |
| let value = u16::try_from(uint_value).map_err(|_| oversized_numeric!(u16, uint_value))?; | |
| set_partial_value(partial, value) | |
| } | |
| u32::BITS => { | |
| let value = u32::try_from(uint_value).map_err(|_| oversized_numeric!(u32, uint_value))?; | |
| set_partial_value(partial, value) | |
| } | |
| u64::BITS => set_partial_value(partial, uint_value), | |
| _ => Err(generic_error!("Unhandled unsigned integer type '{}'.", partial.shape())), | |
| } | |
| } | |
| fn assign_float_value_to_partial<'a>(partial: Partial<'a>, value: &Value) -> Result<Partial<'a>, GenericError> { | |
| debug_log(&partial, "apply_float_value_to_partial"); | |
| if partial.shape() == f32::SHAPE { | |
| value | |
| .as_number() | |
| .and_then(|n| n.to_f32()) | |
| .ok_or_else(|| type_mismatch("f32", value)) | |
| .and_then(|value| set_partial_value(partial, value)) | |
| } else if partial.shape() == f64::SHAPE { | |
| value | |
| .as_number() | |
| .and_then(|n| n.to_f64()) | |
| .ok_or_else(|| type_mismatch("f64", value)) | |
| .and_then(|value| set_partial_value(partial, value)) | |
| } else { | |
| Err(generic_error!("Unhandled float type '{}'.", partial.shape())) | |
| } | |
| } | |
| fn apply_textual_value_to_partial<'a>( | |
| tt: TextualType, partial: Partial<'a>, value: &'a Value, | |
| ) -> Result<Partial<'a>, GenericError> { | |
| debug_log(&partial, "apply_textual_value_to_partial"); | |
| let str_value = value.as_string().ok_or_else(|| type_mismatch("string", value))?; | |
| match tt { | |
| TextualType::Char => { | |
| let mut str_chars = str_value.chars(); | |
| // We expect a single character, so if we got an empty string, or a string with two or more characters, that's an error. | |
| let char_value = str_chars | |
| .next() | |
| .ok_or_else(|| generic_error!("Expected a single character, got '{}'.", str_value))?; | |
| if str_chars.next().is_some() { | |
| return Err(generic_error!("Expected a single character, got '{}'.", str_value)); | |
| } | |
| set_partial_value(partial, char_value) | |
| } | |
| TextualType::Str => set_partial_value(partial, str_value.as_str()), | |
| } | |
| } | |
| fn apply_sequence_value_to_partial<'a>( | |
| st: SequenceType, partial: Partial<'a>, value: &Value, | |
| ) -> Result<Partial<'a>, GenericError> { | |
| debug_log(&partial, "apply_sequence_value_to_partial"); | |
| Err(generic_error!("Sequences are not supported.")) | |
| } | |
| fn apply_user_value_to_partial<'a>( | |
| ut: UserType, partial: Partial<'a>, value: &'a Value, | |
| ) -> Result<Partial<'a>, GenericError> { | |
| debug_log(&partial, "apply_user_value_to_partial"); | |
| match ut { | |
| UserType::Struct(st) => apply_struct_value_to_partial(st, partial, value), | |
| UserType::Enum(et) => apply_enum_value_to_partial(et, partial, value), | |
| UserType::Union(_) => Err(generic_error!("Union types are not supported.")), | |
| UserType::Opaque => { | |
| debug_log(&partial, format!("arrived at opaque type: {:#?}", partial.shape())); | |
| Err(generic_error!("Opaque types are not supported.")) | |
| } | |
| } | |
| } | |
| fn apply_struct_value_to_partial<'a>( | |
| st: StructType, partial: Partial<'a>, value: &'a Value, | |
| ) -> Result<Partial<'a>, GenericError> { | |
| debug_log(&partial, "apply_struct_value_to_partial"); | |
| match st.kind { | |
| facet::StructKind::Unit => todo!(), | |
| facet::StructKind::TupleStruct => todo!(), | |
| facet::StructKind::Struct => apply_named_field_struct_value_to_partial(st.fields, partial, value), | |
| facet::StructKind::Tuple => todo!(), | |
| } | |
| } | |
| fn apply_named_field_struct_value_to_partial<'a>( | |
| fields: &'static [Field], mut partial: Partial<'a>, value: &'a Value, | |
| ) -> Result<Partial<'a>, GenericError> { | |
| debug_log(&partial, "apply_named_field_struct_value_to_partial"); | |
| let obj_value = value.as_object().ok_or_else(|| type_mismatch("object", value))?; | |
| for field in fields { | |
| debug_log(&partial, format!("handling field {}", field.name)); | |
| if let Some(field_value) = obj_value.get(field.name) { | |
| debug_log(&partial, format!("found value for field {}, applying...", field.name)); | |
| partial = partial.begin_field(field.name)?; | |
| partial = apply_value_to_partial(partial, field_value)?; | |
| partial = partial.end()?; | |
| } | |
| } | |
| Ok(partial) | |
| } | |
| fn apply_enum_value_to_partial<'a>( | |
| et: EnumType, partial: Partial<'a>, value: &Value, | |
| ) -> Result<Partial<'a>, GenericError> { | |
| debug_log(&partial, "apply_enum_value_to_partial"); | |
| todo!() | |
| } | |
| fn apply_pointer_value_to_partial<'a>( | |
| pt: PointerType, partial: Partial<'a>, value: &Value, | |
| ) -> Result<Partial<'a>, GenericError> { | |
| debug_log(&partial, "apply_pointer_value_to_partial"); | |
| todo!() | |
| } | |
| fn get_shape_bitwidth(shape: &Shape) -> u32 { | |
| match shape.layout { | |
| ShapeLayout::Sized(layout) => u32::try_from(layout.size() * 8).unwrap_or(u32::MAX), | |
| ShapeLayout::Unsized => 0, | |
| } | |
| } | |
| fn set_partial_value<'a, V: Facet<'a>>(partial: Partial<'a>, value: V) -> Result<Partial<'a>, GenericError> { | |
| let path = partial.path(); | |
| partial | |
| .set(value) | |
| .with_error_context(|| format!("Failed to set value for {}.", path)) | |
| } | |
| fn type_mismatch(expected: &'static str, value: &Value) -> GenericError { | |
| generic_error!("Type mismatch: expected {}, got {}", expected, value_type(value)) | |
| } | |
| fn value_type(value: &Value) -> &'static str { | |
| match value.destructure_ref() { | |
| DestructuredRef::Null => "null", | |
| DestructuredRef::Bool(_) => "bool", | |
| DestructuredRef::Number(vnumber) => { | |
| if vnumber.is_float() { | |
| "float" | |
| } else if vnumber.to_u64().is_some() { | |
| "unsigned-int" | |
| } else { | |
| "signed-int" | |
| } | |
| } | |
| DestructuredRef::String(_) => "string", | |
| DestructuredRef::Bytes(_) => "bytes", | |
| DestructuredRef::Array(_) => "array", | |
| DestructuredRef::Object(_) => "object", | |
| DestructuredRef::DateTime(_) => "datetime", | |
| } | |
| } | |
| fn debug_log<'a, S>(partial: &Partial<'a>, message: S) | |
| where | |
| S: AsRef<str>, | |
| { | |
| let indent = partial.frame_count().saturating_sub(1); | |
| let indent_str = " ".repeat(indent); | |
| println!("{}{}", indent_str, message.as_ref()); | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use facet::Facet; | |
| use facet_value::{VObject, VString}; | |
| use super::*; | |
| #[track_caller] | |
| fn finalize_partial<'a, T: Facet<'a>>(partial: Partial<'a>) -> Result<T, GenericError> { | |
| partial.build()?.materialize().map_err(|e| e.into()) | |
| } | |
| #[test] | |
| fn test_apply_value_to_partial_primitive() { | |
| let partial = Partial::alloc::<bool>().unwrap(); | |
| let value = Value::TRUE; | |
| let partial = apply_value_to_partial(partial, &value).unwrap(); | |
| let final_value = finalize_partial::<bool>(partial).unwrap(); | |
| assert_eq!(final_value, true); | |
| } | |
| #[test] | |
| fn test_deserialize_basic() { | |
| #[derive(Facet)] | |
| struct Basic { | |
| flag: bool, | |
| api_key: String, | |
| } | |
| println!("Shape of `Basic`: {:#?}", Basic::SHAPE); | |
| let mut object = VObject::default(); | |
| object.insert("flag", Value::TRUE); | |
| object.insert("api_key", VString::from("123456")); | |
| let value = object.into_value(); | |
| let partial = Partial::alloc::<Basic>().unwrap(); | |
| let partial = apply_value_to_partial(partial, &value).unwrap(); | |
| let final_value = finalize_partial::<Basic>(partial).unwrap(); | |
| assert_eq!(final_value.flag, true); | |
| assert_eq!(final_value.api_key, "123456"); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| running 1 test | |
| test deser::tests::test_deserialize_basic ... FAILED | |
| successes: | |
| successes: | |
| failures: | |
| ---- deser::tests::test_deserialize_basic stdout ---- | |
| Shape of `Basic`: Shape { | |
| id: TypeId(0x4effc403bbd6aa3dd1d49011c085043a), | |
| layout: Sized(Layout { size: 32, align: 8 (1 << 3) }), | |
| vtable: ValueVTable { .. }, | |
| ty: User( | |
| Struct( | |
| StructType { | |
| repr: Repr { | |
| base: Rust, | |
| packed: false, | |
| }, | |
| kind: Struct, | |
| fields: [ | |
| Field { | |
| name: "flag", | |
| shape: 0x00005b2e767c2830, | |
| offset: 24, | |
| attributes: [], | |
| doc: [], | |
| }, | |
| Field { | |
| name: "api_key", | |
| shape: 0x00005b2e767c2be0, | |
| offset: 0, | |
| attributes: [], | |
| doc: [], | |
| }, | |
| ], | |
| }, | |
| ), | |
| ), | |
| def: Undefined, | |
| type_identifier: "Basic", | |
| type_params: [], | |
| doc: [], | |
| attributes: [], | |
| type_tag: None, | |
| inner: None, | |
| } | |
| apply_value_to_partial | |
| apply_user_value_to_partial | |
| apply_struct_value_to_partial | |
| apply_named_field_struct_value_to_partial | |
| handling field flag | |
| found value for field flag, applying... | |
| apply_value_to_partial | |
| apply_primitive_value_to_partial | |
| handling field api_key | |
| found value for field api_key, applying... | |
| apply_value_to_partial | |
| apply_user_value_to_partial | |
| arrived at opaque type: Shape { | |
| id: TypeId(0x652f329a49e7c67669df4a6e3ac0879a), | |
| layout: Sized(Layout { size: 24, align: 8 (1 << 3) }), | |
| vtable: ValueVTable { .. }, | |
| ty: User( | |
| Opaque, | |
| ), | |
| def: Scalar, | |
| type_identifier: "String", | |
| type_params: [], | |
| doc: [], | |
| attributes: [], | |
| type_tag: None, | |
| inner: None, | |
| } | |
| thread 'deser::tests::test_deserialize_basic' panicked at lib/saluki-config2/src/deser.rs:376:63: | |
| called `Result::unwrap()` on an `Err` value: Opaque types are not supported. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment