Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/libs/macros/src/functions/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ fn map_with_path(ty: &Type) -> Option<String> {
"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,
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/libs/utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
1 change: 1 addition & 0 deletions src/libs/utils/src/serializers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod bigint;
pub mod nat;
pub mod principal;
pub mod types;
pub mod uint8array;
128 changes: 128 additions & 0 deletions src/libs/utils/src/serializers/nat.rs
Original file line number Diff line number Diff line change
@@ -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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<D>(deserializer: D) -> Result<Self, D::Error>
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<V>(self, mut map: V) -> Result<JsonDataNat, V::Error>
where
V: MapAccess<'de>,
{
let mut value = None;
while let Some(key) = map.next_key::<String>()? {
if key == "__bigint__" {
if value.is_some() {
return Err(de::Error::duplicate_field("__bigint__"));
}
value = Some(map.next_value::<String>()?);
}
}
let value_str = value.ok_or_else(|| de::Error::missing_field("__bigint__"))?;
let nat_value = value_str
.parse::<u128>()
.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::<JsonDataNat>(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::<JsonDataNat>(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::<JsonDataNat>(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");
}
}
11 changes: 11 additions & 0 deletions src/libs/utils/src/serializers/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/libs/utils/src/with/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
50 changes: 50 additions & 0 deletions src/libs/utils/src/with/nat.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use crate::serializers::types::JsonDataNat;
use serde::{Deserialize, Deserializer, Serialize, Serializer};

pub fn serialize<S>(value: &u128, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
JsonDataNat { value: *value }.serialize(s)
}

pub fn deserialize<'de, D>(deserializer: D) -> Result<u128, D::Error>
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);
}
}
77 changes: 77 additions & 0 deletions src/libs/utils/src/with/nat_opt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
use crate::serializers::types::JsonDataNat;
use serde::{Deserialize, Deserializer, Serialize, Serializer};

pub fn serialize<S>(value: &Option<u128>, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match value {
Some(v) => JsonDataNat { value: *v }.serialize(s),
None => s.serialize_none(),
}
}

pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<u128>, D::Error>
where
D: Deserializer<'de>,
{
Option::<JsonDataNat>::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<u128>,
}

#[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);
}
}
Loading