From f23b7d14089e4c5590f9236cf76b7eb354a71128 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Sun, 29 Mar 2026 22:04:12 +0200 Subject: [PATCH] feat(utils,macros): support for nat --- src/libs/macros/src/functions/derive.rs | 1 + src/libs/utils/src/lib.rs | 4 +- src/libs/utils/src/serializers/mod.rs | 1 + src/libs/utils/src/serializers/nat.rs | 128 ++++++++++++++++++++++++ src/libs/utils/src/serializers/types.rs | 11 ++ src/libs/utils/src/with/mod.rs | 2 + src/libs/utils/src/with/nat.rs | 50 +++++++++ src/libs/utils/src/with/nat_opt.rs | 77 ++++++++++++++ 8 files changed, 272 insertions(+), 2 deletions(-) create mode 100644 src/libs/utils/src/serializers/nat.rs create mode 100644 src/libs/utils/src/with/nat.rs create mode 100644 src/libs/utils/src/with/nat_opt.rs diff --git a/src/libs/macros/src/functions/derive.rs b/src/libs/macros/src/functions/derive.rs index b4ed0ac65c..6f182e0bca 100644 --- a/src/libs/macros/src/functions/derive.rs +++ b/src/libs/macros/src/functions/derive.rs @@ -369,6 +369,7 @@ fn map_with_path(ty: &Type) -> Option { "Principal" | "candid :: Principal" => Some("junobuild_utils::with::principal".to_string()), "Vec < u8 >" => Some("junobuild_utils::with::uint8array".to_string()), "u64" => Some("junobuild_utils::with::bigint".to_string()), + "u128" => Some("junobuild_utils::with::nat".to_string()), _ => None, } } diff --git a/src/libs/utils/src/lib.rs b/src/libs/utils/src/lib.rs index 7a81357037..93241da771 100644 --- a/src/libs/utils/src/lib.rs +++ b/src/libs/utils/src/lib.rs @@ -10,7 +10,7 @@ pub use doc::*; pub use json::*; pub use crate::serializers::types::{ - DocDataBigInt, DocDataPrincipal, DocDataUint8Array, JsonDataBigInt, JsonDataPrincipal, - JsonDataUint8Array, + DocDataBigInt, DocDataPrincipal, DocDataUint8Array, JsonDataBigInt, JsonDataNat, + JsonDataPrincipal, JsonDataUint8Array, }; pub use crate::types::{FromJsonData, IntoJsonData}; diff --git a/src/libs/utils/src/serializers/mod.rs b/src/libs/utils/src/serializers/mod.rs index 5edd82073a..3558269e97 100644 --- a/src/libs/utils/src/serializers/mod.rs +++ b/src/libs/utils/src/serializers/mod.rs @@ -1,4 +1,5 @@ pub mod bigint; +pub mod nat; pub mod principal; pub mod types; pub mod uint8array; diff --git a/src/libs/utils/src/serializers/nat.rs b/src/libs/utils/src/serializers/nat.rs new file mode 100644 index 0000000000..44ef82e54f --- /dev/null +++ b/src/libs/utils/src/serializers/nat.rs @@ -0,0 +1,128 @@ +use crate::serializers::types::JsonDataNat; +use serde::de::{self, MapAccess, Visitor}; +use serde::ser::SerializeStruct; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::fmt; + +impl fmt::Display for JsonDataNat { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.value) + } +} + +impl Serialize for JsonDataNat { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("DocDataNat", 1)?; + state.serialize_field("__bigint__", &self.value.to_string())?; + state.end() + } +} + +impl<'de> Deserialize<'de> for JsonDataNat { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_struct("DocDataNat", &["__bigint__"], DocDataNatVisitor) + } +} + +struct DocDataNatVisitor; + +impl<'de> Visitor<'de> for DocDataNatVisitor { + type Value = JsonDataNat; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an object with a key __bigint__") + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let mut value = None; + while let Some(key) = map.next_key::()? { + if key == "__bigint__" { + if value.is_some() { + return Err(de::Error::duplicate_field("__bigint__")); + } + value = Some(map.next_value::()?); + } + } + let value_str = value.ok_or_else(|| de::Error::missing_field("__bigint__"))?; + let nat_value = value_str + .parse::() + .map_err(|_| de::Error::custom("Invalid format for __bigint__"))?; + Ok(JsonDataNat { value: nat_value }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json; + + #[test] + fn serialize_doc_data_nat() { + let data = JsonDataNat { + value: 12345678901234, + }; + let s = serde_json::to_string(&data).expect("serialize"); + assert_eq!(s, r#"{"__bigint__":"12345678901234"}"#); + } + + #[test] + fn deserialize_doc_data_nat() { + let s = r#"{"__bigint__":"12345678901234"}"#; + let data: JsonDataNat = serde_json::from_str(s).expect("deserialize"); + assert_eq!(data.value, 12345678901234); + } + + #[test] + fn round_trip() { + let original = JsonDataNat { value: u128::MAX }; + let json = serde_json::to_string(&original).unwrap(); + let decoded: JsonDataNat = serde_json::from_str(&json).unwrap(); + assert_eq!(decoded.value, original.value); + } + + #[test] + fn error_on_missing_field() { + let err = serde_json::from_str::(r#"{}"#).unwrap_err(); + assert!( + err.to_string().contains("missing field `__bigint__`"), + "got: {err}" + ); + } + + #[test] + fn error_on_duplicate_field() { + let s = r#"{"__bigint__":"123","__bigint__":"456"}"#; + let err = serde_json::from_str::(s).unwrap_err(); + assert!( + err.to_string().contains("duplicate field `__bigint__`"), + "got: {err}" + ); + } + + #[test] + fn error_on_invalid_nat_format() { + let s = r#"{"__bigint__":"not-a-number"}"#; + let err = serde_json::from_str::(s).unwrap_err(); + assert!( + err.to_string().contains("Invalid format for __bigint__"), + "got: {err}" + ); + } + + #[test] + fn test_display_implementation() { + let data = JsonDataNat { + value: 12345678901234, + }; + assert_eq!(format!("{}", data), "12345678901234"); + } +} diff --git a/src/libs/utils/src/serializers/types.rs b/src/libs/utils/src/serializers/types.rs index 6c55d3fcba..0fa2707a19 100644 --- a/src/libs/utils/src/serializers/types.rs +++ b/src/libs/utils/src/serializers/types.rs @@ -22,6 +22,17 @@ pub struct JsonDataBigInt { pub value: u64, } +/// Represents an arbitrary precision natural number for document data, +/// mirroring Candid's `nat` type. Useful for values that exceed `u64::MAX` +/// or must be compatible with Candid's `nat` type. +/// +/// # Fields +/// - `value`: A `u128` integer representing the natural number. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct JsonDataNat { + pub value: u128, +} + /// Represents a byte array value for document data, mirroring JavaScript's `Uint8Array`. /// /// This struct is useful for transporting raw binary data across the JSON boundary, diff --git a/src/libs/utils/src/with/mod.rs b/src/libs/utils/src/with/mod.rs index 8a19826878..86b2b49dca 100644 --- a/src/libs/utils/src/with/mod.rs +++ b/src/libs/utils/src/with/mod.rs @@ -1,5 +1,7 @@ pub mod bigint; pub mod bigint_opt; +pub mod nat; +pub mod nat_opt; pub mod principal; pub mod principal_opt; pub mod uint8array; diff --git a/src/libs/utils/src/with/nat.rs b/src/libs/utils/src/with/nat.rs new file mode 100644 index 0000000000..7c1d2262f3 --- /dev/null +++ b/src/libs/utils/src/with/nat.rs @@ -0,0 +1,50 @@ +use crate::serializers::types::JsonDataNat; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +pub fn serialize(value: &u128, s: S) -> Result +where + S: Serializer, +{ + JsonDataNat { value: *value }.serialize(s) +} + +pub fn deserialize<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + JsonDataNat::deserialize(deserializer).map(|d| d.value) +} + +#[cfg(test)] +mod tests { + use serde::{Deserialize, Serialize}; + use serde_json; + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct TestStruct { + #[serde(with = "crate::with::nat")] + value: u128, + } + + #[test] + fn serialize_nat() { + let s = TestStruct { value: 42 }; + let json = serde_json::to_string(&s).expect("serialize"); + assert_eq!(json, r#"{"value":{"__bigint__":"42"}}"#); + } + + #[test] + fn deserialize_nat() { + let json = r#"{"value":{"__bigint__":"42"}}"#; + let s: TestStruct = serde_json::from_str(json).expect("deserialize"); + assert_eq!(s.value, 42); + } + + #[test] + fn round_trip() { + let original = TestStruct { value: u128::MAX }; + let json = serde_json::to_string(&original).unwrap(); + let decoded: TestStruct = serde_json::from_str(&json).unwrap(); + assert_eq!(decoded.value, original.value); + } +} diff --git a/src/libs/utils/src/with/nat_opt.rs b/src/libs/utils/src/with/nat_opt.rs new file mode 100644 index 0000000000..464fc1a0a9 --- /dev/null +++ b/src/libs/utils/src/with/nat_opt.rs @@ -0,0 +1,77 @@ +use crate::serializers::types::JsonDataNat; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +pub fn serialize(value: &Option, s: S) -> Result +where + S: Serializer, +{ + match value { + Some(v) => JsonDataNat { value: *v }.serialize(s), + None => s.serialize_none(), + } +} + +pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + Option::::deserialize(deserializer).map(|opt| opt.map(|d| d.value)) +} + +#[cfg(test)] +mod tests { + use serde::{Deserialize, Serialize}; + use serde_json; + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct TestStruct { + #[serde(with = "super")] + value: Option, + } + + #[test] + fn serialize_some() { + let s = TestStruct { value: Some(42) }; + let json = serde_json::to_string(&s).expect("serialize"); + assert_eq!(json, r#"{"value":{"__bigint__":"42"}}"#); + } + + #[test] + fn serialize_none() { + let s = TestStruct { value: None }; + let json = serde_json::to_string(&s).expect("serialize"); + assert_eq!(json, r#"{"value":null}"#); + } + + #[test] + fn deserialize_some() { + let json = r#"{"value":{"__bigint__":"42"}}"#; + let s: TestStruct = serde_json::from_str(json).expect("deserialize"); + assert_eq!(s.value, Some(42)); + } + + #[test] + fn deserialize_none() { + let json = r#"{"value":null}"#; + let s: TestStruct = serde_json::from_str(json).expect("deserialize"); + assert_eq!(s.value, None); + } + + #[test] + fn round_trip_some() { + let original = TestStruct { + value: Some(u128::MAX), + }; + let json = serde_json::to_string(&original).unwrap(); + let decoded: TestStruct = serde_json::from_str(&json).unwrap(); + assert_eq!(decoded.value, original.value); + } + + #[test] + fn round_trip_none() { + let original = TestStruct { value: None }; + let json = serde_json::to_string(&original).unwrap(); + let decoded: TestStruct = serde_json::from_str(&json).unwrap(); + assert_eq!(decoded.value, original.value); + } +}