diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2a1c7f..b728a4b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,4 +38,6 @@ jobs: timeout_minutes: 20 max_attempts: 3 retry_wait_seconds: 30 - command: cargo test --workspace --all-features + command: | + cargo test --workspace --all-features + cargo run --example structs --all-features diff --git a/Cargo.lock b/Cargo.lock index 5e14b3c..4d17b9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -240,6 +240,7 @@ dependencies = [ "clap", "clap_complete", "convert_case", + "paste", "serde", "serde_json", "starknet", @@ -1215,6 +1216,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pbkdf2" version = "0.11.0" diff --git a/Cargo.toml b/Cargo.toml index 602adac..49e021f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,9 @@ tracing-subscriber.workspace = true url.workspace = true tokio = { version = "1.40", features = ["full"], optional = true } +[dev-dependencies] +paste = "1.0" + [features] default = [] abigen-rs = ["cainome-rs-macro"] diff --git a/crates/cairo-serde/src/lib.rs b/crates/cairo-serde/src/lib.rs index 333d245..35915c8 100644 --- a/crates/cairo-serde/src/lib.rs +++ b/crates/cairo-serde/src/lib.rs @@ -10,8 +10,10 @@ mod error; pub use error::{Error, Result}; pub mod call; +pub mod serde_hex; pub mod types; -use serde::ser::SerializeSeq; + +pub use serde_hex::*; pub use types::array_legacy::*; pub use types::byte_array::*; pub use types::non_zero::*; @@ -52,62 +54,3 @@ pub trait CairoSerde { /// Deserializes an array of felts into the given type. fn cairo_deserialize(felts: &[Felt], offset: usize) -> Result; } - -/// Serialize a value as a hex string. -pub fn serialize_as_hex(value: &T, serializer: S) -> std::result::Result -where - S: serde::Serializer, - T: serde::Serialize + std::fmt::LowerHex, -{ - serializer.serialize_str(&format!("{:#x}", value)) -} - -/// Serialize a vector of values as a hex string. -pub fn serialize_as_hex_vec( - value: &Vec, - serializer: S, -) -> std::result::Result -where - S: serde::Serializer, - T: serde::Serialize + std::fmt::LowerHex, -{ - let mut seq = serializer.serialize_seq(Some(value.len()))?; - for v in value { - seq.serialize_element(&format!("{:#x}", v))?; - } - seq.end() -} - -/// Serialize a tuple of two values as a hex string. -pub fn serialize_as_hex_t2( - value: &(T1, T2), - serializer: S, -) -> std::result::Result -where - S: serde::Serializer, - T1: serde::Serialize + std::fmt::LowerHex, - T2: serde::Serialize + std::fmt::LowerHex, -{ - let mut seq = serializer.serialize_seq(Some(2))?; - seq.serialize_element(&format!("{:#x}", value.0))?; - seq.serialize_element(&format!("{:#x}", value.1))?; - seq.end() -} - -/// Serialize a tuple of three values as a hex string. -pub fn serialize_as_hex_t3( - value: &(T1, T2, T3), - serializer: S, -) -> std::result::Result -where - S: serde::Serializer, - T1: serde::Serialize + std::fmt::LowerHex, - T2: serde::Serialize + std::fmt::LowerHex, - T3: serde::Serialize + std::fmt::LowerHex, -{ - let mut seq = serializer.serialize_seq(Some(2))?; - seq.serialize_element(&format!("{:#x}", value.0))?; - seq.serialize_element(&format!("{:#x}", value.1))?; - seq.serialize_element(&format!("{:#x}", value.2))?; - seq.end() -} diff --git a/crates/cairo-serde/src/serde_hex.rs b/crates/cairo-serde/src/serde_hex.rs new file mode 100644 index 0000000..81bb07d --- /dev/null +++ b/crates/cairo-serde/src/serde_hex.rs @@ -0,0 +1,180 @@ +use serde::ser::SerializeSeq; +use std::num::ParseIntError; + +pub trait FromStrHexOrDec: Sized { + fn from_str_hex_or_dec(s: &str) -> Result; +} + +impl FromStrHexOrDec for u64 { + fn from_str_hex_or_dec(s: &str) -> Result { + if s.starts_with("0x") || s.starts_with("0X") { + u64::from_str_radix(&s[2..], 16) + } else { + s.parse::() + } + } +} + +impl FromStrHexOrDec for u128 { + fn from_str_hex_or_dec(s: &str) -> Result { + if s.starts_with("0x") || s.starts_with("0X") { + u128::from_str_radix(&s[2..], 16) + } else { + s.parse::() + } + } +} + +impl FromStrHexOrDec for i64 { + fn from_str_hex_or_dec(s: &str) -> Result { + u64::from_str_hex_or_dec(s).map(|v| v as i64) + } +} + +impl FromStrHexOrDec for i128 { + fn from_str_hex_or_dec(s: &str) -> Result { + u128::from_str_hex_or_dec(s).map(|v| v as i128) + } +} + +/// Serialize a value as a hex string. +pub fn serialize_as_hex(value: &T, serializer: S) -> std::result::Result +where + S: serde::Serializer, + T: serde::Serialize + std::fmt::LowerHex, +{ + serializer.serialize_str(&format!("{:#x}", value)) +} + +/// Serialize a vector of values as a hex string. +pub fn serialize_as_hex_vec( + value: &Vec, + serializer: S, +) -> std::result::Result +where + S: serde::Serializer, + T: serde::Serialize + std::fmt::LowerHex, +{ + let mut seq = serializer.serialize_seq(Some(value.len()))?; + for v in value { + seq.serialize_element(&format!("{:#x}", v))?; + } + seq.end() +} + +/// Serialize a tuple of two values as a hex string. +pub fn serialize_as_hex_t2( + value: &(T1, T2), + serializer: S, +) -> std::result::Result +where + S: serde::Serializer, + T1: serde::Serialize + std::fmt::LowerHex, + T2: serde::Serialize + std::fmt::LowerHex, +{ + let mut seq = serializer.serialize_seq(Some(2))?; + seq.serialize_element(&format!("{:#x}", value.0))?; + seq.serialize_element(&format!("{:#x}", value.1))?; + seq.end() +} + +/// Serialize a tuple of three values as a hex string. +pub fn serialize_as_hex_t3( + value: &(T1, T2, T3), + serializer: S, +) -> std::result::Result +where + S: serde::Serializer, + T1: serde::Serialize + std::fmt::LowerHex, + T2: serde::Serialize + std::fmt::LowerHex, + T3: serde::Serialize + std::fmt::LowerHex, +{ + let mut seq = serializer.serialize_seq(Some(2))?; + seq.serialize_element(&format!("{:#x}", value.0))?; + seq.serialize_element(&format!("{:#x}", value.1))?; + seq.serialize_element(&format!("{:#x}", value.2))?; + seq.end() +} + +/// Deserialize a single hex string into a value. +pub fn deserialize_from_hex<'de, D, T>(deserializer: D) -> std::result::Result +where + D: serde::Deserializer<'de>, + T: serde::Deserialize<'de> + FromStrHexOrDec, +{ + let hex_string: String = serde::Deserialize::deserialize(deserializer)?; + T::from_str_hex_or_dec(&hex_string).map_err(serde::de::Error::custom) +} + +/// Deserialize a vector of hex strings into values. +pub fn deserialize_from_hex_vec<'de, D, T>(deserializer: D) -> std::result::Result, D::Error> +where + D: serde::Deserializer<'de>, + T: serde::Deserialize<'de> + FromStrHexOrDec, +{ + let hex_strings: Vec = serde::Deserialize::deserialize(deserializer)?; + hex_strings + .into_iter() + .map(|s| T::from_str_hex_or_dec(&s).map_err(serde::de::Error::custom)) + .collect() +} + +/// Deserialize a string into a value, trying first to use `from_str` default. +/// If it fails, tries to parse as a hex string. +macro_rules! deserialize_hex { + ($hex_string:expr, $type:ty) => { + <$type>::from_str($hex_string).or_else(|_| { + let hex_string = $hex_string.trim_start_matches("0x"); + u128::from_str_radix(hex_string, 16) + .map(|num| num.to_string()) + .or_else(|_| Ok(hex_string.to_string())) + .and_then(|dec_string| { + <$type>::from_str(&dec_string).map_err(serde::de::Error::custom) + }) + }) + }; +} + +/// Deserialize a tuple of two hex strings into values. +/// For tuples, we can't enforce all the elements to implement `FromStrHexOrDec` +/// in this naive implementation. +pub fn deserialize_from_hex_t2<'de, D, T1, T2>( + deserializer: D, +) -> std::result::Result<(T1, T2), D::Error> +where + D: serde::Deserializer<'de>, + T1: serde::Deserialize<'de> + std::str::FromStr, + T2: serde::Deserialize<'de> + std::str::FromStr, + ::Err: std::fmt::Display, + ::Err: std::fmt::Display, +{ + let hex_strings: (String, String) = serde::Deserialize::deserialize(deserializer)?; + + let v1 = deserialize_hex!(&hex_strings.0, T1)?; + let v2 = deserialize_hex!(&hex_strings.1, T2)?; + + Ok((v1, v2)) +} + +/// Deserialize a tuple of three hex strings into values. +/// For tuples, we can't enforce all the elements to implement `FromStrHexOrDec` +/// in this naive implementation. +pub fn deserialize_from_hex_t3<'de, D, T1, T2, T3>( + deserializer: D, +) -> std::result::Result<(T1, T2, T3), D::Error> +where + D: serde::Deserializer<'de>, + T1: serde::Deserialize<'de> + std::str::FromStr, + T2: serde::Deserialize<'de> + std::str::FromStr, + T3: serde::Deserialize<'de> + std::str::FromStr, + ::Err: std::fmt::Display, + ::Err: std::fmt::Display, + ::Err: std::fmt::Display, +{ + let hex_strings: (String, String, String) = serde::Deserialize::deserialize(deserializer)?; + + let v1 = deserialize_hex!(&hex_strings.0, T1)?; + let v2 = deserialize_hex!(&hex_strings.1, T2)?; + let v3 = deserialize_hex!(&hex_strings.2, T3)?; + Ok((v1, v2, v3)) +} diff --git a/crates/rs/src/expand/utils.rs b/crates/rs/src/expand/utils.rs index 5400aca..c7e2be2 100644 --- a/crates/rs/src/expand/utils.rs +++ b/crates/rs/src/expand/utils.rs @@ -97,21 +97,26 @@ pub fn serde_hex_derive(ty: &str) -> TokenStream2 { let serde_tuple_2 = format!("{}::serialize_as_hex_t2", cainome_cairo_serde_path()); let serde_tuple_3 = format!("{}::serialize_as_hex_t3", cainome_cairo_serde_path()); + let deser_single = format!("{}::deserialize_from_hex", cainome_cairo_serde_path()); + let deser_vec = format!("{}::deserialize_from_hex_vec", cainome_cairo_serde_path()); + let deser_tuple_2 = format!("{}::deserialize_from_hex_t2", cainome_cairo_serde_path()); + let deser_tuple_3 = format!("{}::deserialize_from_hex_t3", cainome_cairo_serde_path()); + let serde_hex = is_serde_hex_int(ty); match serde_hex { SerdeHexType::None => quote!(), SerdeHexType::Single => quote! { - #[serde(serialize_with = #serde_single)] + #[serde(serialize_with = #serde_single, deserialize_with = #deser_single)] }, SerdeHexType::Tuple(2) => quote! { - #[serde(serialize_with = #serde_tuple_2)] + #[serde(serialize_with = #serde_tuple_2, deserialize_with = #deser_tuple_2)] }, SerdeHexType::Tuple(3) => quote! { - #[serde(serialize_with = #serde_tuple_3)] + #[serde(serialize_with = #serde_tuple_3, deserialize_with = #deser_tuple_3)] }, SerdeHexType::Vec => quote! { - #[serde(serialize_with = #serde_vec)] + #[serde(serialize_with = #serde_vec, deserialize_with = #deser_vec)] }, _ => panic!("Unsupported type {} for serde_hex", ty), } diff --git a/examples/structs.rs b/examples/structs.rs index feb5fb6..815bd4f 100644 --- a/examples/structs.rs +++ b/examples/structs.rs @@ -1,12 +1,30 @@ use cainome::rs::abigen; +use paste::paste; use starknet::core::types::Felt; abigen!( MyContract, "./contracts/abi/gen.abi.json", - derives(Debug, Clone, serde::Serialize) + derives( + Debug, + Clone, + PartialEq, + serde::Serialize, + serde::Deserialize + ) ); +/// Uses paste since `concat_ident` is not available for stable Rust yet. +macro_rules! test_enum { + ($name:ident, $variant:expr) => { + paste! { + let $name = $variant; + let [<$name _deser>] = serde_json::from_str(&serde_json::to_string(&$name).unwrap()).unwrap(); + assert_eq!($name, [<$name _deser>]); + } + }; +} + #[tokio::main] async fn main() { let s = PlainStruct { @@ -21,40 +39,24 @@ async fn main() { f9: vec![1_u128, 2_u128], }; - println!("{}", serde_json::to_string(&s).unwrap()); - - let _s2 = s.clone(); - - let e = MyEnum::One(1_u8); - println!("{}", serde_json::to_string(&e).unwrap()); - - let e = MyEnum::Two(1_u16); - println!("{}", serde_json::to_string(&e).unwrap()); + let s_str = serde_json::to_string(&s).unwrap(); + println!("{}", s_str); - let e = MyEnum::Three(1_u32); - println!("{}", serde_json::to_string(&e).unwrap()); + let s_deser = serde_json::from_str(&s_str).unwrap(); + assert_eq!(s, s_deser); + println!("{:?}", s_deser); - let e = MyEnum::Four(1_u64); - println!("{}", serde_json::to_string(&e).unwrap()); - - let e = MyEnum::Five(1_u128); - println!("{}", serde_json::to_string(&e).unwrap()); - - let e = MyEnum::Six(Felt::from(6)); - println!("{}", serde_json::to_string(&e).unwrap()); - - let e = MyEnum::Seven(-1_i32); - println!("{}", serde_json::to_string(&e).unwrap()); - - let e = MyEnum::Eight(-1_i64); - println!("{}", serde_json::to_string(&e).unwrap()); - - let e = MyEnum::Nine(-1_i128); - println!("{}", serde_json::to_string(&e).unwrap()); - - let e = MyEnum::Ten((1_u8, 1_u128)); - println!("{}", serde_json::to_string(&e).unwrap()); + let _s2 = s.clone(); - let e = MyEnum::Eleven((Felt::from(1), 1_u8, 1_u128)); - println!("{}", serde_json::to_string(&e).unwrap()); + test_enum!(e1, MyEnum::One(1_u8)); + test_enum!(e2, MyEnum::Two(1_u16)); + test_enum!(e3, MyEnum::Three(1_u32)); + test_enum!(e4, MyEnum::Four(1_u64)); + test_enum!(e5, MyEnum::Five(1_u128)); + test_enum!(e6, MyEnum::Six(Felt::from(6))); + test_enum!(e7, MyEnum::Seven(-1_i32)); + test_enum!(e8, MyEnum::Eight(-1_i64)); + test_enum!(e9, MyEnum::Nine(-1_i128)); + test_enum!(e10, MyEnum::Ten((1_u8, 1_u128))); + test_enum!(e11, MyEnum::Eleven((Felt::from(1), 1_u8, 1_u128))); } diff --git a/scripts/test_all.sh b/scripts/test_all.sh index 274eecd..18df47c 100644 --- a/scripts/test_all.sh +++ b/scripts/test_all.sh @@ -1 +1,6 @@ cargo test --workspace --all-features + +# Somes examples are currently containing some generated +# code to test the serde implementation. +# TODO: this should be moved to the test suite. +cargo run --example structs --all-features