Skip to content

Commit

Permalink
Added serde feature
Browse files Browse the repository at this point in the history
  • Loading branch information
d-sonuga committed Dec 11, 2024
1 parent 237ceed commit 1d3a99d
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 0 deletions.
7 changes: 7 additions & 0 deletions piecrust-uplink/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add serde `Serialize` and `Deserialize` implementations for `ContractId` and `Event` [#414]
- Add `serde`, `hex`, `base64` and `serde_json` optional dependencies [#414]
- Add `serde` feature [#414]

## [0.17.1] - 2024-09-24

### Changed
Expand Down Expand Up @@ -216,6 +222,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- First `piecrust-uplink` release

<!-- ISSUES -->
[#414]: https://github.com/dusk-network/piecrust/issues/414
[#375]: https://github.com/dusk-network/piecrust/issues/375
[#365]: https://github.com/dusk-network/piecrust/issues/365
[#357]: https://github.com/dusk-network/piecrust/issues/357
Expand Down
8 changes: 8 additions & 0 deletions piecrust-uplink/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,18 @@ license = "MPL-2.0"
rkyv = { version = "0.7", default-features = false, features = ["size_32", "alloc", "validation"] }
bytecheck = { version = "0.6", default-features = false }
dlmalloc = { version = "0.2", optional = true, features = ["global"] }
serde = { version = "1.0", optional = true }
hex = { version = "0.4" , optional = true }
base64 = { version = "0.22", optional = true }
serde_json = { version = "1.0", optional = true }

[dev-dependencies]
rand = "0.8"

[features]
abi = []
debug = []
serde = ["dep:serde", "serde_json", "hex", "base64"]

[package.metadata.docs.rs]
all-features = true
Expand Down
3 changes: 3 additions & 0 deletions piecrust-uplink/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ pub use types::*;
mod error;
pub use error::*;

#[cfg(feature = "serde")]
mod serde_support;

/// How many bytes to use for scratch space when serializing
pub const SCRATCH_BUF_BYTES: usize = 1024;

Expand Down
140 changes: 140 additions & 0 deletions piecrust-uplink/src/serde_support.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) DUSK NETWORK. All rights reserved.

use alloc::format;
use alloc::string::String;

use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use base64::Engine;
use serde::de::{Error as SerdeError, MapAccess, Visitor};
use serde::ser::SerializeStruct;
use serde::{Deserialize, Deserializer, Serialize, Serializer};

use crate::{ContractId, Event, CONTRACT_ID_BYTES};

impl Serialize for ContractId {
fn serialize<S: Serializer>(
&self,
serializer: S,
) -> Result<S::Ok, S::Error> {
let s = hex::encode(self.to_bytes());
serializer.serialize_str(&s)
}
}

impl<'de> Deserialize<'de> for ContractId {
fn deserialize<D: Deserializer<'de>>(
deserializer: D,
) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
let decoded = hex::decode(&s).map_err(SerdeError::custom)?;
let decoded_len = decoded.len();
let byte_length_str = format!("{CONTRACT_ID_BYTES}");
let bytes: [u8; CONTRACT_ID_BYTES] =
decoded.try_into().map_err(|_| {
SerdeError::invalid_length(
decoded_len,
&byte_length_str.as_str(),
)
})?;
Ok(bytes.into())
}
}

impl Serialize for Event {
fn serialize<S: Serializer>(
&self,
serializer: S,
) -> Result<S::Ok, S::Error> {
let mut struct_ser = serializer.serialize_struct("Event", 3)?;
struct_ser.serialize_field("source", &self.source)?;
struct_ser.serialize_field("topic", &self.topic)?;
struct_ser
.serialize_field("data", &BASE64_STANDARD.encode(&self.data))?;
struct_ser.end()
}
}

impl<'de> Deserialize<'de> for Event {
fn deserialize<D: Deserializer<'de>>(
deserializer: D,
) -> Result<Self, D::Error> {
struct StructVisitor;

impl<'de> Visitor<'de> for StructVisitor {
type Value = Event;

fn expecting(
&self,
formatter: &mut alloc::fmt::Formatter,
) -> alloc::fmt::Result {
formatter
.write_str("a struct with fields: source, topic, and data")
}

fn visit_map<A: MapAccess<'de>>(
self,
mut map: A,
) -> Result<Self::Value, A::Error> {
let (mut source, mut topic, mut data) = (None, None, None);
while let Some(key) = map.next_key()? {
match key {
"source" => {
if source.is_some() {
return Err(SerdeError::duplicate_field(
"source",
));
}
source = Some(map.next_value()?);
}
"topic" => {
if topic.is_some() {
return Err(SerdeError::duplicate_field(
"topic",
));
}
topic = Some(map.next_value()?);
}
"data" => {
if data.is_some() {
return Err(SerdeError::duplicate_field(
"data",
));
}
data = Some(map.next_value()?);
}
field => {
return Err(SerdeError::unknown_field(
field,
&["source", "topic", "data"],
))
}
};
}
let data: String =
data.ok_or_else(|| SerdeError::missing_field("data"))?;
let data = BASE64_STANDARD.decode(data).map_err(|e| {
SerdeError::custom(format!(
"failed to base64 decode Event data: {e}"
))
})?;
Ok(Event {
source: source
.ok_or_else(|| SerdeError::missing_field("source"))?,
topic: topic
.ok_or_else(|| SerdeError::missing_field("topic"))?,
data,
})
}
}

deserializer.deserialize_struct(
"Event",
&["source", "topic", "data"],
StructVisitor,
)
}
}
85 changes: 85 additions & 0 deletions piecrust-uplink/tests/serde.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) DUSK NETWORK. All rights reserved.

#![cfg(feature = "serde")]

use piecrust_uplink::{ContractId, Event, CONTRACT_ID_BYTES};
use rand::rngs::StdRng;
use rand::{RngCore, SeedableRng};

fn rand_contract_id(rng: &mut StdRng) -> ContractId {
let mut bytes = [0; CONTRACT_ID_BYTES];
rng.fill_bytes(&mut bytes);
bytes.into()
}

fn rand_event(rng: &mut StdRng) -> Event {
let mut data = [0; 50];
rng.fill_bytes(&mut data);
Event {
source: rand_contract_id(rng),
topic: "a-contract-topic".into(),
data: data.into(),
}
}

#[test]
fn serde_contract_id() {
let mut rng = StdRng::seed_from_u64(0xdead);
let id: ContractId = rand_contract_id(&mut rng);
let ser = serde_json::to_string(&id).unwrap();
let deser: ContractId = serde_json::from_str(&ser).unwrap();
assert_eq!(id, deser);
}

#[test]
fn serde_event() {
let mut rng = StdRng::seed_from_u64(0xbeef);
let event = rand_event(&mut rng);
let ser = serde_json::to_string(&event).unwrap();
let deser: Event = serde_json::from_str(&ser).unwrap();
assert_eq!(event, deser);
}

#[test]
fn serde_wrong_encoded() {
let wrong_encoded = "wrong-encoded";

let contract_id: Result<ContractId, _> =
serde_json::from_str(&wrong_encoded);
assert!(contract_id.is_err());
}

#[test]
fn serde_too_long_encoded() {
let length_33_enc = "\"e4ab9de40283a85d6ea0cd0120500697d8b01c71b7b4b520292252d20937000631\"";

let contract_id: Result<ContractId, _> =
serde_json::from_str(&length_33_enc);
assert!(contract_id.is_err());
}

#[test]
fn serde_too_short_encoded() {
let length_31_enc =
"\"1751c37a1dca7aa4c048fcc6177194243edc3637bae042e167e4285945e046\"";

let contract_id: Result<ContractId, _> =
serde_json::from_str(&length_31_enc);
assert!(contract_id.is_err());
}

#[test]
fn serde_event_fields() {
let serde_json_string = "{\"source\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"topic\":\"\",\"data\":\"\"}";
let event = Event {
source: ContractId::from_bytes([0; CONTRACT_ID_BYTES]),
topic: String::new(),
data: Vec::new(),
};
let ser = serde_json::to_string(&event).unwrap();
assert_eq!(serde_json_string, ser);
}

0 comments on commit 1d3a99d

Please sign in to comment.