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

feat: borsh and json schemas support #43

Merged
merged 11 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from 9 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
15 changes: 15 additions & 0 deletions .github/workflows/test-borsh.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: test borsh
run-name: ${{ github.actor }}'s patch
on: [push]
jobs:
build-and-test:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache: true
toolchain: nightly
- run: |
cargo test --no-default-features --features=borsh
cargo test --no-default-features --features=borsh,std
17 changes: 7 additions & 10 deletions .github/workflows/test-serde.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,12 @@ jobs:
build-and-test:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '14'
- uses: actions-rs/toolchain@v1
- uses: actions/checkout@v4
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache: true
toolchain: nightly
override: true
- uses: actions-rs/cargo@v1
with:
command: test
args: --no-default-features --features serde
- run: |
cargo test --no-default-features --features=serde
cargo test --no-default-features --features=serde,std
cargo test --no-default-features --features=schemars
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,16 @@ defmt = ["dep:defmt"]
# Supports serde
serde = ["dep:serde"]

borsh = ["dep:borsh"]

schemars = ["dep:schemars", "std"]

[dependencies]
num-traits = { version = "0.2.17", default-features = false, optional = true }
defmt = { version = "0.3.5", optional = true }
serde = { version = "1.0", optional = true, default-features = false}
borsh = { version = "1.5.1", optional = true, features = ["unstable__schema"], default-features = false }
schemars = { version = "0.8.1", optional = true, features = ["derive"], default-features = false }

[dev-dependencies]
serde_test = "1.0"
1 change: 1 addition & 0 deletions rust-toolchain
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
stable
111 changes: 111 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
)]
#![cfg_attr(feature = "step_trait", feature(step_trait))]

#[cfg(all(feature = "borsh", not(feature = "std")))]
extern crate alloc;

use core::fmt::{Binary, Debug, Display, Formatter, LowerHex, Octal, UpperHex};
use core::hash::{Hash, Hasher};
#[cfg(feature = "step_trait")]
Expand All @@ -18,6 +21,18 @@ use core::ops::{
#[cfg(feature = "serde")]
use serde::{Deserialize, Deserializer, Serialize, Serializer};

#[cfg(feature = "borsh")]
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};

#[cfg(all(feature = "borsh", not(feature = "std")))]
use alloc::{collections::BTreeMap, string::ToString};

#[cfg(all(feature = "borsh", feature = "std"))]
use std::{collections::BTreeMap, string::ToString};

#[cfg(feature = "schemars")]
use schemars::JsonSchema;

#[derive(Debug, Clone, Eq, PartialEq)]
pub struct TryNewError;

Expand Down Expand Up @@ -1054,6 +1069,76 @@ where
}
}

// Borsh is byte-size little-endian de-needs-external-schema no-bit-compression serde.
// Current ser/de for it is not optimal impl because const math is not stable nor primitives has bits traits.
// Uses minimal amount of bytes to fit needed amount of bits without compression (borsh does not have it anyway).
#[cfg(feature = "borsh")]
impl<T, const BITS: usize> BorshSerialize for UInt<T, BITS>
where
Self: Number,
T: BorshSerialize
+ From<u8>
+ BitAnd<T, Output = T>
+ TryInto<u8>
+ Copy
+ Shr<usize, Output = T>,
<UInt<T, BITS> as Number>::UnderlyingType:
Shr<usize, Output = T> + TryInto<u8> + From<u8> + BitAnd<T>,
{
fn serialize<W: borsh::io::Write>(&self, writer: &mut W) -> borsh::io::Result<()> {
let value = self.value();
let length = (BITS + 7) / 8;
let mut bytes = 0;
let mask: T = u8::MAX.into();
while bytes < length {
let le_byte: u8 = ((value >> (bytes << 3)) & mask)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be simplified:

let le_bytes = (value >> (bytes << 3)) as u8;

No need for the expect, try_into() etc

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value is not primitive, so 'as' does not work as i tried

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could use https://docs.rs/num/latest/num/traits/trait.AsPrimitive.html , but do not want to force num traits usage

.try_into()
.ok()
.expect("we cut to u8 via mask");
writer.write(&[le_byte])?;
bytes += 1;
}
Ok(())
}
}

#[cfg(feature = "borsh")]
impl<
T: BorshDeserialize + core::cmp::PartialOrd<<UInt<T, BITS> as Number>::UnderlyingType>,
const BITS: usize,
> BorshDeserialize for UInt<T, BITS>
where
Self: Number,
{
fn deserialize_reader<R: borsh::io::Read>(reader: &mut R) -> borsh::io::Result<Self> {
let value = T::deserialize_reader(reader)?;
if value >= Self::MIN.value() && value <= Self::MAX.value() {
Ok(Self { value })
} else {
Err(borsh::io::Error::new(
borsh::io::ErrorKind::InvalidData,
"Value out of range",
))
}
}
}

#[cfg(feature = "borsh")]
impl<T, const BITS: usize> BorshSchema for UInt<T, BITS> {
fn add_definitions_recursively(
definitions: &mut BTreeMap<borsh::schema::Declaration, borsh::schema::Definition>,
) {
definitions.insert(
["u", &BITS.to_string()].concat(),
borsh::schema::Definition::Primitive(((BITS + 7) / 8) as u8),
);
}

fn declaration() -> borsh::schema::Declaration {
["u", &BITS.to_string()].concat()
}
}

#[cfg(feature = "serde")]
impl<T, const BITS: usize> Serialize for UInt<T, BITS>
where
Expand Down Expand Up @@ -1106,6 +1191,32 @@ where
}
}

#[cfg(feature = "schemars")]
impl<T, const BITS: usize> JsonSchema for UInt<T, BITS>
where
Self: Number,
{
fn schema_name() -> String {
["uint", &BITS.to_string()].concat()
}

fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
use schemars::schema::{NumberValidation, Schema, SchemaObject};
let schema_object = SchemaObject {
instance_type: Some(schemars::schema::InstanceType::Integer.into()),
format: Some(Self::schema_name()),
number: Some(Box::new(NumberValidation {
// can be done with https://github.com/rust-lang/rfcs/pull/2484
// minimum: Some(Self::MIN.value().try_into().ok().unwrap()),
// maximum: Some(Self::MAX.value().try_into().ok().unwrap()),
..Default::default()
})),
..Default::default()
};
Schema::Object(schema_object)
}
}

impl<T, const BITS: usize> Hash for UInt<T, BITS>
where
T: Hash,
Expand Down
52 changes: 52 additions & 0 deletions tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1911,3 +1911,55 @@ fn serde() {
"invalid value: integer `-1`, expected u128",
);
}

#[cfg(all(feature = "borsh", feature = "std"))]
#[test]
fn borsh() {
use borsh::schema::BorshSchemaContainer;
use borsh::{BorshDeserialize, BorshSerialize};
let mut buf = Vec::new();
let input = u9::new(42);
input.serialize(&mut buf).unwrap();
let output = u9::deserialize(&mut buf.as_ref()).unwrap();
assert_eq!(buf.len(), (u9::BITS + 7) / 8);
assert_eq!(input, output);

let input = u63::MAX;
let mut buf = Vec::new();
input.serialize(&mut buf).unwrap();
let output: u63 = u63::deserialize(&mut buf.as_ref()).unwrap();
assert_eq!(buf.len(), (u63::BITS + 7) / 8);
assert_eq!(input, output);

let schema = BorshSchemaContainer::for_type::<u9>();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just to understand this: BorschSchemaContainer does NOT require schemars? So we have two different things that are called schema?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One schema is JSON/OpenAPI shema integrated with serde.

Other shema is borshschema integrated with borsh.

There are schemas for protobuf/scale/etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do not require, just supporting several schemas

match schema.get_definition("u9").expect("exists") {
borsh::schema::Definition::Primitive(2) => {}
_ => panic!("unexpected schema"),
}
}

#[cfg(feature = "schemars")]
#[test]
fn schemars() {
use schemars::schema_for;
let mut u8 = schema_for!(u8);
let u9 = schema_for!(u9);
assert_eq!(
u8.schema.format.clone().unwrap().replace("8", "9"),
u9.schema.format.clone().unwrap()
);
u8.schema.format = u9.schema.format.clone();
assert_eq!(
u8.schema
.metadata
.clone()
.unwrap()
.title
.unwrap()
.replace("8", "9"),
u9.schema.metadata.clone().unwrap().title.unwrap()
);
u8.schema.metadata = u9.schema.metadata.clone();
u8.schema.number = u9.schema.number.clone();
assert_eq!(u8, u9);
}