Merge remote-tracking branch 'nativemodel/main' into develop

This commit is contained in:
DecDuck
2026-03-30 19:44:33 +11:00
51 changed files with 2490 additions and 0 deletions
@@ -0,0 +1,66 @@
name: Build Test Release
on:
push:
branches: [ main, next ]
pull_request:
branches: [ main, next ]
workflow_dispatch:
jobs:
build_test_common_os:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
toolchain: [stable]
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.toolchain }}
override: true
- uses: extractions/setup-just@v3
- uses: hustcer/setup-nu@v3.23
with:
version: '0.105.1'
- name: Just version
run: just --version
- name: Build
run: just build_all
- name: Test
run: just test_all
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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
with:
ref: main
fetch-depth: 0
- name: install npm
uses: actions/setup-node@v6
with:
node-version: '20'
- name: Semantic Release
uses: cycjimmy/semantic-release-action@v4
with:
dry_run: ${{ github.event_name != 'workflow_dispatch' }}
extra_plugins: |
@semantic-release/commit-analyzer
@semantic-release/release-notes-generator
@semantic-release/exec
@semantic-release/github
env:
GITHUB_TOKEN: ${{ secrets.PAT_GLOBAL }}
CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }}
@@ -0,0 +1,30 @@
name: Clippy Check
on:
push:
branches: [ main ]
pull_request:
branches: [ main, next ]
schedule:
- cron: '0 23 * * 4'
env:
RUST_BACKTRACE: full
jobs:
clippy_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- uses: extractions/setup-just@v3
- uses: hustcer/setup-nu@v3.23
with:
version: '0.105.1'
env:
GITHUB_TOKEN: ${{ secrets.PAT_GLOBAL }}
- name: Just version
run: just --version
- name: Clippy Check
run: just clippy_check
@@ -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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- uses: webiny/action-conventional-commits@v1.3.1
+30
View File
@@ -0,0 +1,30 @@
name: Fmt Check
on:
push:
branches: [ main ]
pull_request:
branches: [ main, next ]
schedule:
- cron: '0 23 * * 4'
env:
RUST_BACKTRACE: full
jobs:
fmt_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- uses: extractions/setup-just@v3
- uses: hustcer/setup-nu@v3.23
with:
version: '0.105.1'
env:
GITHUB_TOKEN: ${{ secrets.PAT_GLOBAL }}
- name: Just version
run: just --version
- name: Fmt Check
run: just fmt_check
+10
View File
@@ -0,0 +1,10 @@
/target
/Cargo.lock
# TODO: remove it used by semantic-release/exec
node_modules/
package-lock.json
package.json
/native_model_macro/target
/native_model_macro/Cargo.lock
+112
View File
@@ -0,0 +1,112 @@
{
"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"
]
}
+40
View File
@@ -0,0 +1,40 @@
[package]
name = "native_model"
version = "0.6.4"
authors = ["Vincent Herlemont <vincent@herlemont.fr>", "quexeky <git@quexeky.dev>"]
edition = "2021"
description = "A thin wrapper around serialized data which add information of identity and version."
license = "MIT"
repository = "https://github.com/Drop-OSS/native_model"
readme = "README.md"
keywords = ["serialization", "interoperability", "data-consistency", "flexibility", "performance"]
categories = ["data-structures", "encoding", "rust-patterns"]
rust-version = "1.73.0"
[workspace]
members = ["native_model_macro"]
[dependencies]
zerocopy = { version = "0.8.0", features = [ "derive"] }
thiserror = "2.0.0"
anyhow = "1.0.82"
native_model_macro = { version = "0.6.2", path = "native_model_macro" }
serde = { version = "1.0.200", features = ["derive"], optional = true }
bincode_1_3 = { package = "bincode", version = "1.3.3", optional = true }
bincode_2 = { package = "bincode", version = "2.0", features = ["serde"], optional = true }
postcard_1_0 = { package = "postcard", version = "1.0.8", features = ["alloc"], optional = true }
rmp_serde_1_3 = { package = "rmp-serde", version = "1.3", optional = true }
doc-comment = "0.3.3"
log = "0.4.27"
[dev-dependencies]
serde_json = "1.0.116"
criterion = { version = "0.8.0" }
[features]
default = ["serde", "bincode_1_3"]
[[bench]]
name = "overhead"
harness = false
+21
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.
+327
View File
@@ -0,0 +1,327 @@
# Native model
[![Crates.io](https://img.shields.io/crates/v/native_model)](https://crates.io/crates/native_model)
[![Build Test 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)
Add interoperability on the top of serialization formats like bincode, postcard etc.
See [concepts](#concepts) for more details.
## 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 (encode: ~20 ns, decode: ~40 ps). More details [here](#performance).
## Usage
```text
Application 1 (DotV1) Application 2 (DotV1 and DotV2)
| |
Encode DotV1 |--------------------------------> | Decode DotV1 to DotV2
| | Modify DotV2
Decode DotV1 | <--------------------------------| Encode DotV2 back to DotV1
| |
```
```rust
use native_model::native_model;
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, PartialEq, Debug)]
#[native_model(id = 1, version = 1)]
struct DotV1(u32, u32);
#[derive(Deserialize, Serialize, 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)
}
}
// 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 (upgrade).
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 (downgrade).
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_crate/tests/example/example_main.rs).
## Serialization format
You can use default serialization formats via the feature flags, like:
```toml
[dependencies]
native_model = { version = "0.1", features = ["bincode_2"] }
```
Each feature flag corresponds to a specific minor version of the serialization format. In order to avoid breaking
changes, the default serialization format is the oldest one.
- `bincode_1_3`: [bincode](https://docs.rs/bincode/1.3.3/bincode/) v1.3 (default)
- `bincode_2`: [bincode](https://docs.rs/bincode/2.0.0-rc.3/bincode/) v2.0.0-rc3
- `postcard_1_0`: [postcard](https://docs.rs/postcard/1.0.0/postcard/) v1.0
- `rpm_serde_1_3`: [rmp-serde](https://docs.rs/rmp-serde/1.3.0/rmp_serde/) v1.3
### Custom serialization format
Define a struct with the name you want. This struct must implement [`native_model::Encode`](https://docs.rs/native_model/latest/native_model/trait.Encode.html) and [`native_model::Decode`](https://docs.rs/native_model/latest/native_model/trait.Decode.html) traits.
Full examples:
- [bincode with encode/decode](./tests_crate/tests/example/custom_codec/bincode.rs)
- [bincode with serde](./tests_crate/tests/example/custom_codec/bincode_serde.rs)
Others examples, see the default implementations:
- [bincode v1.3](./src/codec/bincode_1_3.rs)
- [bincode v2.0 (rc)](./src/codec/bincode_2.rs)
- [postcard v1.0](./src/codec/postcard_1_0.rs)
- [rmp-serde v1.3](./src/codec/rmp_serde_1_3.rs)
### Notice
`native_model` provides implementations that rely on metadata-less formats and `serde`.
There are known issues with some `serde` advanced features such as:
- `#[serde(flatten)]`
- `#[serde(skip)]`
- `#[serde(skip_deserializing)]`
- `#[serde(skip_serializing)]`
- `#[serde(skip_serializing_if = "path")]`
- `#[serde(tag = "...")]`
- `#[serde(untagged)]`
Or types implementing similar strategies such as [`serde_json::Value`][serde_json_value].
The `rmp-serde` serialization format can optionally support them serializing structs as maps, the `RmpSerdeNamed` struct is provided to support this use-case.
[serde_json_value]: https://docs.rs/serde_json/latest/serde_json/enum.Value.html
## 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.
- `with = type`: The serialization format that you use for the Encode/Decode implementation. Setup [here](#setup-your-serialization-format).
- `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
use native_model::native_model;
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, PartialEq, Debug)]
#[native_model(id = 1, version = 1)]
struct DotV1(u32, u32);
#[derive(Deserialize, Serialize, 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.
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(Deserialize, Serialize, PartialEq, Debug)]
#[native_model(id = 1, version = 3, try_from = (DotV2, anyhow::Error))]
struct DotV3 {
name: String,
cord: Cord,
}
#[derive(Deserialize, Serialize, PartialEq, Debug)]
struct Cord {
x: u64,
y: u64,
}
// Implement the conversion between versions From<DotV2> for DotV3 and From<DotV3> for DotV2.
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,
})
}
}
```
## Codecs
`native_model` comes with several optional built-in serializer features available:
- [bincode 1.3](https://crates.io/crates/bincode/1.3.3)
- This is the default codec.
- **Warning: This codec may not work with all serde-derived types.**
- [bincode 2.0.0-rc.3](https://crates.io/crates/bincode/2.0.0-rc.3)
- Enable the `bincode_2` feature and use the `native_model::bincode_2::Bincode` attribute to have `native_db` use this crate for serializing & deserializing.
- **Warning: This codec may not work with all serde-derived types.**
- [postcard 1.0](https://crates.io/crates/postcard/1.0.8)
- Enable the `postcard_1_0` feature and use the `native_model::postcard_1_0::PostCard` attribute.
- **Warning: This codec may not work with all serde-derived types.**
- [rmp-serde 1.3](https://crates.io/crates/rmp-serde/1.3.0)
- Enable the `rmp_serde_1_3` feature and use the `native_model::rmp_serde_1_3::RmpSerde` attribute.
###### Codec example:
As example, to use `rmp-serde`:
1. In your project's `Cargo.toml` file, enable the `rmp_serde_1_3` feature for the `native_model` dependency.
- Be sure to check `crates.io` for the most recent [`native_model`](https://crates.io/crates/native_model) version number.
```toml
[dependencies]
serde = { version = "1.0", features = [ "derive" ] }
native_model = { version = "0.4", features = [ "rmp_serde_1_3" ] }
```
2. Assign the `rmp_serde_1_3` codec to your `struct` using the `with` attribute:
```rust
use native_model::native_model;
#[derive(Clone, Default, serde::Deserialize, serde::Serialize)]
#[native_model(id = 1, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
struct MyStruct {
my_string: String,
// etc.
}
```
###### Additional reading
You may also want to check out [David Koloski](https://github.com/djkoloski)'s [Rust serialization benchmarks](https://github.com/djkoloski/rust_serialization_benchmark) for help selecting the codec (i.e. `bincode_1_3`, `rmp_serde_1_3`, etc.) that's best for your project.
## Status
Early development. Not ready for production.
## Concepts
In order to understand how the native model works, you need to understand the following concepts.
- **Identity**(`id`): The identity is the unique identifier of the model. It is used to identify the model and
prevent to decode a model into the wrong Rust type.
- **Version**(`version`) The version is the version of the model. It is used to check the compatibility between two
models.
- **Encode**: The encode is the process of converting a model into a byte array.
- **Decode**: The decode is the process of converting a byte array into a model.
- **Downgrade**: The downgrade is the process of converting a model into a previous version of the model.
- **Upgrade**: The upgrade is the process of converting a model into a newer version of the model.
Under the hood, the native model is a thin wrapper around serialized data. The `id` and the `version` are twice encoded with a [`little_endian::U32`](https://docs.rs/zerocopy/latest/zerocopy/byteorder/little_endian/type.U32.html). That represents 8 bytes, that are added at the beginning of the data.
``` text
+------------------+------------------+------------------------------------+
| ID (4 bytes) | Version (4 bytes)| Data (indeterminate-length bytes) |
+------------------+------------------+------------------------------------+
```
Full example [here](tests/example/example_define_model.rs).
## Performance
Native model has
been designed to have a minimal and constant overhead. That means that the overhead is the same
whatever the size of the data. Under the hood we use the [zerocopy](https://docs.rs/zerocopy/latest/zerocopy/) crate
to avoid unnecessary copies.
👉 To know the total time of the encode/decode, you need to add the time of your serialization format.
Resume:
- **Encode**: ~20 ns
- **Decode**: ~40 ps
| data size | encode time (ns) | decode time (ps) |
|:--------------------:|:---------------------:|:-----------------------:|
| 1 B | 19.769 ns - 20.154 ns | 40.526 ps - 40.617 ps |
| 1 KiB | 19.597 ns - 19.971 ns | 40.534 ps - 40.633 ps |
| 1 MiB | 19.662 ns - 19.910 ns | 40.508 ps - 40.632 ps |
| 10 MiB | 19.591 ns - 19.980 ns | 40.504 ps - 40.605 ps |
| 100 MiB | 19.669 ns - 19.867 ns | 40.520 ps - 40.644 ps |
Benchmark of the native model overhead [here](benches/overhead.rs).
@@ -0,0 +1,41 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use native_model::Model;
use native_model_macro::native_model;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
#[native_model(id = 1, version = 1)]
struct Data(Vec<u8>);
fn wrap(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 mut encode_body = data.native_model_encode_body().unwrap();
group.bench_function(BenchmarkId::new("encode", nb_bytes), |b| {
b.iter(|| wrap(&mut encode_body))
});
// decode
let data = Data(vec![1; nb_bytes]);
let mut encode_body = native_model::encode(&data).unwrap();
group.bench_function(BenchmarkId::new("decode", nb_bytes), |b| {
b.iter(|| unwrap(&mut encode_body))
});
}
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
+14
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 $@
+79
View File
@@ -0,0 +1,79 @@
set shell := ["nu", "-c"]
default:
@just --list --unsorted;
build_no_default:
cargo build --no-default-features
build_default:
cargo build
build_serde:
cargo build --no-default-features --features serde
build_bincode_1_3:
cargo build --features bincode_1_3
build_no_default_bincode_1_3:
cargo build --no-default-features --features serde --features bincode_1_3
build_bincode_2:
cargo build --features bincode_2
build_no_default_bincode_2:
cargo build --no-default-features --features serde --features bincode_2
build_postcard_1_0:
cargo build --features postcard_1_0
build_no_default_postcard_1_0:
cargo build --no-default-features --features serde --features postcard_1_0
build_all: build_no_default build_default build_serde build_bincode_1_3 build_no_default_bincode_1_3 build_bincode_2 build_no_default_bincode_2 build_postcard_1_0 build_no_default_postcard_1_0
_tests_crate args='':
cd tests_crate; \
cargo test {{args}}
test_no_default:
@just _tests_crate '--no-default-features'
test_default:
@just _tests_crate args=''
test_bincode_1_3:
@just _tests_crate '--features bincode_1_3'
test_bincode_2:
@just _tests_crate '--features bincode_2'
test_postcard_1_0:
@just _tests_crate '--features postcard_1_0'
test_docs:
cargo test --doc --all-features
test_all: test_docs test_no_default test_default test_bincode_1_3 test_bincode_2 test_postcard_1_0
bench_overhead:
cargo bench --bench overhead
bench_all: bench_overhead
format:
cargo clippy; \
cargo fmt --all
fmt_check:
cargo fmt --all -- --check
clippy_check:
rustc --version; \
cargo clippy --version; \
cargo clippy -- -D warnings
# Format check
fc:
just fmt_check; \
just clippy_check
@@ -0,0 +1,20 @@
[package]
name = "native_model_macro"
version = "0.6.4"
authors = ["Vincent Herlemont <vincent@herlemont.fr>", "quexeky <git@quexeky.dev>"]
edition = "2018"
description = "A procedural macro for native_model"
license = "MIT"
repository = "https://github.com/Drop-OSS/native_model"
readme = "README.md"
[lib]
path = "src/lib.rs"
proc-macro = true
[dependencies]
syn = { version = "2.0.60", features = ["full"] }
quote = "1.0.36"
proc-macro2 = "1.0.81"
log = "0.4.27"
@@ -0,0 +1 @@
A procedural macro for [native_model](https://github.com/Drop-OSS/native_model).
@@ -0,0 +1,127 @@
extern crate proc_macro;
extern crate log;
mod method;
use crate::method::{
generate_native_model_decode_body, generate_native_model_decode_upgrade_body,
generate_native_model_encode_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
pub(crate) struct ModelAttributes {
pub(crate) id: Option<LitInt>,
pub(crate) version: Option<LitInt>,
// type
pub(crate) with: Option<Path>,
// type
pub(crate) from: Option<Path>,
// (type, try_from::Error type)
pub(crate) try_from: Option<(Path, Path)>,
}
impl Default for ModelAttributes {
fn default() -> Self {
ModelAttributes {
id: None,
version: None,
with: Some(syn::parse_str::<Path>("native_model::bincode_1_3::Bincode").unwrap()),
from: None,
try_from: None,
}
}
}
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("with") {
self.with = 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());
}
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.
/// - `with` = type: Required, the serialization/deserialization library that you use. Must implement `native_model::Encode` and `native_model::Decode`.
/// - `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/Drop-OSS/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(&attrs);
let native_model_decode_body_fn = generate_native_model_decode_body(&attrs);
let native_model_decode_upgrade_body_fn = generate_native_model_decode_upgrade_body(&attrs, struct_name);
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_decode_body_fn
#native_model_decode_upgrade_body_fn
}
};
gen.into()
}
@@ -0,0 +1,23 @@
use crate::ModelAttributes;
use proc_macro2::TokenStream;
use quote::quote;
pub(crate) fn generate_native_model_decode_body(attrs: &ModelAttributes) -> TokenStream {
let id = attrs.id.clone().expect("`id` is required");
let with = attrs.with.clone().expect("`with` is required");
let gen = quote! {
fn native_model_decode_body(data: Vec<u8>, id: u32) -> std::result::Result<Self, native_model::DecodeBodyError> {
if id != #id {
return Err(native_model::DecodeBodyError::MismatchedModelId);
}
use native_model::Decode;
#with::decode(data).map_err(|e| native_model::DecodeBodyError::DecodeError {
msg: format!("{}", e),
source: e.into(),
})
}
};
gen
}
@@ -0,0 +1,56 @@
use crate::ModelAttributes;
use proc_macro2::TokenStream;
use quote::quote;
use syn::Ident;
pub(crate) fn generate_native_model_decode_upgrade_body(attrs: &ModelAttributes, struct_name: &Ident) -> TokenStream {
let native_model_from = attrs.from.clone();
let native_model_try_from = attrs.try_from.clone();
let name = struct_name.to_string();
let model_from_or_try_from = if let Some(from) = native_model_from {
quote! {
::log::info!("Upgrading database {} from version {} to version {}", #name, #from::native_model_version(), Self::native_model_version());
#from::native_model_decode_upgrade_body(data, id, version).map(|a| a.into())
}
} else if let Some((try_from, error_try_from)) = native_model_try_from {
quote! {
::log::info!("Attempting to upgrade database {} from version {} to version {}", #name, #try_from::native_model_version(), Self::native_model_version());
let result = #try_from::native_model_decode_upgrade_body(data, id, version).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: version,
to: Self::native_model_version(),
})
}
};
let gen = quote! {
fn native_model_decode_upgrade_body(data: Vec<u8>, id: u32, version: u32) -> native_model::Result<Self> {
if version == Self::native_model_version() {
let result = Self::native_model_decode_body(data, id)?;
Ok(result)
} else if version < Self::native_model_version() {
#model_from_or_try_from
} else {
Err(native_model::Error::UpgradeNotSupported {
from: version,
to: Self::native_model_version(),
})
}
}
};
gen
}
@@ -0,0 +1,18 @@
use crate::ModelAttributes;
use proc_macro2::TokenStream;
use quote::quote;
pub(crate) fn generate_native_model_encode_body(attrs: &ModelAttributes) -> TokenStream {
let with = attrs.with.clone().expect("`with` is required");
let gen = quote! {
fn native_model_encode_body(&self) -> std::result::Result<Vec<u8>, native_model::EncodeBodyError> {
use native_model::Encode;
#with::encode(self).map_err(|e| native_model::EncodeBodyError {
msg: format!("{}", e),
source: e.into(),
})
}
};
gen
}
@@ -0,0 +1,17 @@
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
}
fn native_model_id_str() -> &'static str {
stringify!(#native_model_id)
}
};
gen
}
@@ -0,0 +1,11 @@
mod decode_body;
mod decode_upgrade_body;
mod encode_body;
mod id;
mod version;
pub(crate) use decode_body::*;
pub(crate) use decode_upgrade_body::*;
pub(crate) use encode_body::*;
pub(crate) use id::*;
pub(crate) use version::*;
@@ -0,0 +1,17 @@
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
}
fn native_model_version_str() -> &'static str {
stringify!(#native_model_version)
}
};
gen
}
+45
View File
@@ -0,0 +1,45 @@
{
"$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
}
],
"regexManagers": [
{
"fileMatch": [
"^\\.github/workflows/[^/]+\\.ya?ml$"
],
"matchStrings": [
"uses: hustcer/setup-nu@.*?\\n.*?version: '\\s*(?<currentValue>.*?)'"
],
"depNameTemplate": "nushell",
"datasourceTemplate": "github-releases",
"packageNameTemplate": "nushell/nushell"
}
]
}
@@ -0,0 +1,52 @@
//! [bincode 1.3](https://crates.io/crates/bincode/1.3.3) ·
//! The default codec for serializing & deserializing.
/// Used to specify that the
/// [bincode 1.3](https://crates.io/crates/bincode/1.3.3) crate is to be used
/// for serialization & deserialization.
///
/// # Warning
///
/// `bincode` [does not implement](https://github.com/bincode-org/bincode/issues/548)
/// all [serde](https://crates.io/crates/serde) features. Errors may be
/// encountered when using this with some types.
///
/// If you are encountering errors when using this codec on your types, try
/// using the `rmp_serde_1_3` codec instead.
///
/// # Basic usage
///
/// Use the [`with`](crate::native_model) attribute on your type to instruct
/// `native_model` to use `Bincode` for serialization & deserialization.
///
/// Example usage:
///
/// ```rust
/// # use native_model::*;
/// #[derive(Clone, Default, serde::Deserialize, serde::Serialize)]
/// #[native_model(id = 1, version = 1, with = native_model::bincode_1_3::Bincode)]
/// struct MyStruct {
/// my_string: String
/// }
/// ```
#[derive(Default)]
pub struct Bincode;
#[cfg(all(feature = "serde", feature = "bincode_1_3"))]
impl<T: serde::Serialize> super::Encode<T> for Bincode {
type Error = bincode_1_3::Error;
/// Serializes a type into bytes using the `bincode` `1.3` crate.
fn encode(obj: &T) -> Result<Vec<u8>, Self::Error> {
bincode_1_3::serialize(obj)
}
}
#[cfg(all(feature = "serde", feature = "bincode_1_3"))]
impl<T: for<'de> serde::Deserialize<'de>> super::Decode<T> for Bincode {
type Error = bincode_1_3::Error;
/// Deserializes a type from bytes using the `bincode` `1.3` crate.
fn decode(data: Vec<u8>) -> Result<T, Self::Error> {
bincode_1_3::deserialize(&data[..])
}
}
@@ -0,0 +1,54 @@
//! [bincode 2.0](https://crates.io/crates/bincode/2.0.1) ·
//! Enable the `bincode_2` feature and annotate your type with
//! `native_model::bincode_2::Bincode` to have `native_db` use this crate for
//! serializing & deserializing.
/// Used to specify the
/// [bincode 2.0](https://crates.io/crates/bincode/2.0.1)
/// crate for serialization & deserialization.
///
/// # Warning
///
/// `bincode` [does not implement](https://docs.rs/bincode/2.0.1/bincode/serde/index.html#known-issues)
/// all [serde](https://crates.io/crates/serde) features. Errors may be
/// encountered when using this with some types.
///
/// If you are encountering errors when using this codec on your types, try
/// using the `rmp_serde_1_3` codec instead.
///
/// # Basic usage
///
/// After enabling the `bincode_2` feature in your `Cargo.toml`, use the
/// [`with`](crate::native_model) attribute on your type to instruct
/// `native_model` to use `Bincode` for serialization & deserialization.
///
/// Example usage:
///
/// ```rust
/// # use native_model::*;
/// #[derive(Clone, Default, serde::Deserialize, serde::Serialize)]
/// #[native_model(id = 1, version = 1, with = native_model::bincode_2::Bincode)]
/// struct MyStruct {
/// my_string: String
/// }
/// ```
pub struct Bincode;
#[cfg(all(feature = "serde", feature = "bincode_2"))]
impl<T: serde::Serialize> super::Encode<T> for Bincode {
type Error = bincode_2::error::EncodeError;
/// Serializes a type into bytes using the `bincode` `2.0` crate.
fn encode(obj: &T) -> Result<Vec<u8>, Self::Error> {
bincode_2::serde::encode_to_vec(obj, bincode_2::config::standard())
}
}
#[cfg(all(feature = "serde", feature = "bincode_2"))]
impl<T: for<'de> serde::Deserialize<'de>> super::Decode<T> for Bincode {
type Error = bincode_2::error::DecodeError;
/// Deserializes a type from bytes using the `bincode` `2.0` crate.
fn decode(data: Vec<u8>) -> Result<T, Self::Error> {
Ok(bincode_2::serde::decode_from_slice(&data, bincode_2::config::standard())?.0)
}
}
+62
View File
@@ -0,0 +1,62 @@
//! Traits and implementations for encoding types into a series of bytes and
//! decoding bytes back into types.
#[cfg(any(all(feature = "serde", feature = "bincode_1_3"), doc))]
pub mod bincode_1_3;
#[cfg(any(all(feature = "serde", feature = "bincode_2"), doc))]
pub mod bincode_2;
#[cfg(any(all(feature = "serde", feature = "postcard_1_0"), doc))]
pub mod postcard_1_0;
#[cfg(any(all(feature = "serde", feature = "rmp_serde_1_3"), doc))]
pub mod rmp_serde_1_3;
/// Encode trait for your own encoding method.
///
/// Example:
/// ```rust
/// use bincode_2::{error::EncodeError,serde::encode_to_vec, config::standard};
/// use serde::Serialize;
/// pub struct Bincode;
///
/// impl<T: Serialize> native_model::Encode<T> for Bincode {
/// type Error = EncodeError;
/// fn encode(obj: &T) -> Result<Vec<u8>, EncodeError> {
/// Ok(encode_to_vec(&obj, standard())?)
/// }
/// }
/// ```
pub trait Encode<T> {
type Error;
/// Encodes a `T` type into a series of bytes.
///
/// # Errors
///
/// The errors returned from this function depend on the trait implementor
/// (the serializer), i.e. `bincode_2`.
fn encode(obj: &T) -> Result<Vec<u8>, Self::Error>;
}
/// Decode trait for your own decoding method.
///
/// Example:
/// ```rust
/// use bincode_2::{error::DecodeError,serde::decode_from_slice, config::standard};
/// use serde::Deserialize;
/// pub struct Bincode;
///
/// impl<T: for<'a> Deserialize<'a>> native_model::Decode<T> for Bincode {
/// type Error = DecodeError;
/// fn decode(data: Vec<u8>) -> Result<T, DecodeError> {
/// Ok(decode_from_slice(&data, standard())?.0)
/// }
/// }
pub trait Decode<T> {
type Error;
/// Decodes a series of bytes back into a `T` type.
///
/// # Errors
///
/// The errors returned from this function depend on the trait implementor
/// (the deserializer), i.e. `bincode_2`.
fn decode(data: Vec<u8>) -> Result<T, Self::Error>;
}
@@ -0,0 +1,51 @@
//! [postcard 1.0](https://crates.io/crates/postcard/1.0.8) ·
//! Enable the `postcard_1_0` feature and annotate your type with
//! `native_model::postcard_1_0::PostCard` to have `native_db` use this crate.
/// Used to specify the [postcard 1.0](https://crates.io/crates/postcard/1.0.8)
/// crate for serialization & deserialization.
///
/// # Warning
///
/// `postcard` does not implement all [serde](https://crates.io/crates/serde)
/// features. Errors may be encountered when using this with some types.
///
/// If you are encountering errors when using this codec on your types, try
/// using the `rmp_serde_1_3` codec instead.
///
/// # Basic usage
///
/// After enabling the `postcard_1_0` feature in your `Cargo.toml`, use the
/// [`with`](crate::native_model) attribute on your type to instruct
/// `native_model` to use `PostCard` for serialization & deserialization.
///
/// Example usage:
///
/// ```rust
/// # use native_model::*;
/// #[derive(Clone, Default, serde::Deserialize, serde::Serialize)]
/// #[native_model(id = 1, version = 1, with = native_model::postcard_1_0::PostCard)]
/// struct MyStruct {
/// my_string: String
/// }
/// ```
pub struct PostCard;
#[cfg(all(feature = "serde", feature = "postcard_1_0"))]
impl<T: serde::Serialize> super::Encode<T> for PostCard {
type Error = postcard_1_0::Error;
/// Serializes a type into bytes using the `postcard` `1.0` crate.
fn encode(obj: &T) -> Result<Vec<u8>, Self::Error> {
postcard_1_0::to_allocvec(obj)
}
}
#[cfg(all(feature = "serde", feature = "postcard_1_0"))]
impl<T: for<'de> serde::Deserialize<'de>> super::Decode<T> for PostCard {
type Error = postcard_1_0::Error;
/// Deserializes a type from bytes using the `postcard` `1.0` crate.
fn decode(data: Vec<u8>) -> Result<T, Self::Error> {
postcard_1_0::from_bytes(&data)
}
}
@@ -0,0 +1,91 @@
//! [rmp-serde 1.3](https://crates.io/crates/rmp-serde/1.3.0) ·
//! Enable the `rmp_serde_1_3` feature and
//! [`annotate your type`](crate::native_model) with
//! `native_model::rmp_serde_1_3::RmpSerde` or `native_model::rmp_serde_1_3::RmpSerdeNamed`
//! to have `native_db` use this crate.
/// Used to specify the
/// [rmp-serde 1.3](https://crates.io/crates/rmp-serde/1.3.0)
/// crate for serialization & deserialization, using arrays to serialize structs.
///
/// Do not use this if you plan to use serde features that skip serializing fields,
/// use [RmpSerdeNamed] instead.
///
/// # Basic usage
///
/// After enabling the `rmp_serde_1_3` feature in your `Cargo.toml`, use the
/// [`with`](crate::native_model) attribute on your type to instruct
/// `native_model` to use `RmpSerde` for serialization & deserialization.
///
/// Example usage:
///
/// ```rust
/// # use native_model::*;
/// #[derive(Clone, Default, serde::Deserialize, serde::Serialize)]
/// #[native_model(id = 1, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
/// struct MyStruct {
/// my_string: String
/// }
/// ```
pub struct RmpSerde;
#[cfg(all(feature = "serde", feature = "rmp_serde_1_3"))]
impl<T: serde::Serialize> crate::Encode<T> for RmpSerde {
type Error = rmp_serde_1_3::encode::Error;
/// Serializes a type into bytes using the `rmp-serde` `1.3` crate.
fn encode(obj: &T) -> Result<Vec<u8>, Self::Error> {
rmp_serde_1_3::encode::to_vec(obj)
}
}
#[cfg(all(feature = "serde", feature = "rmp_serde_1_3"))]
impl<T: for<'de> serde::Deserialize<'de>> crate::Decode<T> for RmpSerde {
type Error = rmp_serde_1_3::decode::Error;
/// Deserializes a type from bytes using the `rmp-serde` `1.3` crate.
fn decode(data: Vec<u8>) -> Result<T, Self::Error> {
rmp_serde_1_3::decode::from_slice(&data)
}
}
/// Used to specify the
/// [rmp-serde 1.3](https://crates.io/crates/rmp-serde/1.3.0)
/// crate for serialization & deserialization, using maps to serialize structs.
///
/// # Basic usage
///
/// After enabling the `rmp_serde_1_3` feature in your `Cargo.toml`, use the
/// [`with`](crate::native_model) attribute on your type to instruct
/// `native_model` to use `RmpSerdeNamed` for serialization & deserialization.
///
/// Example usage:
///
/// ```rust
/// # use native_model::*;
/// #[derive(Clone, Default, serde::Deserialize, serde::Serialize)]
/// #[native_model(id = 1, version = 1, with = native_model::rmp_serde_1_3::RmpSerdeNamed)]
/// struct MyStruct {
/// #[serde(skip_serializing_if = "String::is_empty")]
/// my_string: String
/// }
/// ```
pub struct RmpSerdeNamed;
#[cfg(all(feature = "serde", feature = "rmp_serde_1_3"))]
impl<T: serde::Serialize> crate::Encode<T> for RmpSerdeNamed {
type Error = rmp_serde_1_3::encode::Error;
/// Serializes a type into bytes using the `rmp-serde` `1.3` crate.
fn encode(obj: &T) -> Result<Vec<u8>, Self::Error> {
rmp_serde_1_3::encode::to_vec_named(obj)
}
}
#[cfg(all(feature = "serde", feature = "rmp_serde_1_3"))]
impl<T: for<'de> serde::Deserialize<'de>> crate::Decode<T> for RmpSerdeNamed {
type Error = rmp_serde_1_3::decode::Error;
/// Deserializes a type from bytes using the `rmp-serde` `1.3` crate.
fn decode(data: Vec<u8>) -> Result<T, Self::Error> {
rmp_serde_1_3::decode::from_slice(&data)
}
}
+9
View File
@@ -0,0 +1,9 @@
use zerocopy::little_endian::U32;
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
#[derive(FromBytes, IntoBytes, Immutable, KnownLayout, Debug)]
#[repr(C)]
pub struct Header {
pub(crate) id: U32,
pub(crate) version: U32,
}
+184
View File
@@ -0,0 +1,184 @@
//! `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. Mode details [here](https://github.com/Drop-OSS/native_model#setup-your-serialization-format).
//! - **Minimal Performance Overhead**: Current performance has a minimal overhead see [performance](https://github.com/Drop-OSS/native_model#performance) section.
//! - **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. Mode details [here](https://github.com/Drop-OSS/native_model#setup-your-serialization-format).
//! - **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/Drop-OSS/native_model) file.
#[cfg(doctest)]
#[macro_use]
extern crate doc_comment;
#[cfg(doctest)]
doc_comment! {
include_str!("../README.md")
}
#[cfg(any(
feature = "serde",
feature = "bincode_1_3",
feature = "bincode_2",
feature = "postcard_1_0",
feature = "rmp_serde_1_3",
doc
))]
mod codec;
#[cfg(any(
feature = "serde",
feature = "bincode_1_3",
feature = "bincode_2",
feature = "postcard_1_0",
feature = "rmp_serde_1_3",
doc
))]
pub use codec::*;
mod header;
pub mod wrapper;
// 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 enum DecodeBodyError {
#[error("Mismatched model id")]
MismatchedModelId,
#[error("Decode error: {msg}")]
DecodeError {
msg: String,
#[source]
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/Drop-OSS/native_model) file.
/// - other [examples](https://github.com/Drop-OSS/native_model/tree/master/tests/example)
///
/// # Errors
///
/// The errors returned from this function depend on the [`Encode`] trait
/// implementor (the serializer), i.e. `bincode_2`.
pub fn encode<T: crate::Model>(model: &T) -> Result<Vec<u8>> {
T::native_model_encode(model)
}
/// Allows to decode a [`native_model`] from a [`Vec<u8>`] and returns the version ([`u32`]).
/// See examples:
/// - [README.md](https://github.com/Drop-OSS/native_model) file.
/// - other [examples](https://github.com/Drop-OSS/native_model/tree/master/tests/example)
///
/// # Errors
///
/// The errors returned from this function depend on the [`Decode`] trait
/// implementor (the deserializer), i.e. `bincode_2`.
pub fn decode<T: crate::Model>(data: Vec<u8>) -> Result<(T, u32)> {
T::native_model_decode(data)
}
pub trait Model: Sized {
fn native_model_id() -> u32;
fn native_model_id_str() -> &'static str;
fn native_model_version() -> u32;
fn native_model_version_str() -> &'static str;
// --------------- Decode ---------------
fn native_model_decode_body(data: Vec<u8>, id: u32) -> DecodeResult<Self>;
fn native_model_decode_upgrade_body(data: Vec<u8>, id: u32, version: u32) -> Result<Self>;
fn native_model_decode(data: impl AsRef<[u8]>) -> Result<(Self, u32)> {
let native_model = crate::Wrapper::deserialize(data.as_ref()).unwrap();
let source_id = native_model.get_id();
let source_version = native_model.get_version();
let result = Self::native_model_decode_upgrade_body(
native_model.value().to_vec(),
source_id,
source_version,
)?;
Ok((result, source_version))
}
// --------------- Encode ---------------
fn native_model_encode_body(&self) -> EncodeResult<Vec<u8>>;
fn native_model_encode(&self) -> Result<Vec<u8>> {
let mut data = self.native_model_encode_body()?;
let data = crate::native_model_encode(
&mut data,
Self::native_model_id(),
Self::native_model_version(),
);
Ok(data)
}
}
+71
View File
@@ -0,0 +1,71 @@
use crate::header::Header;
use zerocopy::little_endian::U32;
use zerocopy::{IntoBytes, Ref, SplitByteSlice, SplitByteSliceMut};
pub struct Wrapper<T: SplitByteSlice> {
header: Ref<T, Header>,
value: T,
}
impl<T: SplitByteSlice> Wrapper<T> {
pub fn deserialize(packed: T) -> Option<Self> {
let (header_lv, rest) = Ref::<_, Header>::from_prefix(packed).ok()?;
let native_model = Self {
header: header_lv,
value: rest,
};
Some(native_model)
}
pub const fn value(&self) -> &T {
&self.value
}
pub fn get_type_id(&self) -> u32 {
self.header.id.get()
}
pub fn get_id(&self) -> u32 {
self.header.id.get()
}
pub fn get_version(&self) -> u32 {
self.header.version.get()
}
}
impl<T: SplitByteSliceMut> Wrapper<T> {
pub fn set_type_id(&mut self, type_id: u32) {
self.header.id = U32::new(type_id);
}
pub fn set_version(&mut self, version: u32) {
self.header.version = U32::new(version);
}
}
pub fn native_model_encode(data: &mut Vec<u8>, type_id: u32, version: u32) -> Vec<u8> {
let header = Header {
id: U32::new(type_id),
version: U32::new(version),
};
let mut header = header.as_bytes().to_vec();
header.append(data);
header
}
#[cfg(test)]
mod tests {
use crate::{native_model_encode, Wrapper};
#[test]
fn native_model_deserialize_with_body() {
let mut data = vec![0u8; 8];
let data = 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);
}
}
@@ -0,0 +1,2 @@
/target
/Cargo.lock
@@ -0,0 +1,20 @@
[package]
name = "tests_crate"
version = "0.1.0"
edition = "2021"
[workspace]
[dependencies]
native_model = { path = "../", no-default-features = true }
serde = { version = "1.0.200", features = ["derive"], optional = true }
bincode = { version = "2.0.0-rc.3", features = ["serde"] , optional = true }
postcard = { version = "1.0.8", features = ["alloc"], optional = true }
anyhow = "1.0.82"
[features]
default = ["bincode_1_3"]
bincode_1_3 = ["serde", "native_model/bincode_1_3"]
bincode_2 = ["serde", "native_model/bincode_2", "bincode"]
postcard_1_0 = ["serde", "native_model/postcard_1_0", "postcard"]
@@ -0,0 +1 @@
mod example;
@@ -0,0 +1,34 @@
use bincode;
use bincode::{config, Decode, Encode};
pub struct Bincode;
impl<T: bincode::Encode> native_model::Encode<T> for Bincode {
type Error = bincode::error::EncodeError;
fn encode(obj: &T) -> Result<Vec<u8>, bincode::error::EncodeError> {
bincode::encode_to_vec(obj, config::standard())
}
}
impl<T: bincode::Decode> native_model::Decode<T> for Bincode {
type Error = bincode::error::DecodeError;
fn decode(data: Vec<u8>) -> Result<T, bincode::error::DecodeError> {
bincode::decode_from_slice(&data, config::standard()).map(|(result, _)| result)
}
}
use native_model::native_model;
#[derive(Encode, Decode, PartialEq, Debug)]
#[native_model(id = 1, version = 1, with = Bincode)]
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));
}
@@ -0,0 +1,34 @@
use bincode;
use serde::{Deserialize, Serialize};
pub struct Bincode;
impl<T: Serialize> native_model::Encode<T> for Bincode {
type Error = bincode::error::EncodeError;
fn encode(obj: &T) -> Result<Vec<u8>, bincode::error::EncodeError> {
bincode::serde::encode_to_vec(obj, bincode::config::standard())
}
}
impl<T: for<'a> Deserialize<'a>> native_model::Decode<T> for Bincode {
type Error = bincode::error::DecodeError;
fn decode(data: Vec<u8>) -> Result<T, bincode::error::DecodeError> {
Ok(bincode::serde::decode_from_slice(&data, bincode::config::standard())?.0)
}
}
use native_model::native_model;
#[derive(Serialize, Deserialize, PartialEq, Debug)]
#[native_model(id = 1, version = 1, with = Bincode)]
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));
}
@@ -0,0 +1,2 @@
mod bincode;
mod bincode_serde;
@@ -0,0 +1,19 @@
#![cfg(feature = "bincode_1_3")]
use native_model::native_model;
use serde::Deserialize;
use serde::Serialize;
#[derive(Serialize, Deserialize, PartialEq, Debug)]
#[native_model(id = 1, version = 1, with = native_model::bincode_1_3::Bincode)]
struct Example {
a: u32,
b: u32,
}
#[test]
fn encode_decode() {
let example = Example { a: 1, b: 2 };
let bytes = native_model::encode(&example).unwrap();
let (example, _) = native_model::decode::<Example>(bytes).unwrap();
assert_eq!(example, Example { a: 1, b: 2 });
}
@@ -0,0 +1,19 @@
#![cfg(feature = "bincode_2")]
use native_model::{native_model};
use serde::Deserialize;
use serde::Serialize;
#[derive(Serialize, Deserialize, PartialEq, Debug)]
#[native_model(id = 1, version = 1, with = native_model::bincode_2::Bincode)]
struct Example {
a: u32,
b: u32,
}
#[test]
fn encode_decode() {
let example = Example { a: 1, b: 2 };
let bytes = native_model::encode(&example).unwrap();
let (example, _) = native_model::decode::<Example>(bytes).unwrap();
assert_eq!(example, Example { a: 1, b: 2 });
}
@@ -0,0 +1,19 @@
#![cfg(feature = "bincode_1_3")]
use native_model::native_model;
use serde::Deserialize;
use serde::Serialize;
#[derive(Serialize, Deserialize, PartialEq, Debug)]
#[native_model(id = 1, version = 1)]
struct Example {
a: u32,
b: u32,
}
#[test]
fn encode_decode() {
let example = Example { a: 1, b: 2 };
let bytes = native_model::encode(&example).unwrap();
let (example, _) = native_model::decode::<Example>(bytes).unwrap();
assert_eq!(example, Example { a: 1, b: 2 });
}
@@ -0,0 +1,4 @@
mod default;
mod bincode_1_3;
mod bincode_2;
mod postcard_1_0;
@@ -0,0 +1,20 @@
#![cfg(feature = "postcard_1_0")]
use native_model::{native_model};
use serde::Deserialize;
use serde::Serialize;
#[derive(Serialize)]
#[derive(Deserialize, PartialEq, Debug)]
#[native_model(id = 1, version = 1, with = native_model::postcard_1_0::PostCard)]
struct Example {
a: u32,
b: u32,
}
#[test]
fn encode_decode() {
let example = Example { a: 1, b: 2 };
let bytes = native_model::encode(&example).unwrap();
let (example, _) = native_model::decode::<Example>(bytes).unwrap();
assert_eq!(example, Example { a: 1, b: 2 });
}
@@ -0,0 +1,111 @@
#![cfg(feature = "bincode")]
use bincode::{config, Decode, Encode};
use native_model::native_model;
pub struct Bincode;
impl<T: bincode::Encode> native_model::Encode<T> for Bincode {
type Error = bincode::error::EncodeError;
fn encode(obj: &T) -> Result<Vec<u8>, bincode::error::EncodeError> {
bincode::encode_to_vec(obj, config::standard())
}
}
impl<T: bincode::Decode<()>> native_model::Decode<T> for Bincode {
type Error = bincode::error::DecodeError;
fn 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, with = Bincode)]
struct DotV1(u32, u32);
#[derive(Encode, Decode, PartialEq, Debug)]
#[native_model(id = 1, version = 2, with = Bincode, 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, with = Bincode, 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
);
}
@@ -0,0 +1,54 @@
#![cfg(feature = "bincode_1_3")]
use native_model::native_model;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug)]
#[native_model(id = 1, version = 1)]
struct DotV1(u32, u32);
#[derive(Serialize, Deserialize, 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));
}
@@ -0,0 +1,3 @@
mod default_codec;
mod example_define_model;
mod example_main;
@@ -0,0 +1,46 @@
#![cfg(feature = "bincode_1_3")]
use serde::{Deserialize, Serialize};
use native_model::{native_model, Model};
#[derive(Debug, Serialize, Deserialize)]
#[native_model(id = 1, version = 1)]
struct Foo1 {
x: i32,
}
#[derive(Debug, Serialize, Deserialize)]
#[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 get_id_version_int() {
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);
}
#[test]
fn get_id_version_str() {
assert_eq!(Foo1::native_model_id_str(), "1");
assert_eq!(Foo1::native_model_version_str(), "1");
assert_eq!(Foo2::native_model_id_str(), "1");
assert_eq!(Foo2::native_model_version_str(), "2");
}
@@ -0,0 +1,164 @@
#![cfg(feature = "bincode_1_3")]
use native_model::native_model;
use native_model::Model;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[native_model(id = 1, version = 1)]
struct Foo1 {
x: i32,
}
#[derive(Debug, Serialize, Deserialize, 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, Serialize, Deserialize, 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, 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, 1, 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, 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, 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, 1, 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, 1, 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, 1, 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, 1, 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, 1, 2);
assert!(foo2_decoded.is_err());
assert!(matches!(
foo2_decoded.unwrap_err(),
native_model::Error::UpgradeNotSupported { from: 2, to: 1 }
));
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[native_model(id = 2, version = 1)]
struct Foo1Bis {
x: i32,
}
#[test]
fn test_prevent_to_decode_the_wrong_model() {
let foo1 = Foo1 { x: 100 };
let foo1_encoded = foo1.native_model_encode_body().unwrap();
let foo1_decoded = Foo1Bis::native_model_decode_upgrade_body(foo1_encoded, 1, 1);
dbg!(&foo1_decoded);
// assert!(foo1_decoded.is_err());
// assert!(matches!(
// foo1_decoded.unwrap_err(),
// native_model::Error::TypeIdMismatch {
// expected: 1,
// actual: 1
// }
// ));
}
@@ -0,0 +1,42 @@
#![cfg(feature = "bincode_1_3")]
use native_model::native_model;
use native_model::Model;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[native_model(id = 1, version = 1)]
struct Foo1 {
x: i32,
}
#[derive(Debug, Serialize, Deserialize, 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);
}
@@ -0,0 +1 @@
mod example;
@@ -0,0 +1,96 @@
#![cfg(feature = "bincode_1_3")]
use native_model::native_model;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[native_model(id = 1, version = 1)]
struct Foo1 {
x: i32,
}
#[derive(Debug, Serialize, Deserialize, 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, 100, 0, 0, 0, 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, 100, 0, 0, 0]);
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, 100, 0, 0, 0]);
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, 100, 0, 0, 0]);
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);
}
@@ -0,0 +1,68 @@
#![cfg(feature = "bincode_1_3")]
use native_model::native_model;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[native_model(id = 1, version = 1)]
struct Foo1 {
x: i32,
}
#[derive(Debug, Serialize, Deserialize, 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(_)
));
}
+39
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