commit a2d63d80c6869a7e3b23c9f713d44b1d470e1135 Author: Vincent Herlemont Date: Sat Sep 2 12:49:01 2023 +0200 feat: init diff --git a/.github/workflows/build_and_test_release.yml b/.github/workflows/build_and_test_release.yml new file mode 100644 index 0000000..2cdc77d --- /dev/null +++ b/.github/workflows/build_and_test_release.yml @@ -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 }} \ No newline at end of file diff --git a/.github/workflows/conventional_commits.yml b/.github/workflows/conventional_commits.yml new file mode 100644 index 0000000..1a04683 --- /dev/null +++ b/.github/workflows/conventional_commits.yml @@ -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 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5dd8b9d --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/target +/Cargo.lock + +/native_model_macro/target +/native_model_macro/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4da3106 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "native_model" +version = "0.1.0" +authors = ["Vincent Herlemont "] +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" \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..60a8c5e --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4017dee --- /dev/null +++ b/README.md @@ -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::(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::(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(obj: &T) -> Result, dyn Error> { + ... +} + +fn native_model_decode_body(data: Vec) -> Result { + ... +} +``` +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 for DotV2 and From 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 for DotV3 and From 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. + diff --git a/README.md.skt.md b/README.md.skt.md new file mode 100644 index 0000000..969e1ff --- /dev/null +++ b/README.md.skt.md @@ -0,0 +1,140 @@ +```rust,skt-main +use bincode; +use bincode::{{Decode, Encode}}; +use native_model_macro::native_model; + +fn native_model_encode_body( + model: &T, +) -> Result, bincode::error::EncodeError> {{ + {{ + bincode::encode_to_vec(model, bincode::config::standard()) + }} +}} + +fn native_model_decode_body( + data: Vec, +) -> Result {{ + {{ + 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 for DotV2 {{ + fn from(dot: DotV1) -> Self {{ + DotV2 {{ + name: "".to_string(), + x: dot.0 as u64, + y: dot.1 as u64, + }} + }} +}} + +impl From 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(obj: &T) -> Result, bincode::error::EncodeError> {{ + bincode::encode_to_vec(obj, config::standard()) +}} + +#[allow(dead_code)] +fn native_model_decode_body(data: Vec) -> Result {{ + bincode::decode_from_slice(&data, config::standard()).map(|(result, _)| result) +}} + + +{} + +impl From for DotV2 {{ + fn from(dot: DotV1) -> Self {{ + DotV2 {{ + name: "".to_string(), + x: dot.0 as u64, + y: dot.1 as u64, + }} + }} +}} + +impl From for DotV1 {{ + fn from(dot: DotV2) -> Self {{ + DotV1(dot.x as u32, dot.y as u32) + }} +}} + +impl TryFrom for DotV3 {{ + type Error = anyhow::Error; + + fn try_from(dot: DotV2) -> Result {{ + Ok(DotV3 {{ + name: dot.name, + cord: Cord {{ x: dot.x, y: dot.y }}, + }}) + }} +}} + +impl TryFrom for DotV2 {{ + type Error = anyhow::Error; + + fn try_from(dot: DotV3) -> Result {{ + 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::(bytes.clone()).unwrap(); + assert_eq!(dot, dot_decoded); + + let (dot_decoded, _) = native_model::decode::(bytes.clone()).unwrap(); + assert_eq!( + DotV2 {{ + name: "".to_string(), + x: 1, + y: 2 + }}, + dot_decoded + ); + + let (dot_decoded, _) = native_model::decode::(bytes.clone()).unwrap(); + assert_eq!( + DotV3 {{ + name: "".to_string(), + cord: Cord {{ x: 1, y: 2 }} + }}, + dot_decoded + ); +}} + +``` \ No newline at end of file diff --git a/benches/overhead.rs b/benches/overhead.rs new file mode 100644 index 0000000..2946afe --- /dev/null +++ b/benches/overhead.rs @@ -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(obj: &T) -> Result, bincode::error::EncodeError> { + bincode::encode_to_vec(obj, bincode::config::standard()) +} + +fn native_model_decode_body(data: Vec) -> Result { + bincode::decode_from_slice(&data, bincode::config::standard()).map(|(result, _)| result) +} + +#[derive(Encode, Decode)] +#[native_model(id = 1, version = 1)] +struct Data(Vec); + +fn wrapper(data: &mut Vec) { + native_model::wrapper::native_model_encode(data, 1, 1); +} + +fn unwrap(data: &mut Vec) { + 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); diff --git a/benches/overhead_on_bincode.rs b/benches/overhead_on_bincode.rs new file mode 100644 index 0000000..48b1212 --- /dev/null +++ b/benches/overhead_on_bincode.rs @@ -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(obj: &T) -> Result, bincode::error::EncodeError> { + bincode::encode_to_vec(obj, bincode::config::standard()) +} + +fn native_model_decode_body(data: Vec) -> Result { + bincode::decode_from_slice(&data, bincode::config::standard()).map(|(result, _)| result) +} + +fn encode_with_bincode(data: &DataForBincode) -> Vec { + native_model_encode_body(data).unwrap() +} + +fn decode_with_bincode(data: Vec) -> 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 { + native_model::encode(data).unwrap() +} + +fn decode_with_native_model(data: Vec) -> DataForNativeModel { + let (data, _) = native_model::decode::(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); diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..5be349f --- /dev/null +++ b/build.rs @@ -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); + } +} diff --git a/cargo_publish.sh b/cargo_publish.sh new file mode 100755 index 0000000..36787b9 --- /dev/null +++ b/cargo_publish.sh @@ -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 $@ \ No newline at end of file diff --git a/native_model_macro/Cargo.toml b/native_model_macro/Cargo.toml new file mode 100644 index 0000000..04306f2 --- /dev/null +++ b/native_model_macro/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "native_model_macro" +version = "0.1.0" +authors = ["Vincent Herlemont "] +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" diff --git a/native_model_macro/README.md b/native_model_macro/README.md new file mode 100644 index 0000000..136027b --- /dev/null +++ b/native_model_macro/README.md @@ -0,0 +1 @@ +A procedural macro for [native_model](https://github.com/vincent-herlemont/native_model). \ No newline at end of file diff --git a/native_model_macro/src/lib.rs b/native_model_macro/src/lib.rs new file mode 100644 index 0000000..80fce1d --- /dev/null +++ b/native_model_macro/src/lib.rs @@ -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, + pub(crate) version: Option, + // type + pub(crate) from: Option, + // (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, +} + +impl Parse for TupleTryFrom { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + 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() +} diff --git a/native_model_macro/src/method/decode_body.rs b/native_model_macro/src/method/decode_body.rs new file mode 100644 index 0000000..9913787 --- /dev/null +++ b/native_model_macro/src/method/decode_body.rs @@ -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) -> Result { + native_model_decode_body(data).map_err(|e| native_model::DecodeBodyError { + msg: format!("{}", e), + source: e.into(), + }) + } + }; + + gen.into() +} \ No newline at end of file diff --git a/native_model_macro/src/method/decode_upgrade_body.rs b/native_model_macro/src/method/decode_upgrade_body.rs new file mode 100644 index 0000000..bc4ad5c --- /dev/null +++ b/native_model_macro/src/method/decode_upgrade_body.rs @@ -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, x: u32) -> native_model::Result { + 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 +} diff --git a/native_model_macro/src/method/encode_body.rs b/native_model_macro/src/method/encode_body.rs new file mode 100644 index 0000000..2fa097f --- /dev/null +++ b/native_model_macro/src/method/encode_body.rs @@ -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, native_model::EncodeBodyError> { + native_model_encode_body(self).map_err(|e| native_model::EncodeBodyError { + msg: format!("{}", e), + source: e.into(), + }) + } + }; + + gen.into() +} \ No newline at end of file diff --git a/native_model_macro/src/method/encode_downgrade_body.rs b/native_model_macro/src/method/encode_downgrade_body.rs new file mode 100644 index 0000000..76b824b --- /dev/null +++ b/native_model_macro/src/method/encode_downgrade_body.rs @@ -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> { + 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 }, diff --git a/native_model_macro/src/method/id.rs b/native_model_macro/src/method/id.rs new file mode 100644 index 0000000..6257e43 --- /dev/null +++ b/native_model_macro/src/method/id.rs @@ -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 +} diff --git a/native_model_macro/src/method/mod.rs b/native_model_macro/src/method/mod.rs new file mode 100644 index 0000000..1eb669e --- /dev/null +++ b/native_model_macro/src/method/mod.rs @@ -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::*; diff --git a/native_model_macro/src/method/version.rs b/native_model_macro/src/method/version.rs new file mode 100644 index 0000000..fd10d82 --- /dev/null +++ b/native_model_macro/src/method/version.rs @@ -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 +} diff --git a/release.config.js b/release.config.js new file mode 100644 index 0000000..b050a20 --- /dev/null +++ b/release.config.js @@ -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', + ], +}; \ No newline at end of file diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..067588e --- /dev/null +++ b/renovate.json @@ -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 + } + ] +} \ No newline at end of file diff --git a/src/header.rs b/src/header.rs new file mode 100644 index 0000000..5d09cce --- /dev/null +++ b/src/header.rs @@ -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, +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..40bb28d --- /dev/null +++ b/src/lib.rs @@ -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 = std::result::Result; + +#[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 = std::result::Result; + +#[derive(Error, Debug)] +#[error("Decode body error: {msg}")] +pub struct DecodeBodyError { + pub msg: String, + #[source] + pub source: anyhow::Error, +} + +pub type EncodeResult = std::result::Result; + +#[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`]. +/// +/// 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(model: &T) -> Result> { + T::native_model_encode(model) +} + +/// Allows to encode a [`native_model`] into a [`Vec`] 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(model: T, version: u32) -> Result> { + T::native_model_encode_downgrade(model, version) +} + +/// Allows to decode a [`native_model`] from a [`Vec`] 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(data: Vec) -> Result<(T, u32)> { + T::native_model_decode(data) +} diff --git a/src/model.rs b/src/model.rs new file mode 100644 index 0000000..f1160af --- /dev/null +++ b/src/model.rs @@ -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) -> DecodeResult + where + Self: Sized; + + fn native_model_decode_upgrade_body(data: Vec, version: u32) -> Result + where + Self: Sized; + + fn native_model_decode(data: Vec) -> 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> + where + Self: Sized; + + fn native_model_encode_downgrade_body(self, version: u32) -> Result> + where + Self: Sized; + + fn native_model_encode(&self) -> Result> + 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> + 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) + } +} diff --git a/src/wrapper.rs b/src/wrapper.rs new file mode 100644 index 0000000..cc45652 --- /dev/null +++ b/src/wrapper.rs @@ -0,0 +1,86 @@ +use crate::header::Header; +use zerocopy::little_endian::U32; +use zerocopy::{AsBytes, ByteSlice, ByteSliceMut, Ref}; + +#[derive(Debug)] +pub struct Wrapper { + header: Ref, // Deprecated: Rename LayoutVerified to Ref #203 + value: T, +} + +impl Wrapper { + pub fn deserialize(packed: T) -> Option { + 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 Wrapper { + 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, 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); + } +} diff --git a/tests/_example.rs b/tests/_example.rs new file mode 100644 index 0000000..4fb0273 --- /dev/null +++ b/tests/_example.rs @@ -0,0 +1 @@ +mod example; diff --git a/tests/_experiment.rs b/tests/_experiment.rs new file mode 100644 index 0000000..42ddfff --- /dev/null +++ b/tests/_experiment.rs @@ -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(obj: &T) -> anyhow::Result> { + let result = bincode::encode_to_vec(obj, config::standard())?; + Ok(result) +} + +// Add this function to the macro for custom deserialization +fn native_model_decode(data: Vec) -> anyhow::Result { + 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, x: u32) -> Result { + 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> + where + Self: Sized, + { + native_model_encode(self).map_err(|e| EncodeBodyError { + msg: format!("{}", e), + source: e.into(), + }) + } + + fn native_model_decode_body(data: Vec) -> DecodeResult + 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> + 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, x: u32) -> Result { + 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> + where + Self: Sized, + { + native_model_encode(self).map_err(|e| EncodeBodyError { + msg: format!("{}", e), + source: e.into(), + }) + } + + fn native_model_decode_body(data: Vec) -> DecodeResult + 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> + 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 for A { + fn from(_: B) -> Self { + Self {} + } +} + +impl From 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, x: u32) -> Result { + 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> + where + Self: Sized, + { + native_model_encode(self).map_err(|e| EncodeBodyError { + msg: format!("{}", e), + source: e.into(), + }) + } + + fn native_model_decode_body(data: Vec) -> DecodeResult + 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> + 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 for B { + type Error = anyhow::Error; + + fn try_from(_: C) -> anyhow::Result { + Ok(Self {}) + } +} + +impl TryFrom for C { + type Error = anyhow::Error; + + fn try_from(_: B) -> anyhow::Result { + 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( + _data: Vec, + model_id: u32, + version: u32, +) -> native_model::Result +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); +} diff --git a/tests/example/encode_decode/bincode.rs b/tests/example/encode_decode/bincode.rs new file mode 100644 index 0000000..d3b8ce3 --- /dev/null +++ b/tests/example/encode_decode/bincode.rs @@ -0,0 +1,34 @@ +use bincode; +use bincode::{Decode, Encode}; + +fn native_model_encode_body( + model: &T, +) -> Result, bincode::error::EncodeError> { + { + bincode::encode_to_vec(model, bincode::config::standard()) + } +} + +fn native_model_decode_body( + data: Vec, +) -> Result { + { + 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::(bytes).unwrap(); + assert_eq!(dot, DotV1(1, 2)); +} diff --git a/tests/example/encode_decode/bincode_serde.rs b/tests/example/encode_decode/bincode_serde.rs new file mode 100644 index 0000000..a4acd97 --- /dev/null +++ b/tests/example/encode_decode/bincode_serde.rs @@ -0,0 +1,34 @@ +use bincode; +use serde::{Deserialize, Serialize}; + +fn native_model_encode_body( + model: &T, +) -> Result, bincode::error::EncodeError> { + { + bincode::serde::encode_to_vec(model, bincode::config::standard()) + } +} + +fn native_model_decode_body Deserialize<'a>>( + data: Vec, +) -> Result { + { + 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::(bytes).unwrap(); + assert_eq!(dot, DotV1(1, 2)); +} diff --git a/tests/example/encode_decode/mod.rs b/tests/example/encode_decode/mod.rs new file mode 100644 index 0000000..cd23757 --- /dev/null +++ b/tests/example/encode_decode/mod.rs @@ -0,0 +1,2 @@ +mod bincode; +mod bincode_serde; diff --git a/tests/example/example_define_model.rs b/tests/example/example_define_model.rs new file mode 100644 index 0000000..6deae70 --- /dev/null +++ b/tests/example/example_define_model.rs @@ -0,0 +1,104 @@ +use bincode::{config, Decode, Encode}; +use native_model_macro::native_model; + +#[allow(dead_code)] +fn native_model_encode_body(obj: &T) -> Result, bincode::error::EncodeError> { + bincode::encode_to_vec(obj, config::standard()) +} + +#[allow(dead_code)] +fn native_model_decode_body(data: Vec) -> Result { + 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 for DotV2 { + fn from(dot: DotV1) -> Self { + DotV2 { + name: "".to_string(), + x: dot.0 as u64, + y: dot.1 as u64, + } + } +} + +impl From 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 for DotV3 { + type Error = anyhow::Error; + + fn try_from(dot: DotV2) -> Result { + Ok(DotV3 { + name: dot.name, + cord: Cord { x: dot.x, y: dot.y }, + }) + } +} + +impl TryFrom for DotV2 { + type Error = anyhow::Error; + + fn try_from(dot: DotV3) -> Result { + 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::(bytes.clone()).unwrap(); + assert_eq!(dot, dot_decoded); + + let (dot_decoded, _) = native_model::decode::(bytes.clone()).unwrap(); + assert_eq!( + DotV2 { + name: "".to_string(), + x: 1, + y: 2 + }, + dot_decoded + ); + + let (dot_decoded, _) = native_model::decode::(bytes.clone()).unwrap(); + assert_eq!( + DotV3 { + name: "".to_string(), + cord: Cord { x: 1, y: 2 } + }, + dot_decoded + ); +} diff --git a/tests/example/example_main.rs b/tests/example/example_main.rs new file mode 100644 index 0000000..5835c46 --- /dev/null +++ b/tests/example/example_main.rs @@ -0,0 +1,70 @@ +use bincode; +use bincode::{Decode, Encode}; +use native_model::native_model; + +fn native_model_encode_body( + model: &T, +) -> Result, bincode::error::EncodeError> { + { + bincode::encode_to_vec(model, bincode::config::standard()) + } +} + +fn native_model_decode_body( + data: Vec, +) -> Result { + { + 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 for DotV2 { + fn from(dot: DotV1) -> Self { + DotV2 { + name: "".to_string(), + x: dot.0 as u64, + y: dot.1 as u64, + } + } +} + +impl From 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::(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::(bytes).unwrap(); + assert_eq!(dot, DotV1(5, 2)); +} diff --git a/tests/example/mod.rs b/tests/example/mod.rs new file mode 100644 index 0000000..1c591e2 --- /dev/null +++ b/tests/example/mod.rs @@ -0,0 +1,3 @@ +mod encode_decode; +mod example_define_model; +mod example_main; diff --git a/tests/macro.rs b/tests/macro.rs new file mode 100644 index 0000000..1b1f48b --- /dev/null +++ b/tests/macro.rs @@ -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(obj: &T) -> Result, bincode::error::EncodeError> { + bincode::encode_to_vec(obj, config::standard()) +} + +#[allow(dead_code)] +fn native_model_decode_body(data: Vec) -> Result { + 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 for Foo2 { + fn from(foo1: Foo1) -> Self { + Foo2 { x: foo1.x } + } +} + +impl From 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); +} diff --git a/tests/macro_decode_decode_upgrade.rs b/tests/macro_decode_decode_upgrade.rs new file mode 100644 index 0000000..39d598c --- /dev/null +++ b/tests/macro_decode_decode_upgrade.rs @@ -0,0 +1,149 @@ +use bincode::{config, Decode, Encode}; +use native_model::Model; +use native_model_macro::native_model; + +fn native_model_encode_body(obj: &T) -> Result, bincode::error::EncodeError> { + bincode::encode_to_vec(obj, config::standard()) +} + +fn native_model_decode_body(data: Vec) -> Result { + 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 for Foo2 { + fn from(foo1: Foo1) -> Self { + Foo2 { + x: foo1.x.to_string(), + } + } +} + +impl From for Foo1 { + fn from(foo2: Foo2) -> Self { + Foo1 { + x: foo2.x.parse::().unwrap(), + } + } +} + +#[derive(Debug, Encode, Decode, PartialEq)] +#[native_model(id = 1, version = 3, from = Foo2)] +enum Foo3 { + X(i32), +} + +impl From for Foo3 { + fn from(foo2: Foo2) -> Self { + Foo3::X(foo2.x.parse::().unwrap()) + } +} + +impl From 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 } + )); +} diff --git a/tests/macro_encode_decode.rs b/tests/macro_encode_decode.rs new file mode 100644 index 0000000..37415c0 --- /dev/null +++ b/tests/macro_encode_decode.rs @@ -0,0 +1,48 @@ +use bincode::{config, Decode, Encode}; +use native_model::Model; +use native_model_macro::native_model; + +fn native_model_encode_body(obj: &T) -> Result, bincode::error::EncodeError> { + bincode::encode_to_vec(obj, config::standard()) +} + +fn native_model_decode_body(data: Vec) -> Result { + 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 for Foo2 { + fn from(foo1: Foo1) -> Self { + Foo2 { x: foo1.x } + } +} + +impl From 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); +} diff --git a/tests/native_model_from.rs b/tests/native_model_from.rs new file mode 100644 index 0000000..6e0acf1 --- /dev/null +++ b/tests/native_model_from.rs @@ -0,0 +1,102 @@ +use bincode::{config, Decode, Encode}; +use native_model_macro::native_model; + +fn native_model_encode_body(obj: &T) -> Result, bincode::error::EncodeError> { + bincode::encode_to_vec(obj, config::standard()) +} + +fn native_model_decode_body(data: Vec) -> Result { + 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 for Foo2 { + fn from(foo1: Foo1) -> Self { + Foo2 { x: foo1.x, c: 'a' } + } +} + +impl From for Foo1 { + fn from(foo2: Foo2) -> Self { + Foo1 { x: foo2.x } + } +} + +impl PartialEq 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_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::(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_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_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_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_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::(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::(foo_packed.clone()).unwrap(); + assert_eq!(Foo1 { x: 101 }, foo1_decoded); +} diff --git a/tests/native_model_try_from.rs b/tests/native_model_try_from.rs new file mode 100644 index 0000000..8cea274 --- /dev/null +++ b/tests/native_model_try_from.rs @@ -0,0 +1,74 @@ +use bincode::{config, Decode, Encode}; +use native_model_macro::native_model; + +fn native_model_encode_body(obj: &T) -> Result, bincode::error::EncodeError> { + bincode::encode_to_vec(obj, config::standard()) +} + +fn native_model_decode_body(data: Vec) -> Result { + 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 for Foo2 { + type Error = anyhow::Error; + + fn try_from(foo1: Foo1) -> Result { + if foo1.x > 10 { + return Err(anyhow::anyhow!("x > 10")); + } + + Ok(Foo2 { x: foo1.x }) + } +} + +impl TryFrom for Foo1 { + type Error = anyhow::Error; + + fn try_from(foo2: Foo2) -> Result { + 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_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::(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::(foo1_packed.clone()); + assert!(foo2_decoded.is_err()); + assert!(matches!( + foo2_decoded.unwrap_err(), + native_model::Error::UpgradeError(_) + )); +} diff --git a/tests/skeptic.rs b/tests/skeptic.rs new file mode 100644 index 0000000..ff46c9c --- /dev/null +++ b/tests/skeptic.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/skeptic-tests.rs")); diff --git a/version_update.sh b/version_update.sh new file mode 100755 index 0000000..95fd074 --- /dev/null +++ b/version_update.sh @@ -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 \ No newline at end of file