feat: init

This commit is contained in:
Vincent Herlemont
2023-09-02 12:49:01 +02:00
commit a2d63d80c6
42 changed files with 2305 additions and 0 deletions

View File

@@ -0,0 +1,71 @@
name: Linux/Windows/macOS (Build/Test/Release)
on:
push:
branches: [ main, next ]
pull_request:
branches: [ main, next ]
jobs:
build_test_common_os:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
toolchain: [stable]
feature: [ no_feature ]
steps:
- uses: actions/checkout@v3
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.toolchain }}
override: true
- name: Setup Feature Args
shell: bash
run: |
if [[ "${{ matrix.feature }}" == "no_feature" ]]; then
echo "FEATURE_ARGS=" >> $GITHUB_ENV
else
echo "FEATURE_ARGS=-F ${{ matrix.feature }}" >> $GITHUB_ENV
fi
- name: Build
uses: actions-rs/cargo@v1
with:
command: build
args: ${{ env.FEATURE_ARGS }}
- name: Run tests
uses: actions-rs/cargo@v1
with:
command: test
args: ${{ env.FEATURE_ARGS }}
release:
name: Release
runs-on: ubuntu-latest
needs: [build_test_common_os]
if: github.ref == 'refs/heads/main'
permissions:
contents: write
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
ref: main
- name: install npm
uses: actions/setup-node@v3
with:
node-version: '16'
- name: install @semantic-release/exec
run: npm install @semantic-release/exec
- name: Semantic Release
uses: cycjimmy/semantic-release-action@v3
with:
branch: main
env:
GITHUB_TOKEN: ${{ secrets.PAT_GLOBAL }}
CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }}

View File

@@ -0,0 +1,13 @@
name: Conventional Commits
on:
pull_request:
branches: [ main ]
jobs:
build:
name: Conventional Commits
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: webiny/action-conventional-commits@v1.1.0

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
/target
/Cargo.lock
/native_model_macro/target
/native_model_macro/Cargo.lock

39
Cargo.toml Normal file
View File

@@ -0,0 +1,39 @@
[package]
name = "native_model"
version = "0.1.0"
authors = ["Vincent Herlemont <vincent@herlemont.fr>"]
edition = "2021"
description = "A thin wrapper around serialized data which add information of identity and version."
license = "MIT"
repository = "https://github.com/vincent-herlemont/native_model"
readme = "README.md"
build = "build.rs"
keywords = ["serialization", "interoperability", "data-consistency", "flexibility", "performance"]
categories = ["data-structures", "encoding", "rust-patterns"]
[workspace]
members = ["native_model_macro"]
[dependencies]
zerocopy = { version = "0.7.1", features = [ "derive"] }
thiserror = "1.0"
anyhow = "1.0"
native_model_macro = { version = "0.1.0", path = "native_model_macro" }
[dev-dependencies]
serde = { version = "1.0", features = ["derive"] }
bincode = { version = "2.0.0-rc.3", features = ["serde"] }
serde_json = "1.0"
criterion = { version = "0.5.1" }
skeptic = "0.13"
[[bench]]
name = "overhead"
harness = false
[[bench]]
name = "overhead_on_bincode"
harness = false
[build-dependencies]
skeptic = "0.13"

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Vincent Herlemont
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

168
README.md Normal file
View File

@@ -0,0 +1,168 @@
# Native model
[![Crates.io](https://img.shields.io/crates/v/native_model)](https://crates.io/crates/native_model)
[![Build and Release](https://github.com/vincent-herlemont/native_model/actions/workflows/build_and_test_release.
yml/badge.
svg)](https://github.com/vincent-herlemont/native_model/actions/workflows/build_and_test_release.yml)
[![Documentation](https://docs.rs/native_model/badge.svg)](https://docs.rs/native_model)
[![License](https://img.shields.io/crates/l/native_model)](LICENSE)
A thin wrapper around serialized data which add information of identity and version.
## Goals
- **Interoperability**: Allows different applications to work together, even if they are using different
versions of the data model.
- **Data Consistency**: Ensure that we process the data expected model.
- **Flexibility**: You can use any serialization format you want. More details [here](#setup-your-serialization-format).
- **Performance**: A minimal overhead. More details [here](#performance).
## Usage
```
Application 1 (DotV1) Application 2 (DotV1 and DotV2)
| |
Encode DotV1 |----------------------------------------> | Decode DotV1 to DotV2
| | Modify DotV2
Decode DotV1 | <----------------------------------------| Encode DotV2 back to DotV1
| |
```
```rust,skt-main
// Application 1
let dot = DotV1(1, 2);
let bytes = native_model::encode(&dot).unwrap();
// Application 1 sends bytes to Application 2.
// Application 2
// We are able to decode the bytes directly into a new type DotV2.
let (mut dot, source_version) = native_model::decode::<DotV2>(bytes).unwrap();
assert_eq!(dot, DotV2 {
name: "".to_string(),
x: 1,
y: 2
});
dot.name = "Dot".to_string();
dot.x = 5;
// For interoperability, we encode the data with the version compatible with Application 1.
let bytes = native_model::encode_downgrade(dot, source_version).unwrap();
// Application 2 sends bytes to Application 1.
// Application 1
let (dot, _) = native_model::decode::<DotV1>(bytes).unwrap();
assert_eq!(dot, DotV1(5, 2));
```
Full example [here](./tests/example/example_main.rs).
When use it?
- Your applications that interact with each other are written in Rust.
- Your applications evolve independently need to read serialized data coming from each other.
- Your applications store data locally and need to read it later by a newer version of the application.
- Your systems need to be upgraded incrementally. Instead of having to upgrade the entire system at once, individual
applications can be upgraded one at a time, while still being able to communicate with each other.
When not use it?
- Your applications that interact with each other are **not all** written in Rust.
- Your applications need to communicate with other systems that you don't control.
- You need to have a human-readable format. (You can use a human-readable format like JSON wrapped in a native model,
but you have to unwrap it to see the data correctly.)
# Status
Early development. Not ready for production.
## Setup your serialization format
First, you need to set up your serialization format. You can use any serialization format.
Just define the following functions, so they must be imported in the scope where you use the native model.
```rust,ignore
fn native_model_encode_body<T: Encode>(obj: &T) -> Result<Vec<u8>, dyn Error> {
...
}
fn native_model_decode_body<T: Decode>(data: Vec<u8>) -> Result<T, dyn Error> {
...
}
```
Examples:
- [bincode with encode/decode](./tests/example/encode_decode/bincode.rs)
- [bincode with serde](./tests/example/encode_decode/bincode_serde.rs)
## Setup your data model
Define your model using the macro [`native_model`](file:///home/vincentherlemont/IdeaProjects/native_model/target/doc/native_model/attr.native_model.html).
Attributes:
- `id = u32`: The unique identifier of the model.
- `version = u32`: The version of the model.
- `from = type`: Optional, the previous version of the model.
- `type`: The previous version of the model that you use for the From implementation.
- `try_from = (type, error)`: Optional, the previous version of the model with error handling.
- `type`: The previous version of the model that you use for the TryFrom implementation.
- `error`: The error type that you use for the TryFrom implementation.
```rust,skt-define-models
use native_model::native_model;
#[derive(Encode, Decode, PartialEq, Debug)]
#[native_model(id = 1, version = 1)]
struct DotV1(u32, u32);
#[derive(Encode, Decode, PartialEq, Debug)]
#[native_model(id = 1, version = 2, from = DotV1)]
struct DotV2 {
name: String,
x: u64,
y: u64,
}
// Implement the conversion between versions From<DotV1> for DotV2 and From<DotV2> for DotV1.
#[derive(Encode, Decode, PartialEq, Debug)]
#[native_model(id = 1, version = 3, try_from = (DotV2, anyhow::Error))]
struct DotV3 {
name: String,
cord: Cord,
}
#[derive(Encode, Decode, PartialEq, Debug)]
struct Cord {
x: u64,
y: u64,
}
// Implement the conversion between versions From<DotV2> for DotV3 and From<DotV3> for DotV2.
```
Full example [here](tests/example/example_define_model.rs).
# Performance
This crate is in an early stage of development, so the performance should be improved in the future.
The goal is to have a minimal and constant overhead for all data sizes. It uses the [zerocopy](https://docs.rs/zerocopy/latest/zerocopy/) crate to avoid unnecessary copies.
Current performance:
- Encode time: have overhead that evolves linearly with the data size.
- Decode time: have overhead of ~162 ps for all data sizes.
| data size | encode time (ns/ps/µs/ms) | decode time (ps) |
|:---------------------:|:--------------------------:|:----------------:|
| 1 B | 40.093 ns - 40.510 ns | 161.87 ps - 162.02 ps |
| 1 KiB (1024 B) | 116.45 ns - 116.83 ns | 161.85 ps - 162.08 ps |
| 1 MiB (1048576 B) | 66.697 µs - 67.634 µs | 161.87 ps - 162.18 ps |
| 10 MiB (10485760 B) | 1.5670 ms - 1.5843 ms | 162.40 ps - 163.52 ps |
| 100 MiB (104857600 B) | 63.778 ms - 64.132 ms | 162.71 ps - 165.10 ps |
Benchmark of the native model overhead [here](benches/overhead.rs).
To know how much time it takes to encode/decode your data, you need to add this overhead to the time of your serialization format.

140
README.md.skt.md Normal file
View File

@@ -0,0 +1,140 @@
```rust,skt-main
use bincode;
use bincode::{{Decode, Encode}};
use native_model_macro::native_model;
fn native_model_encode_body<T: bincode::Encode>(
model: &T,
) -> Result<Vec<u8>, bincode::error::EncodeError> {{
{{
bincode::encode_to_vec(model, bincode::config::standard())
}}
}}
fn native_model_decode_body<T: bincode::Decode>(
data: Vec<u8>,
) -> Result<T, bincode::error::DecodeError> {{
{{
bincode::decode_from_slice(&data, bincode::config::standard()).map(|(result, _)| result)
}}
}}
#[derive(Encode, Decode, PartialEq, Debug)]
#[native_model(id = 1, version = 1)]
struct DotV1(u32, u32);
#[derive(Encode, Decode, PartialEq, Debug)]
#[native_model(id = 1, version = 2, from = DotV1)]
struct DotV2 {{
name: String,
x: u64,
y: u64,
}}
impl From<DotV1> for DotV2 {{
fn from(dot: DotV1) -> Self {{
DotV2 {{
name: "".to_string(),
x: dot.0 as u64,
y: dot.1 as u64,
}}
}}
}}
impl From<DotV2> for DotV1 {{
fn from(dot: DotV2) -> Self {{
DotV1(dot.x as u32, dot.y as u32)
}}
}}
fn main() {{
{}
}}
```
```rust,skt-define-models
use bincode::{{config, Decode, Encode}};
#[allow(dead_code)]
fn native_model_encode_body<T: Encode>(obj: &T) -> Result<Vec<u8>, bincode::error::EncodeError> {{
bincode::encode_to_vec(obj, config::standard())
}}
#[allow(dead_code)]
fn native_model_decode_body<T: Decode>(data: Vec<u8>) -> Result<T, bincode::error::DecodeError> {{
bincode::decode_from_slice(&data, config::standard()).map(|(result, _)| result)
}}
{}
impl From<DotV1> for DotV2 {{
fn from(dot: DotV1) -> Self {{
DotV2 {{
name: "".to_string(),
x: dot.0 as u64,
y: dot.1 as u64,
}}
}}
}}
impl From<DotV2> for DotV1 {{
fn from(dot: DotV2) -> Self {{
DotV1(dot.x as u32, dot.y as u32)
}}
}}
impl TryFrom<DotV2> for DotV3 {{
type Error = anyhow::Error;
fn try_from(dot: DotV2) -> Result<Self, Self::Error> {{
Ok(DotV3 {{
name: dot.name,
cord: Cord {{ x: dot.x, y: dot.y }},
}})
}}
}}
impl TryFrom<DotV3> for DotV2 {{
type Error = anyhow::Error;
fn try_from(dot: DotV3) -> Result<Self, Self::Error> {{
Ok(DotV2 {{
name: dot.name,
x: dot.cord.x,
y: dot.cord.y,
}})
}}
}}
fn main() {{
let dot = DotV1(1, 2);
let bytes = native_model::encode(&dot).unwrap();
let (dot_decoded, _) = native_model::decode::<DotV1>(bytes.clone()).unwrap();
assert_eq!(dot, dot_decoded);
let (dot_decoded, _) = native_model::decode::<DotV2>(bytes.clone()).unwrap();
assert_eq!(
DotV2 {{
name: "".to_string(),
x: 1,
y: 2
}},
dot_decoded
);
let (dot_decoded, _) = native_model::decode::<DotV3>(bytes.clone()).unwrap();
assert_eq!(
DotV3 {{
name: "".to_string(),
cord: Cord {{ x: 1, y: 2 }}
}},
dot_decoded
);
}}
```

49
benches/overhead.rs Normal file
View File

@@ -0,0 +1,49 @@
use bincode::{Decode, Encode};
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use native_model_macro::native_model;
fn native_model_encode_body<T: Encode>(obj: &T) -> Result<Vec<u8>, bincode::error::EncodeError> {
bincode::encode_to_vec(obj, bincode::config::standard())
}
fn native_model_decode_body<T: Decode>(data: Vec<u8>) -> Result<T, bincode::error::DecodeError> {
bincode::decode_from_slice(&data, bincode::config::standard()).map(|(result, _)| result)
}
#[derive(Encode, Decode)]
#[native_model(id = 1, version = 1)]
struct Data(Vec<u8>);
fn wrapper(data: &mut Vec<u8>) {
native_model::wrapper::native_model_encode(data, 1, 1);
}
fn unwrap(data: &mut Vec<u8>) {
native_model::wrapper::Wrapper::deserialize(&data[..]).unwrap();
}
fn criterion_benchmark(c: &mut Criterion) {
let mut group = c.benchmark_group("encode");
// 1 byte, 1KB, 1MB, 10MB, 100MB
for nb_bytes in [1, 1024, 1024 * 1024, 10 * 1024 * 1024, 100 * 1024 * 1024].into_iter() {
group.throughput(criterion::Throughput::Bytes(nb_bytes as u64));
// encode
let data = Data(vec![1; nb_bytes]);
let encode_body = native_model_encode_body(&data).unwrap();
group.bench_function(BenchmarkId::new("encode", nb_bytes), |b| {
b.iter(|| wrapper(&mut encode_body.clone()))
});
// decode
let data = Data(vec![1; nb_bytes]);
let encode_body = native_model::encode(&data).unwrap();
group.bench_function(BenchmarkId::new("decode", nb_bytes), |b| {
b.iter(|| unwrap(&mut encode_body.clone()))
});
}
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

View File

@@ -0,0 +1,88 @@
use bincode::{Decode, Encode};
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use native_model_macro::native_model;
#[derive(Encode, Decode)]
struct DataForBincode {
x: i32,
string: String,
}
// Encode 1 data with bincode
fn native_model_encode_body<T: Encode>(obj: &T) -> Result<Vec<u8>, bincode::error::EncodeError> {
bincode::encode_to_vec(obj, bincode::config::standard())
}
fn native_model_decode_body<T: Decode>(data: Vec<u8>) -> Result<T, bincode::error::DecodeError> {
bincode::decode_from_slice(&data, bincode::config::standard()).map(|(result, _)| result)
}
fn encode_with_bincode(data: &DataForBincode) -> Vec<u8> {
native_model_encode_body(data).unwrap()
}
fn decode_with_bincode(data: Vec<u8>) -> DataForBincode {
native_model_decode_body(data).unwrap()
}
fn encode_decode_with_bincode(data: &DataForBincode) -> DataForBincode {
decode_with_bincode(encode_with_bincode(data))
}
#[derive(Encode, Decode)]
#[native_model(id = 1, version = 1)]
struct DataForNativeModel {
x: i32,
string: String,
}
fn encode_with_native_model(data: &DataForNativeModel) -> Vec<u8> {
native_model::encode(data).unwrap()
}
fn decode_with_native_model(data: Vec<u8>) -> DataForNativeModel {
let (data, _) = native_model::decode::<DataForNativeModel>(data).unwrap();
data
}
fn encode_decode_with_native_model(data: &DataForNativeModel) -> DataForNativeModel {
decode_with_native_model(encode_with_native_model(data))
}
fn criterion_benchmark(c: &mut Criterion) {
// Bincode
let data = DataForBincode {
x: 1,
// Set a very long string
string: "Hello".repeat(10000),
};
c.bench_function("encode_with_bincode", |b| {
b.iter(|| encode_with_bincode(black_box(&data)))
});
let encoded_data = encode_with_bincode(&data);
c.bench_function("decode_with_bincode", |b| {
b.iter(|| decode_with_bincode(black_box(encoded_data.clone())))
});
c.bench_function("encode_decode_with_bincode", |b| {
b.iter(|| encode_decode_with_bincode(black_box(&data)))
});
// Native model
let data = DataForNativeModel {
x: 1,
string: "Hello".repeat(10000),
};
c.bench_function("encode_with_native_model", |b| {
b.iter(|| encode_with_native_model(black_box(&data)))
});
let encoded_data = native_model::encode(&data).unwrap();
c.bench_function("decode_with_native_model", |b| {
b.iter(|| decode_with_native_model(black_box(encoded_data.clone())))
});
c.bench_function("encode_decode_with_native_model", |b| {
b.iter(|| encode_decode_with_native_model(black_box(&data)))
});
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

11
build.rs Normal file
View File

@@ -0,0 +1,11 @@
extern crate skeptic;
use skeptic::{generate_doc_tests, markdown_files_of_directory};
fn main() {
{
let mut mdbook_files = markdown_files_of_directory("doc/");
mdbook_files.push("README.md".into());
generate_doc_tests(&mdbook_files);
}
}

14
cargo_publish.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
set -e
set -x
ARG_TOKEN="--token=$CARGO_TOKEN"
cd $DIR/native_model_macro
cargo publish $ARG_TOKEN $@
cd $DIR
cargo publish $ARG_TOKEN $@

View File

@@ -0,0 +1,19 @@
[package]
name = "native_model_macro"
version = "0.1.0"
authors = ["Vincent Herlemont <vincent@herlemont.fr>"]
edition = "2018"
description = "A procedural macro for native_model"
license = "MIT"
repository = "https://github.com/vincent-herlemont/native_model"
readme = "README.md"
[lib]
path = "src/lib.rs"
proc-macro = true
[dependencies]
syn = { version = "2.0", features = ["full"] }
quote = "1.0"
proc-macro2 = "1.0.66"

View File

@@ -0,0 +1 @@
A procedural macro for [native_model](https://github.com/vincent-herlemont/native_model).

View File

@@ -0,0 +1,114 @@
extern crate proc_macro;
mod method;
use crate::method::{
generate_native_model_decode_body, generate_native_model_decode_upgrade_body,
generate_native_model_encode_body, generate_native_model_encode_downgrade_body,
generate_native_model_id, generate_native_model_version,
};
use proc_macro::TokenStream;
use quote::quote;
use syn::meta::ParseNestedMeta;
use syn::parse::{Parse, Result};
use syn::punctuated::Punctuated;
use syn::token;
use syn::{parse_macro_input, DeriveInput, LitInt, Path, Token};
// Inspiration: https://docs.rs/syn/2.0.29/syn/meta/fn.parser.html#example-1
#[derive(Default)]
pub(crate) struct ModelAttributes {
pub(crate) id: Option<LitInt>,
pub(crate) version: Option<LitInt>,
// type
pub(crate) from: Option<Path>,
// (type, try_from::Error type)
pub(crate) try_from: Option<(Path, Path)>,
}
impl ModelAttributes {
fn parse(&mut self, meta: ParseNestedMeta) -> Result<()> {
if meta.path.is_ident("id") {
self.id = Some(meta.value()?.parse()?);
} else if meta.path.is_ident("version") {
self.version = Some(meta.value()?.parse()?);
} else if meta.path.is_ident("from") {
self.from = Some(meta.value()?.parse()?);
} else if meta.path.is_ident("try_from") {
let tuple_try_from: TupleTryFrom = meta.value()?.parse()?;
let mut fields = tuple_try_from.fields.into_iter();
self.try_from.replace((
fields.next().unwrap().clone(),
fields.next().unwrap().clone(),
));
} else {
panic!(
"Unknown attribute: {}",
meta.path.get_ident().unwrap().to_string()
);
}
Ok(())
}
}
#[derive(Default)]
pub(crate) struct TupleTryFrom {
pub(crate) _parent_token: token::Paren,
pub(crate) fields: Punctuated<Path, Token![,]>,
}
impl Parse for TupleTryFrom {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let content;
Ok(TupleTryFrom {
_parent_token: syn::parenthesized!(content in input),
fields: content.parse_terminated(Path::parse, Token![,])?,
})
}
}
/// Macro which add identity and version to your rust type.
///
/// Attributes:
/// - `id = u32`: The unique identifier of the model.
/// - `version = u32`: The version of the model.
/// - `from = type`: Optional, the previous version of the model.
/// - `type`: The previous version of the model that you use for the From implementation.
/// - `try_from = (type, error)`: Optional, the previous version of the model with error handling.
/// - `type`: The previous version of the model that you use for the TryFrom implementation.
/// - `error`: The error type that you use for the TryFrom implementation.
///
/// See examples:
/// - [Setup your data model](https://github.com/vincent-herlemont/native_model_private#setup-your-data-model).
/// - other [examples](https://github.com/vincent-herlemont/native_model/tree/master/tests/example)
#[proc_macro_attribute]
pub fn native_model(args: TokenStream, input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let struct_name = &ast.ident;
let mut attrs = ModelAttributes::default();
let model_attributes_parser = syn::meta::parser(|meta| attrs.parse(meta));
parse_macro_input!(args with model_attributes_parser);
let native_model_id_fn = generate_native_model_id(&attrs);
let native_model_version_fn = generate_native_model_version(&attrs);
let native_model_encode_body_fn = generate_native_model_encode_body();
let native_model_encode_downgrade_body_fn = generate_native_model_encode_downgrade_body(&attrs);
let native_model_decode_body_fn = generate_native_model_decode_body();
let native_model_decode_upgrade_body_fn = generate_native_model_decode_upgrade_body(&attrs);
let gen = quote! {
#ast
impl native_model::Model for #struct_name {
#native_model_id_fn
#native_model_version_fn
#native_model_encode_body_fn
#native_model_encode_downgrade_body_fn
#native_model_decode_body_fn
#native_model_decode_upgrade_body_fn
}
};
gen.into()
}

View File

@@ -0,0 +1,15 @@
use proc_macro2::TokenStream;
use quote::quote;
pub(crate) fn generate_native_model_decode_body() -> TokenStream {
let gen = quote! {
fn native_model_decode_body(data: Vec<u8>) -> Result<Self, native_model::DecodeBodyError> {
native_model_decode_body(data).map_err(|e| native_model::DecodeBodyError {
msg: format!("{}", e),
source: e.into(),
})
}
};
gen.into()
}

View File

@@ -0,0 +1,50 @@
use crate::ModelAttributes;
use proc_macro2::TokenStream;
use quote::quote;
pub(crate) fn generate_native_model_decode_upgrade_body(attrs: &ModelAttributes) -> TokenStream {
let native_model_from = attrs.from.clone();
let native_model_try_from = attrs.try_from.clone();
let model_from_or_try_from = if let Some(from) = native_model_from {
quote! {
#from::native_model_decode_upgrade_body(data, x).map(|a| a.into())
}
} else if let Some((try_from, error_try_from)) = native_model_try_from {
quote! {
let result = #try_from::native_model_decode_upgrade_body(data, x).map(|b| {
b.try_into()
.map_err(|e: #error_try_from| native_model::UpgradeError {
msg: format!("{}", e),
source: e.into(),
})
})??;
Ok(result)
}
} else {
quote! {
Err(native_model::Error::UpgradeNotSupported {
from: x,
to: Self::native_model_version(),
})
}
};
let gen = quote! {
fn native_model_decode_upgrade_body(data: Vec<u8>, x: u32) -> native_model::Result<Self> {
if x == Self::native_model_version() {
let result = Self::native_model_decode_body(data)?;
Ok(result)
} else if x < Self::native_model_version() {
#model_from_or_try_from
} else {
Err(native_model::Error::UpgradeNotSupported {
from: x,
to: Self::native_model_version(),
})
}
}
};
gen
}

View File

@@ -0,0 +1,15 @@
use proc_macro2::TokenStream;
use quote::quote;
pub(crate) fn generate_native_model_encode_body() -> TokenStream {
let gen = quote! {
fn native_model_encode_body(&self) -> Result<Vec<u8>, native_model::EncodeBodyError> {
native_model_encode_body(self).map_err(|e| native_model::EncodeBodyError {
msg: format!("{}", e),
source: e.into(),
})
}
};
gen.into()
}

View File

@@ -0,0 +1,54 @@
use crate::ModelAttributes;
use proc_macro2::TokenStream;
use quote::quote;
pub(crate) fn generate_native_model_encode_downgrade_body(attrs: &ModelAttributes) -> TokenStream {
let native_model_from = attrs.from.clone();
let native_model_try_from = attrs.try_from.clone();
let model_from_or_try_from = if let Some(from) = native_model_from {
quote! {
#from::native_model_encode_downgrade_body(self.into(), version)
}
} else if let Some((try_from, error_try_from)) = native_model_try_from {
quote! {
let result = #try_from::native_model_encode_downgrade_body(
self.try_into()
.map_err(|e: #error_try_from| native_model::DowngradeError {
msg: format!("{}", e),
source: e.into(),
})?,
version,
)?;
Ok(result)
}
} else {
quote! {
Err(native_model::Error::DowngradeNotSupported {
from: version,
to: Self::native_model_version(),
})
}
};
let gen = quote! {
fn native_model_encode_downgrade_body(self, version: u32) -> native_model::Result<Vec<u8>> {
if version == Self::native_model_version() {
let result = self.native_model_encode_body()?;
Ok(result)
} else if version < Self::native_model_version() {
#model_from_or_try_from
} else {
Err(native_model::Error::DowngradeNotSupported {
from: version,
to: Self::native_model_version(),
})
}
}
};
gen
}
// #[error("Wrong type id expected: {}, actual: {}", expected, actual)]
// WrongTypeId { expected: u32, actual: u32 },

View File

@@ -0,0 +1,13 @@
use crate::ModelAttributes;
use proc_macro2::TokenStream;
use quote::quote;
pub(crate) fn generate_native_model_id(model_attributes: &ModelAttributes) -> TokenStream {
let native_model_id = model_attributes.id.clone().unwrap();
let gen = quote! {
fn native_model_id() -> u32 {
#native_model_id
}
};
gen
}

View File

@@ -0,0 +1,13 @@
mod decode_body;
mod decode_upgrade_body;
mod encode_body;
mod encode_downgrade_body;
mod id;
mod version;
pub(crate) use decode_body::*;
pub(crate) use decode_upgrade_body::*;
pub(crate) use encode_body::*;
pub(crate) use encode_downgrade_body::*;
pub(crate) use id::*;
pub(crate) use version::*;

View File

@@ -0,0 +1,13 @@
use crate::ModelAttributes;
use proc_macro2::TokenStream;
use quote::quote;
pub(crate) fn generate_native_model_version(model_attributes: &ModelAttributes) -> TokenStream {
let native_model_version = model_attributes.version.clone().unwrap();
let gen = quote! {
fn native_model_version() -> u32 {
#native_model_version
}
};
gen
}

38
release.config.js Normal file
View File

@@ -0,0 +1,38 @@
module.exports = {
branches: ['main'],
tagFormat: '${version}',
plugins: [
['@semantic-release/commit-analyzer', {
releaseRules: [
{breaking: true, release: 'minor'},
{revert: true, release: 'patch'},
{type: 'feat', release: 'minor'},
{type: 'fix', release: 'patch'},
{type: 'perf', release: 'patch'},
{type: 'docs', release: 'patch'},
{emoji: ':racehorse:', release: 'patch'},
{emoji: ':bug:', release: 'patch'},
{emoji: ':penguin:', release: 'patch'},
{emoji: ':apple:', release: 'patch'},
{emoji: ':checkered_flag:', release: 'patch'},
{tag: 'BUGFIX', release: 'patch'},
{tag: 'FEATURE', release: 'minor'},
{tag: 'SECURITY', release: 'patch'},
{tag: 'Breaking', release: 'minor'},
{tag: 'Fix', release: 'patch'},
{tag: 'Update', release: 'minor'},
{tag: 'New', release: 'minor'},
{component: 'perf', release: 'patch'},
{component: 'deps', release: 'patch'},
{type: 'FEAT', release: 'minor'},
{type: 'FIX', release: 'patch'},
],
}],
'@semantic-release/release-notes-generator',
['@semantic-release/exec', {
"prepareCmd": "bash version_update.sh ${nextRelease.version}",
"publishCmd": "bash cargo_publish.sh",
}],
'@semantic-release/github',
],
};

21
renovate.json Normal file
View File

@@ -0,0 +1,21 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base"],
"semanticCommits": "enabled",
"semanticCommitType": "chore",
"semanticCommitScope": "deps",
"platformAutomerge": true,
"packageRules": [
{
"description": "Automerge non-major updates",
"matchUpdateTypes": ["minor", "patch"],
"automerge": true
},
{
"description": "Automerge actions",
"matchDepTypes": ["action"],
"matchUpdateTypes": ["major", "minor", "patch"],
"automerge": true
}
]
}

9
src/header.rs Normal file
View File

@@ -0,0 +1,9 @@
use zerocopy::little_endian::U32;
use zerocopy::{AsBytes, FromBytes, FromZeroes};
#[derive(FromZeroes, FromBytes, AsBytes, Debug)]
#[repr(C)]
pub struct Header {
pub(crate) type_id: U32,
pub(crate) version: U32,
}

114
src/lib.rs Normal file
View File

@@ -0,0 +1,114 @@
//! `native_model` is a Rust crate that acts as a thin wrapper around serialized data, adding identity and version information.
//!
//! - It aims to ensure:
//! - **Interoperability**: Different applications can work together even if they use different data model versions.
//! - **Data Consistency**: Ensures the data is processed as expected.
//! - **Flexibility**: Allows the use of any serialization format.
//! - **Minimal Performance Overhead**: Current performance shows linearly increasing encoding overhead with data size, and constant decoding overhead (~162 picoseconds) for all data sizes.
//! - **Suitability**:
//! - Suitable for applications that are written in Rust, evolve independently, store data locally, and require incremental upgrades.
//! - Not suitable for non-Rust applications, systems not controlled by the user, or when human-readable formats are needed.
//! - **Setup**:
//! - Users must define their own serialization format and data model. Examples and a `native_model` macro are provided for this purpose.
//! - **Development Stage**:
//! - The crate is in early development, and performance is expected to improve over time.
//!
//! See examples in the [README.md](https://github.com/vincent-herlemont/native_model) file.
mod header;
mod model;
pub mod wrapper;
pub use model::*;
/// Macro to generate a [`native_model`] implementation for a struct.
pub use native_model_macro::*;
use wrapper::*;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Error, Debug)]
pub enum Error {
#[error("Invalid header")]
InvalidHeader,
#[error("Failed to decode native model")]
DecodeError,
#[error(transparent)]
DecodeBodyError(#[from] DecodeBodyError),
#[error(transparent)]
EncodeBodyError(#[from] EncodeBodyError),
#[error(transparent)]
UpgradeError(#[from] UpgradeError),
#[error("Upgrade from {} to {} is not supported", from, to)]
UpgradeNotSupported { from: u32, to: u32 },
#[error(transparent)]
DowngradeError(#[from] DowngradeError),
#[error("Downgrade from {} to {} is not supported", from, to)]
DowngradeNotSupported { from: u32, to: u32 },
#[error("Wrong type id expected: {}, actual: {}", expected, actual)]
WrongTypeId { expected: u32, actual: u32 },
}
pub type DecodeResult<T> = std::result::Result<T, DecodeBodyError>;
#[derive(Error, Debug)]
#[error("Decode body error: {msg}")]
pub struct DecodeBodyError {
pub msg: String,
#[source]
pub source: anyhow::Error,
}
pub type EncodeResult<T> = std::result::Result<T, EncodeBodyError>;
#[derive(Error, Debug)]
#[error("Encode body error: {msg}")]
pub struct EncodeBodyError {
pub msg: String,
#[source]
pub source: anyhow::Error,
}
#[derive(Error, Debug)]
#[error("Upgrade error: {msg}")]
pub struct UpgradeError {
pub msg: String,
#[source]
pub source: anyhow::Error,
}
#[derive(Error, Debug)]
#[error("Downgrade error: {msg}")]
pub struct DowngradeError {
pub msg: String,
#[source]
pub source: anyhow::Error,
}
/// Allows to encode a [`native_model`] into a [`Vec<u8>`].
///
/// See examples:
/// - [README.md](https://github.com/vincent-herlemont/native_model) file.
/// - other [examples](https://github.com/vincent-herlemont/native_model/tree/master/tests/example)
pub fn encode<T: Model>(model: &T) -> Result<Vec<u8>> {
T::native_model_encode(model)
}
/// Allows to encode a [`native_model`] into a [`Vec<u8>`] with a specific version.
/// See examples:
/// - [README.md](https://github.com/vincent-herlemont/native_model) file.
/// - other [examples](https://github.com/vincent-herlemont/native_model/tree/master/tests/example)
pub fn encode_downgrade<T: Model>(model: T, version: u32) -> Result<Vec<u8>> {
T::native_model_encode_downgrade(model, version)
}
/// Allows to decode a [`native_model`] from a [`Vec<u8>`] and returns the version ([`u32`]).
/// See examples:
/// - [README.md](https://github.com/vincent-herlemont/native_model) file.
/// - other [examples](https://github.com/vincent-herlemont/native_model/tree/master/tests/example)
pub fn decode<T: Model>(data: Vec<u8>) -> Result<(T, u32)> {
T::native_model_decode(data)
}

60
src/model.rs Normal file
View File

@@ -0,0 +1,60 @@
use crate::{DecodeResult, EncodeResult, Result};
pub trait Model: Sized {
fn native_model_id() -> u32;
fn native_model_version() -> u32;
// --------------- Decode ---------------
fn native_model_decode_body(data: Vec<u8>) -> DecodeResult<Self>
where
Self: Sized;
fn native_model_decode_upgrade_body(data: Vec<u8>, version: u32) -> Result<Self>
where
Self: Sized;
fn native_model_decode(data: Vec<u8>) -> Result<(Self, u32)>
where
Self: Sized,
{
let native_model = crate::Wrapper::deserialize(&data[..]).unwrap();
let source_version = native_model.get_version();
let result =
Self::native_model_decode_upgrade_body(native_model.value().to_vec(), source_version)?;
Ok((result, source_version))
}
// --------------- Encode ---------------
fn native_model_encode_body(&self) -> EncodeResult<Vec<u8>>
where
Self: Sized;
fn native_model_encode_downgrade_body(self, version: u32) -> Result<Vec<u8>>
where
Self: Sized;
fn native_model_encode(&self) -> Result<Vec<u8>>
where
Self: Sized,
{
let mut data = self.native_model_encode_body()?;
crate::native_model_encode(
&mut data,
Self::native_model_id(),
Self::native_model_version(),
);
Ok(data)
}
fn native_model_encode_downgrade(self, version: u32) -> Result<Vec<u8>>
where
Self: Sized,
{
let version = version.clone();
let mut data = self.native_model_encode_downgrade_body(version)?;
crate::native_model_encode(&mut data, Self::native_model_id(), version);
Ok(data)
}
}

86
src/wrapper.rs Normal file
View File

@@ -0,0 +1,86 @@
use crate::header::Header;
use zerocopy::little_endian::U32;
use zerocopy::{AsBytes, ByteSlice, ByteSliceMut, Ref};
#[derive(Debug)]
pub struct Wrapper<T: ByteSlice> {
header: Ref<T, Header>, // Deprecated: Rename LayoutVerified to Ref #203
value: T,
}
impl<T: ByteSlice> Wrapper<T> {
pub fn deserialize(packed: T) -> Option<Self> {
let (header_lv, rest) = Ref::<_, Header>::new_from_prefix(packed)?;
let native_model = Self {
header: header_lv,
value: rest,
};
Some(native_model)
}
pub fn value(&self) -> &T {
&self.value
}
pub fn get_type_id(&self) -> u32 {
self.header.type_id.get()
}
pub fn get_version(&self) -> u32 {
self.header.version.get()
}
}
impl<T: ByteSliceMut> Wrapper<T> {
pub fn set_type_id(&mut self, type_id: u32) {
self.header.type_id = U32::new(type_id);
}
pub fn set_version(&mut self, version: u32) {
self.header.version = U32::new(version);
}
}
pub fn native_model_encode(value: &mut Vec<u8>, type_id: u32, version: u32) {
let header = Header {
type_id: U32::new(type_id),
version: U32::new(version),
};
let header = header.as_bytes();
value.reserve(header.len());
value.splice(..0, header.iter().cloned());
// Try to do with unsafe code to improve performance but benchmark shows that it's the same
//
// // Add header to the beginning of the vector
// unsafe {
// // get the raw pointer to the vector's buffer
// let ptr = value.as_mut_ptr();
//
// // move the existing elements to the right
// ptr.offset(header.len() as isize)
// .copy_from_nonoverlapping(ptr, value.len());
//
// // copy the elements from the header to the beginning of the vector
// ptr.copy_from_nonoverlapping(header.as_ptr(), header.len());
//
// // update the length of the vector
// value.set_len(value.len() + header.len());
// }
}
#[cfg(test)]
mod tests {
use crate::{native_model_encode, Wrapper};
#[test]
fn native_model_deserialize_with_body() {
let mut data = vec![0u8; 8];
native_model_encode(&mut data, 200000, 100000);
assert_eq!(data.len(), 16);
let model = Wrapper::deserialize(&data[..]).unwrap();
assert_eq!(model.get_type_id(), 200000);
assert_eq!(model.get_version(), 100000);
assert_eq!(model.value().len(), 8);
}
}

1
tests/_example.rs Normal file
View File

@@ -0,0 +1 @@
mod example;

344
tests/_experiment.rs Normal file
View File

@@ -0,0 +1,344 @@
use bincode::{config, Decode, Encode};
use native_model::Result;
use native_model::{DecodeBodyError, DecodeResult, EncodeBodyError, EncodeResult, Model};
// Add this function to the macro for custom serialization
fn native_model_encode<T: Encode>(obj: &T) -> anyhow::Result<Vec<u8>> {
let result = bincode::encode_to_vec(obj, config::standard())?;
Ok(result)
}
// Add this function to the macro for custom deserialization
fn native_model_decode<T: Decode>(data: Vec<u8>) -> anyhow::Result<T> {
let (result, _) =
bincode::decode_from_slice(&data, config::standard()).map_err(|e| EncodeBodyError {
msg: format!("Decode error: {}", e),
source: e.into(),
})?;
Ok(result)
}
#[derive(Debug, Encode, Decode)]
struct A {}
impl Model for A {
fn native_model_id() -> u32 {
1
}
fn native_model_version() -> u32 {
1
}
fn native_model_decode_upgrade_body(_data: Vec<u8>, x: u32) -> Result<Self> {
println!(
"A::deserialization_and_upgrade({}, {})",
x,
Self::native_model_version()
);
if x == Self::native_model_version() {
Ok(Self {})
} else if x < Self::native_model_version() {
panic!("The version {} not supported", x);
} else {
panic!("Not implemented");
}
}
fn native_model_encode_body(&self) -> EncodeResult<Vec<u8>>
where
Self: Sized,
{
native_model_encode(self).map_err(|e| EncodeBodyError {
msg: format!("{}", e),
source: e.into(),
})
}
fn native_model_decode_body(data: Vec<u8>) -> DecodeResult<Self>
where
Self: Sized,
{
native_model_decode(data).map_err(|e| DecodeBodyError {
msg: format!("{}", e),
source: e.into(),
})
}
fn native_model_encode_downgrade_body(self, version: u32) -> Result<Vec<u8>>
where
Self: Sized,
{
println!(
"A::serialization_and_downgrade({}, {})",
version,
Self::native_model_version()
);
if version == Self::native_model_version() {
let result = self.native_model_encode_body()?;
Ok(result)
} else if version < Self::native_model_version() {
panic!("The version {} not supported", version);
} else {
panic!("Not implemented");
}
}
}
#[derive(Debug, Encode, Decode)]
struct B {}
impl Model for B {
fn native_model_id() -> u32 {
1
}
fn native_model_version() -> u32 {
2
}
fn native_model_decode_upgrade_body(_data: Vec<u8>, x: u32) -> Result<Self> {
println!(
"B::deserialization_and_upgrade({}, {})",
x,
Self::native_model_version()
);
if x == Self::native_model_version() {
Ok(Self {})
} else if x < Self::native_model_version() {
A::native_model_decode_upgrade_body(_data, x).map(|a| a.into())
} else {
panic!("Not implemented");
}
}
fn native_model_encode_body(&self) -> EncodeResult<Vec<u8>>
where
Self: Sized,
{
native_model_encode(self).map_err(|e| EncodeBodyError {
msg: format!("{}", e),
source: e.into(),
})
}
fn native_model_decode_body(data: Vec<u8>) -> DecodeResult<Self>
where
Self: Sized,
{
native_model_decode(data).map_err(|e| DecodeBodyError {
msg: format!("{}", e),
source: e.into(),
})
}
fn native_model_encode_downgrade_body(self, version: u32) -> Result<Vec<u8>>
where
Self: Sized,
{
println!(
"B::serialization_and_downgrade({}, {})",
version,
Self::native_model_version()
);
if version == Self::native_model_version() {
let result = self.native_model_encode_body()?;
Ok(result)
} else if version < Self::native_model_version() {
A::native_model_encode_downgrade_body(self.into(), version)
} else {
panic!("Not implemented");
}
}
}
impl From<B> for A {
fn from(_: B) -> Self {
Self {}
}
}
impl From<A> for B {
fn from(_: A) -> Self {
Self {}
}
}
#[derive(Debug, Encode, Decode)]
struct C {}
impl Model for C {
fn native_model_id() -> u32 {
1
}
fn native_model_version() -> u32 {
3
}
fn native_model_decode_upgrade_body(_data: Vec<u8>, x: u32) -> Result<Self> {
println!(
"C::deserialization_and_upgrade({}, {})",
x,
Self::native_model_version()
);
if x == Self::native_model_version() {
Ok(Self {})
} else if x < Self::native_model_version() {
let result = B::native_model_decode_upgrade_body(_data, x).map(|b| {
b.try_into()
.map_err(|e: anyhow::Error| native_model::UpgradeError {
msg: format!("{}", e),
source: e.into(),
})
})??;
Ok(result)
} else {
panic!("Not implemented");
}
}
fn native_model_encode_body(&self) -> EncodeResult<Vec<u8>>
where
Self: Sized,
{
native_model_encode(self).map_err(|e| EncodeBodyError {
msg: format!("{}", e),
source: e.into(),
})
}
fn native_model_decode_body(data: Vec<u8>) -> DecodeResult<Self>
where
Self: Sized,
{
native_model_decode(data).map_err(|e| DecodeBodyError {
msg: format!("{}", e),
source: e.into(),
})
}
fn native_model_encode_downgrade_body(self, version: u32) -> Result<Vec<u8>>
where
Self: Sized,
{
println!(
"C::serialization_and_downgrade({}, {})",
version,
Self::native_model_version()
);
if version == Self::native_model_version() {
let result = self.native_model_encode_body()?;
Ok(result)
} else if version < Self::native_model_version() {
let result = B::native_model_encode_downgrade_body(
self.try_into()
.map_err(|e: anyhow::Error| native_model::DowngradeError {
msg: format!("{}", e),
source: e.into(),
})?,
version,
)?;
Ok(result)
} else {
panic!("Not implemented");
}
}
}
impl TryFrom<C> for B {
type Error = anyhow::Error;
fn try_from(_: C) -> anyhow::Result<Self> {
Ok(Self {})
}
}
impl TryFrom<B> for C {
type Error = anyhow::Error;
fn try_from(_: B) -> anyhow::Result<Self> {
Ok(Self {})
}
}
/**
I want to manage the upgrade and downgrade of native types using From and Into traits.
Let see 3 model A,B,C of a model id 1.
A is the oldest version of the model and is the version 1.
B is the intermediate version of the model and is the version 2.
C is the most recent version of the model and is the version 3.
We need to imagine that the data are serialized as a vector of bytes. The only things that we know
is the model id 1 and the version of the model.
I need to found an elegant way to deserialize the data as the most recent version of the model.
**/
#[test]
fn test_encode_downgrade() {
let x = 3;
let result = C::native_model_encode_downgrade_body(C {}, x);
dbg!(&result);
let x = 2;
let result = C::native_model_encode_downgrade_body(C {}, x);
dbg!(&result);
let x = 1;
let result = C::native_model_encode_downgrade_body(C {}, x);
dbg!(&result);
}
#[test]
fn test_decode_upgrade() {
let x = 3;
let result = C::native_model_decode_upgrade_body(vec![], x);
dbg!(&result);
let x = 2;
let result = C::native_model_decode_upgrade_body(vec![], x);
dbg!(&result);
let x = 1;
let result = C::native_model_decode_upgrade_body(vec![], x);
dbg!(&result);
}
fn native_model_decode_upgrade<T>(
_data: Vec<u8>,
model_id: u32,
version: u32,
) -> native_model::Result<T>
where
T: Model,
{
if model_id == T::native_model_id() {
T::native_model_decode_upgrade_body(_data, version)
} else {
panic!("The model id {} not supported", model_id);
}
}
#[test]
fn test_decode_upgrade_c() {
let x = 3;
let result: C = native_model_decode_upgrade(vec![], 1, x).unwrap();
dbg!(&result);
let x = 2;
let result: C = native_model_decode_upgrade(vec![], 1, x).unwrap();
dbg!(&result);
let x = 1;
let result: C = native_model_decode_upgrade(vec![], 1, x).unwrap();
dbg!(&result);
}
#[test]
fn test_decode_upgrade_b() {
let x = 2;
let result: B = native_model_decode_upgrade(vec![], 1, x).unwrap();
dbg!(&result);
// let x = 2;
// let result: B = native_model_decode_upgrade(vec![], 1, x).unwrap();
// dbg!(&result);
}

View File

@@ -0,0 +1,34 @@
use bincode;
use bincode::{Decode, Encode};
fn native_model_encode_body<T: bincode::Encode>(
model: &T,
) -> Result<Vec<u8>, bincode::error::EncodeError> {
{
bincode::encode_to_vec(model, bincode::config::standard())
}
}
fn native_model_decode_body<T: bincode::Decode>(
data: Vec<u8>,
) -> Result<T, bincode::error::DecodeError> {
{
bincode::decode_from_slice(&data, bincode::config::standard()).map(|(result, _)| result)
}
}
use native_model_macro::native_model;
#[derive(Encode, Decode, PartialEq, Debug)]
#[native_model(id = 1, version = 1)]
struct DotV1(u32, u32);
#[test]
fn test_bincode_encode_decode() {
// Application 1
let dot = DotV1(1, 2);
let bytes = native_model::encode(&dot).unwrap();
// Application 1
let (dot, _) = native_model::decode::<DotV1>(bytes).unwrap();
assert_eq!(dot, DotV1(1, 2));
}

View File

@@ -0,0 +1,34 @@
use bincode;
use serde::{Deserialize, Serialize};
fn native_model_encode_body<T: Serialize>(
model: &T,
) -> Result<Vec<u8>, bincode::error::EncodeError> {
{
bincode::serde::encode_to_vec(model, bincode::config::standard())
}
}
fn native_model_decode_body<T: for<'a> Deserialize<'a>>(
data: Vec<u8>,
) -> Result<T, bincode::error::DecodeError> {
{
Ok(bincode::serde::decode_from_slice(&data, bincode::config::standard())?.0)
}
}
use native_model_macro::native_model;
#[derive(Serialize, Deserialize, PartialEq, Debug)]
#[native_model(id = 1, version = 1)]
struct DotV1(u32, u32);
#[test]
fn test_bincode_serde_serialize_deserialize() {
// Application 1
let dot = DotV1(1, 2);
let bytes = native_model::encode(&dot).unwrap();
// Application 1
let (dot, _) = native_model::decode::<DotV1>(bytes).unwrap();
assert_eq!(dot, DotV1(1, 2));
}

View File

@@ -0,0 +1,2 @@
mod bincode;
mod bincode_serde;

View File

@@ -0,0 +1,104 @@
use bincode::{config, Decode, Encode};
use native_model_macro::native_model;
#[allow(dead_code)]
fn native_model_encode_body<T: Encode>(obj: &T) -> Result<Vec<u8>, bincode::error::EncodeError> {
bincode::encode_to_vec(obj, config::standard())
}
#[allow(dead_code)]
fn native_model_decode_body<T: Decode>(data: Vec<u8>) -> Result<T, bincode::error::DecodeError> {
bincode::decode_from_slice(&data, config::standard()).map(|(result, _)| result)
}
#[derive(Encode, Decode, PartialEq, Debug)]
#[native_model(id = 1, version = 1)]
struct DotV1(u32, u32);
#[derive(Encode, Decode, PartialEq, Debug)]
#[native_model(id = 1, version = 2, from = DotV1)]
struct DotV2 {
name: String,
x: u64,
y: u64,
}
impl From<DotV1> for DotV2 {
fn from(dot: DotV1) -> Self {
DotV2 {
name: "".to_string(),
x: dot.0 as u64,
y: dot.1 as u64,
}
}
}
impl From<DotV2> for DotV1 {
fn from(dot: DotV2) -> Self {
DotV1(dot.x as u32, dot.y as u32)
}
}
#[derive(Encode, Decode, PartialEq, Debug)]
#[native_model(id = 1, version = 3, try_from = (DotV2, anyhow::Error))]
struct DotV3 {
name: String,
cord: Cord,
}
#[derive(Encode, Decode, PartialEq, Debug)]
struct Cord {
x: u64,
y: u64,
}
impl TryFrom<DotV2> for DotV3 {
type Error = anyhow::Error;
fn try_from(dot: DotV2) -> Result<Self, Self::Error> {
Ok(DotV3 {
name: dot.name,
cord: Cord { x: dot.x, y: dot.y },
})
}
}
impl TryFrom<DotV3> for DotV2 {
type Error = anyhow::Error;
fn try_from(dot: DotV3) -> Result<Self, Self::Error> {
Ok(DotV2 {
name: dot.name,
x: dot.cord.x,
y: dot.cord.y,
})
}
}
#[test]
fn simple_test() {
let dot = DotV1(1, 2);
let bytes = native_model::encode(&dot).unwrap();
let (dot_decoded, _) = native_model::decode::<DotV1>(bytes.clone()).unwrap();
assert_eq!(dot, dot_decoded);
let (dot_decoded, _) = native_model::decode::<DotV2>(bytes.clone()).unwrap();
assert_eq!(
DotV2 {
name: "".to_string(),
x: 1,
y: 2
},
dot_decoded
);
let (dot_decoded, _) = native_model::decode::<DotV3>(bytes.clone()).unwrap();
assert_eq!(
DotV3 {
name: "".to_string(),
cord: Cord { x: 1, y: 2 }
},
dot_decoded
);
}

View File

@@ -0,0 +1,70 @@
use bincode;
use bincode::{Decode, Encode};
use native_model::native_model;
fn native_model_encode_body<T: bincode::Encode>(
model: &T,
) -> Result<Vec<u8>, bincode::error::EncodeError> {
{
bincode::encode_to_vec(model, bincode::config::standard())
}
}
fn native_model_decode_body<T: bincode::Decode>(
data: Vec<u8>,
) -> Result<T, bincode::error::DecodeError> {
{
bincode::decode_from_slice(&data, bincode::config::standard()).map(|(result, _)| result)
}
}
#[derive(Encode, Decode, PartialEq, Debug)]
#[native_model(id = 1, version = 1)]
struct DotV1(u32, u32);
#[derive(Encode, Decode, PartialEq, Debug)]
#[native_model(id = 1, version = 2, from = DotV1)]
struct DotV2 {
name: String,
x: u64,
y: u64,
}
impl From<DotV1> for DotV2 {
fn from(dot: DotV1) -> Self {
DotV2 {
name: "".to_string(),
x: dot.0 as u64,
y: dot.1 as u64,
}
}
}
impl From<DotV2> for DotV1 {
fn from(dot: DotV2) -> Self {
DotV1(dot.x as u32, dot.y as u32)
}
}
#[test]
fn run_example() {
// Application 1
let dot = DotV1(1, 2);
let bytes = native_model::encode(&dot).unwrap();
// Application 1 sends bytes to Application 2.
// Application 2
let (mut dot, source_version) = native_model::decode::<DotV2>(bytes).unwrap();
// Use the struct DataV2 which has more fields and a different structure.
dot.name = "Dot".to_string();
dot.x = 5;
// Encode the dot with the application 1 version in order to be compatible with it.
let bytes = native_model::encode_downgrade(dot, source_version).unwrap();
// Application 2 sends bytes to Application 1.
// Application 1
let (dot, _) = native_model::decode::<DotV1>(bytes).unwrap();
assert_eq!(dot, DotV1(5, 2));
}

3
tests/example/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
mod encode_decode;
mod example_define_model;
mod example_main;

46
tests/macro.rs Normal file
View File

@@ -0,0 +1,46 @@
use bincode::{config, Decode, Encode};
use native_model::Model;
use native_model_macro::native_model;
#[allow(dead_code)]
fn native_model_encode_body<T: Encode>(obj: &T) -> Result<Vec<u8>, bincode::error::EncodeError> {
bincode::encode_to_vec(obj, config::standard())
}
#[allow(dead_code)]
fn native_model_decode_body<T: Decode>(data: Vec<u8>) -> Result<T, bincode::error::DecodeError> {
bincode::decode_from_slice(&data, config::standard()).map(|(result, _)| result)
}
#[derive(Debug, Encode, Decode)]
#[native_model(id = 1, version = 1)]
struct Foo1 {
x: i32,
}
#[derive(Debug, Encode, Decode)]
#[native_model(id = 1, version = 2, from = Foo1)]
struct Foo2 {
x: i32,
}
impl From<Foo1> for Foo2 {
fn from(foo1: Foo1) -> Self {
Foo2 { x: foo1.x }
}
}
impl From<Foo2> for Foo1 {
fn from(foo2: Foo2) -> Self {
Foo1 { x: foo2.x }
}
}
#[test]
fn test_simple() {
assert_eq!(Foo1::native_model_id(), 1);
assert_eq!(Foo1::native_model_version(), 1);
assert_eq!(Foo2::native_model_id(), 1);
assert_eq!(Foo2::native_model_version(), 2);
}

View File

@@ -0,0 +1,149 @@
use bincode::{config, Decode, Encode};
use native_model::Model;
use native_model_macro::native_model;
fn native_model_encode_body<T: Encode>(obj: &T) -> Result<Vec<u8>, bincode::error::EncodeError> {
bincode::encode_to_vec(obj, config::standard())
}
fn native_model_decode_body<T: Decode>(data: Vec<u8>) -> Result<T, bincode::error::DecodeError> {
bincode::decode_from_slice(&data, config::standard()).map(|(result, _)| result)
}
#[derive(Debug, Encode, Decode, PartialEq)]
#[native_model(id = 1, version = 1)]
struct Foo1 {
x: i32,
}
#[derive(Debug, Encode, Decode, PartialEq)]
#[native_model(id = 1, version = 2, from = Foo1)]
struct Foo2 {
x: String,
}
impl From<Foo1> for Foo2 {
fn from(foo1: Foo1) -> Self {
Foo2 {
x: foo1.x.to_string(),
}
}
}
impl From<Foo2> for Foo1 {
fn from(foo2: Foo2) -> Self {
Foo1 {
x: foo2.x.parse::<i32>().unwrap(),
}
}
}
#[derive(Debug, Encode, Decode, PartialEq)]
#[native_model(id = 1, version = 3, from = Foo2)]
enum Foo3 {
X(i32),
}
impl From<Foo2> for Foo3 {
fn from(foo2: Foo2) -> Self {
Foo3::X(foo2.x.parse::<i32>().unwrap())
}
}
impl From<Foo3> for Foo2 {
fn from(foo3: Foo3) -> Self {
match foo3 {
Foo3::X(x) => Foo2 { x: x.to_string() },
}
}
}
#[test]
fn test_decode_foo1_to_foo2() {
let foo1 = Foo1 { x: 100 };
let foo1_encoded = foo1.native_model_encode_body().unwrap();
let foo2_decoded = Foo2::native_model_decode_upgrade_body(foo1_encoded, 1).unwrap();
assert_eq!(foo1.x.to_string(), foo2_decoded.x);
}
#[test]
fn test_decode_foo2_to_foo3() {
let foo2 = Foo2 {
x: "100".to_string(),
};
let foo2_encoded = foo2.native_model_encode_body().unwrap();
let foo3_decoded = Foo3::native_model_decode_upgrade_body(foo2_encoded, 2).unwrap();
assert_eq!(Foo3::X(100), foo3_decoded);
}
#[test]
fn test_decode_foo1_to_foo3() {
let foo1 = Foo1 { x: 100 };
let foo1_encoded = foo1.native_model_encode_body().unwrap();
let foo3_decoded = Foo3::native_model_decode_upgrade_body(foo1_encoded, 1).unwrap();
assert_eq!(Foo3::X(100), foo3_decoded);
}
#[test]
fn test_decode_foo1_to_foo1() {
let foo1 = Foo1 { x: 100 };
let foo1_encoded = foo1.native_model_encode_body().unwrap();
let foo1_decoded = Foo1::native_model_decode_upgrade_body(foo1_encoded, 1).unwrap();
assert_eq!(foo1, foo1_decoded);
}
#[test]
fn test_decode_foo2_to_foo2() {
let foo2 = Foo2 {
x: "100".to_string(),
};
let foo2_encoded = foo2.native_model_encode_body().unwrap();
let foo2_decoded = Foo2::native_model_decode_upgrade_body(foo2_encoded, 2).unwrap();
assert_eq!(foo2, foo2_decoded);
}
#[test]
fn test_decode_foo3_to_foo3() {
let foo3 = Foo3::X(100);
let foo3_encoded = foo3.native_model_encode_body().unwrap();
let foo3_decoded = Foo3::native_model_decode_upgrade_body(foo3_encoded, 3).unwrap();
assert_eq!(foo3, foo3_decoded);
}
#[test]
fn test_should_fail_decode_foo3_to_foo2() {
let foo3 = Foo3::X(100);
let foo3_encoded = foo3.native_model_encode_body().unwrap();
let foo3_decoded = Foo2::native_model_decode_upgrade_body(foo3_encoded, 3);
assert!(foo3_decoded.is_err());
assert!(matches!(
foo3_decoded.unwrap_err(),
native_model::Error::UpgradeNotSupported { from: 3, to: 2 }
));
}
#[test]
fn test_should_fail_decode_foo3_to_foo1() {
let foo3 = Foo3::X(100);
let foo3_encoded = foo3.native_model_encode_body().unwrap();
let foo3_decoded = Foo1::native_model_decode_upgrade_body(foo3_encoded, 3);
assert!(foo3_decoded.is_err());
assert!(matches!(
foo3_decoded.unwrap_err(),
native_model::Error::UpgradeNotSupported { from: 3, to: 1 }
));
}
#[test]
fn test_should_fail_decode_foo2_to_foo1() {
let foo2 = Foo2 {
x: "100".to_string(),
};
let foo2_encoded = foo2.native_model_encode_body().unwrap();
let foo2_decoded = Foo1::native_model_decode_upgrade_body(foo2_encoded, 2);
assert!(foo2_decoded.is_err());
assert!(matches!(
foo2_decoded.unwrap_err(),
native_model::Error::UpgradeNotSupported { from: 2, to: 1 }
));
}

View File

@@ -0,0 +1,48 @@
use bincode::{config, Decode, Encode};
use native_model::Model;
use native_model_macro::native_model;
fn native_model_encode_body<T: Encode>(obj: &T) -> Result<Vec<u8>, bincode::error::EncodeError> {
bincode::encode_to_vec(obj, config::standard())
}
fn native_model_decode_body<T: Decode>(data: Vec<u8>) -> Result<T, bincode::error::DecodeError> {
bincode::decode_from_slice(&data, config::standard()).map(|(result, _)| result)
}
#[derive(Debug, Encode, Decode, PartialEq)]
#[native_model(id = 1, version = 1)]
struct Foo1 {
x: i32,
}
#[derive(Debug, Encode, Decode, PartialEq)]
#[native_model(id = 1, version = 2, from = Foo1)]
struct Foo2 {
x: i32,
}
impl From<Foo1> for Foo2 {
fn from(foo1: Foo1) -> Self {
Foo2 { x: foo1.x }
}
}
impl From<Foo2> for Foo1 {
fn from(foo2: Foo2) -> Self {
Foo1 { x: foo2.x }
}
}
#[test]
fn test_simple() {
let foo1 = Foo1 { x: 100 };
let foo2 = Foo2 { x: 200 };
let foo1_encoded = foo1.native_model_encode().unwrap();
let foo2_encoded = foo2.native_model_encode().unwrap();
let (foo1_decoded, _) = Foo1::native_model_decode(foo1_encoded).unwrap();
assert!(foo1_decoded == foo1);
let (foo2_decoded, _) = Foo2::native_model_decode(foo2_encoded).unwrap();
assert!(foo2_decoded == foo2);
}

102
tests/native_model_from.rs Normal file
View File

@@ -0,0 +1,102 @@
use bincode::{config, Decode, Encode};
use native_model_macro::native_model;
fn native_model_encode_body<T: Encode>(obj: &T) -> Result<Vec<u8>, bincode::error::EncodeError> {
bincode::encode_to_vec(obj, config::standard())
}
fn native_model_decode_body<T: Decode>(data: Vec<u8>) -> Result<T, bincode::error::DecodeError> {
bincode::decode_from_slice(&data, config::standard()).map(|(result, _)| result)
}
#[derive(Debug, Encode, Decode, PartialEq)]
#[native_model(id = 1, version = 1)]
struct Foo1 {
x: i32,
}
#[derive(Debug, Encode, Decode, PartialEq)]
#[native_model(id = 1, version = 2, from = Foo1)]
struct Foo2 {
x: i32,
c: char,
}
impl From<Foo1> for Foo2 {
fn from(foo1: Foo1) -> Self {
Foo2 { x: foo1.x, c: 'a' }
}
}
impl From<Foo2> for Foo1 {
fn from(foo2: Foo2) -> Self {
Foo1 { x: foo2.x }
}
}
impl PartialEq<Foo1> for Foo2 {
fn eq(&self, other: &Foo1) -> bool {
self.x == other.x
}
}
#[test]
fn test_decode_foo1_to_foo1() {
let foo1 = Foo1 { x: 100 };
let foo1_packed = native_model::encode(&foo1).unwrap();
let (foo1_decoded, _) = native_model::decode::<Foo1>(foo1_packed.clone()).unwrap();
assert_eq!(foo1, foo1_decoded);
}
#[test]
fn test_decode_foo1_to_foo2() {
let foo1 = Foo1 { x: 100 };
let foo1_packed = native_model::encode(&foo1).unwrap();
let (foo2_decoded, _) = native_model::decode::<Foo2>(foo1_packed.clone()).unwrap();
assert_eq!(Foo2 { x: 100, c: 'a' }, foo2_decoded);
}
#[test]
fn test_encode_foo2_to_foo1() {
let foo2 = Foo2 { x: 100, c: 'a' };
let foo2_packed = native_model::encode(&foo2).unwrap();
assert_eq!(foo2_packed, vec![1, 0, 0, 0, 2, 0, 0, 0, 200, 97]);
let (foo2_decoded, _) = native_model::decode::<Foo2>(foo2_packed.clone()).unwrap();
assert_eq!(Foo2 { x: 100, c: 'a' }, foo2_decoded);
let foo1_packed = native_model::encode_downgrade(foo2, 1).unwrap();
assert_eq!(foo1_packed, vec![1, 0, 0, 0, 1, 0, 0, 0, 200]);
let (foo1_decoded, _) = native_model::decode::<Foo1>(foo1_packed.clone()).unwrap();
assert_eq!(Foo1 { x: 100 }, foo1_decoded);
}
#[test]
fn test_encode_foo1_to_foo1() {
let foo1 = Foo1 { x: 100 };
let foo1_packed = native_model::encode(&foo1).unwrap();
assert_eq!(foo1_packed, vec![1, 0, 0, 0, 1, 0, 0, 0, 200]);
let (foo1_decoded, _) = native_model::decode::<Foo1>(foo1_packed.clone()).unwrap();
assert_eq!(Foo1 { x: 100 }, foo1_decoded);
let foo1_packed = native_model::encode_downgrade(foo1, 1).unwrap();
assert_eq!(foo1_packed, vec![1, 0, 0, 0, 1, 0, 0, 0, 200]);
let (foo1_decoded, _) = native_model::decode::<Foo1>(foo1_packed.clone()).unwrap();
assert_eq!(Foo1 { x: 100 }, foo1_decoded);
}
#[test]
fn encode_decode_with_same_version() {
// Client 1
let foo1 = Foo1 { x: 100 };
let foo_packed = native_model::encode(&foo1).unwrap();
// Send foo_packed to server
// Server
let (mut foo2, version) = native_model::decode::<Foo2>(foo_packed.clone()).unwrap();
// Do something with foo2
foo2.x += 1;
let foo_packed = native_model::encode_downgrade(foo2, version).unwrap();
// Send foo_packed back to client
// Client
let (foo1_decoded, _) = native_model::decode::<Foo1>(foo_packed.clone()).unwrap();
assert_eq!(Foo1 { x: 101 }, foo1_decoded);
}

View File

@@ -0,0 +1,74 @@
use bincode::{config, Decode, Encode};
use native_model_macro::native_model;
fn native_model_encode_body<T: Encode>(obj: &T) -> Result<Vec<u8>, bincode::error::EncodeError> {
bincode::encode_to_vec(obj, config::standard())
}
fn native_model_decode_body<T: Decode>(data: Vec<u8>) -> Result<T, bincode::error::DecodeError> {
bincode::decode_from_slice(&data, config::standard()).map(|(result, _)| result)
}
#[derive(Debug, Encode, Decode, PartialEq)]
#[native_model(id = 1, version = 1)]
struct Foo1 {
x: i32,
}
#[derive(Debug, Encode, Decode, PartialEq)]
#[native_model(id = 1, version = 2, try_from = (Foo1, anyhow::Error))]
struct Foo2 {
x: i32,
}
impl TryFrom<Foo1> for Foo2 {
type Error = anyhow::Error;
fn try_from(foo1: Foo1) -> Result<Self, Self::Error> {
if foo1.x > 10 {
return Err(anyhow::anyhow!("x > 10"));
}
Ok(Foo2 { x: foo1.x })
}
}
impl TryFrom<Foo2> for Foo1 {
type Error = anyhow::Error;
fn try_from(foo2: Foo2) -> Result<Self, Self::Error> {
if foo2.x > 10 {
return Err(anyhow::anyhow!("x > 10"));
}
Ok(Foo1 { x: foo2.x })
}
}
#[test]
fn test_foo1_to_foo1() {
let foo1 = Foo1 { x: 1 };
let foo1_packed = native_model::encode(&foo1).unwrap();
let (foo1_decoded, _) = native_model::decode::<Foo1>(foo1_packed.clone()).unwrap();
assert_eq!(foo1, foo1_decoded);
}
#[test]
fn test_foo1_to_foo2() {
let foo1 = Foo1 { x: 1 };
let foo1_packed = native_model::encode(&foo1).unwrap();
let (foo2_decoded, _) = native_model::decode::<Foo2>(foo1_packed.clone()).unwrap();
assert_eq!(Foo2 { x: 1 }, foo2_decoded);
}
#[test]
fn test_foo1_to_foo2_error() {
let foo1 = Foo1 { x: 1000 };
let foo1_packed = native_model::encode(&foo1).unwrap();
let foo2_decoded = native_model::decode::<Foo2>(foo1_packed.clone());
assert!(foo2_decoded.is_err());
assert!(matches!(
foo2_decoded.unwrap_err(),
native_model::Error::UpgradeError(_)
));
}

1
tests/skeptic.rs Normal file
View File

@@ -0,0 +1 @@
include!(concat!(env!("OUT_DIR"), "/skeptic-tests.rs"));

39
version_update.sh Executable file
View File

@@ -0,0 +1,39 @@
#!/bin/bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
# Bash script to update version for native_model_macro
# Semantic release version obtained from argument
NEW_VERSION=$1
# Exit if NEW_VERSION is not set
if [ -z "$NEW_VERSION" ]; then
echo "NEW_VERSION argument not set"
exit 1
fi
# Directories containing Cargo.toml files to update
declare -a directories=("." "native_model_macro")
for directory in "${directories[@]}"
do
# Check if Cargo.toml exists in the directory
if [ -f "$directory/Cargo.toml" ]; then
echo "Updating version in $directory/Cargo.toml to $NEW_VERSION"
# Use sed to find and replace the version string
sed -i -E "s/^version = \"[0-9]+\.[0-9]+\.[0-9]+\"/version = \"$NEW_VERSION\"/g" "$directory/Cargo.toml"
# Update the dependency version for native_model_macro in native_model_macro's Cargo.toml
if [ "$directory" == "." ]; then
sed -i -E "s/native_model_macro = \{ version = \"[0-9]+\.[0-9]+\.[0-9]+\", path = \"native_model_macro\" \}/native_model_macro = { version = \"$NEW_VERSION\", path = \"native_model_macro\" }/g" "$directory/Cargo.toml"
fi
fi
done
cd "$DIR/"
# Commit
git commit --all --message "chore: update version to $NEW_VERSION"
git push