Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added serde feature #413

Merged
merged 1 commit into from
Dec 17, 2024
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
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);
}
Loading