Skip to content

Commit

Permalink
Added pipelines and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Nawy committed Mar 7, 2024
1 parent bf54770 commit e29a483
Show file tree
Hide file tree
Showing 3 changed files with 237 additions and 6 deletions.
36 changes: 36 additions & 0 deletions .github/workflows/ci.yml
@@ -0,0 +1,36 @@
name: Rust

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

env:
CARGO_TERM_COLOR: always

jobs:
build:
name: Build
runs-on: self-hosted
timeout-minutes: 50
steps:
- uses: actions/checkout@v4
- run: cargo build --verbose

test:
name: Test
runs-on: self-hosted
timeout-minutes: 50
steps:
- uses: actions/checkout@v4
- run: cargo test -- verbose

clippy:
name: Clippy
runs-on: self-hosted
timeout-minutes: 50
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@clippy
- run: cargo clippy -- -Aclippy::style -Dclippy::perf -Dwarnings
11 changes: 11 additions & 0 deletions README.md
@@ -1,6 +1,17 @@
# Serde Flow
The `serde_flow` is a Rust library that helps manage changes in serialized data formats during software development. As data structures evolve over time, the library makes it easy to migrate files and maintain compatibility across different versions of your application's data structures, similar to how database migration tools handle schema evolution.

```toml
[dependencies]
serde_flow = "1.0.0"
```

# Basics
Serde Flow primarily consists of three major components:
1. `#[derive(Flow)]`: To utilize Serde Flow, you must annotate your class with `serde_flow::Flow`. This annotation serves as a signal to the library that the class is eligible for data migration.
2. `#[flow(variant = N)]`: Utilize this annotation to specify the version of the entity. Simply replace N with a `u16` number that represents the version. This helps in managing different versions of your data structures efficiently.
3. `#[variants(StructA, StructB, ...)]` (*Optional*): This annotation is optional but highly recommended for comprehensive data migration management. Here, you list the structs that are essential for migrating into the struct highlighted with this annotation. *To ensure, you need to implement `From<VariantStruct>` for all structs listed in `#[variants(..)]`*.

# Usage
```rust

Expand Down
196 changes: 190 additions & 6 deletions test_suite/src/async_migration.rs
@@ -1,4 +1,5 @@
use serde::{Deserialize, Serialize};
use serde_flow::error::SerdeFlowError;
use serde_flow::{encoder::bincode, flow::FileAsync, flow::FileMigrateAsync, Flow};
use tempfile::tempdir;

Expand Down Expand Up @@ -63,7 +64,7 @@ async fn test_save_to_path() {
}

#[tokio::test]
async fn test_load_from_path() {
async fn test_load_from_path() -> Result<(), SerdeFlowError> {
let car_v2 = CarV2 {
brand: "BMW".to_string(),
model: "x3".to_string(),
Expand All @@ -75,13 +76,196 @@ async fn test_load_from_path() {

car_v2
.save_to_path_async::<bincode::Encoder>(path.as_path())
.await
.unwrap();
.await?;

let car = Car::load_from_path_async::<bincode::Encoder>(path.as_path())
.await
.unwrap();
let car = Car::load_from_path_async::<bincode::Encoder>(path.as_path()).await?;

assert_eq!(car.name, "BMW x3".to_string());
assert_eq!(car.price, "$45000".to_string());
Ok(())
}

#[derive(Serialize, Deserialize, Flow)]
#[flow(variant = 4, file(nonbloking))]
pub struct CarNoMigration {
pub name: String,
pub price: String,
}

#[tokio::test]
async fn test_load_from_path_variant_not_found() -> Result<(), SerdeFlowError> {
let car_v2 = CarV2 {
brand: "BMW".to_string(),
model: "x3".to_string(),
price: 45000,
};

let temp_dir = tempdir().unwrap();
let path = temp_dir.path().to_path_buf().join("car");

car_v2
.save_to_path_async::<bincode::Encoder>(path.as_path())
.await?;

let result = CarNoMigration::load_from_path_async::<bincode::Encoder>(path.as_path()).await;
let Err(SerdeFlowError::VariantNotFound) = result else {
panic!("load_from_path no variant, must return VariantNotFound");
};
Ok(())
}

#[derive(Serialize, Deserialize, Flow)]
#[flow(variant = 4, file(nonbloking))]
#[variants(CarV1)]
pub struct CarWithMigration {
pub name: String,
pub price: String,
}

impl From<CarV1> for CarWithMigration {
fn from(value: CarV1) -> Self {
CarWithMigration {
name: format!("{} {}", value.brand, value.model),
price: value.price,
}
}
}

#[tokio::test]
async fn test_load_from_path_insufficient_variants() -> Result<(), SerdeFlowError> {
let car_v2 = CarV2 {
brand: "BMW".to_string(),
model: "x3".to_string(),
price: 45000,
};

let temp_dir = tempdir().unwrap();
let path = temp_dir.path().to_path_buf().join("car");

car_v2
.save_to_path_async::<bincode::Encoder>(path.as_path())
.await?;

let result = CarWithMigration::load_from_path_async::<bincode::Encoder>(path.as_path()).await;
let Err(SerdeFlowError::VariantNotFound) = result else {
panic!("load_from_path no variant, must return VariantNotFound");
};
Ok(())
}

#[derive(Serialize, Deserialize, Flow)]
#[flow(variant = 3, file(nonbloking))]
pub struct CarTest {
pub name: String,
pub price: String,
}

#[tokio::test]
async fn test_migrate() -> Result<(), SerdeFlowError> {
let car_v2 = CarV2 {
brand: "BMW".to_string(),
model: "x3".to_string(),
price: 45000,
};

let temp_dir = tempdir().unwrap();
let path = temp_dir.path().to_path_buf().join("car");

car_v2
.save_to_path_async::<bincode::Encoder>(path.as_path())
.await?;

// There is no migration for CarTest, loading unmigrated entity will cause an error
let result = CarTest::load_from_path_async::<bincode::Encoder>(path.as_path()).await;
assert!(result.is_err());

// migrate with entity with migrations capabilities
Car::migrate_async::<bincode::Encoder>(path.as_path()).await?;
// load with enitty, that doesn't contain migration capabilities
let car = CarTest::load_from_path_async::<bincode::Encoder>(path.as_path()).await?;

assert_eq!(car.name, "BMW x3".to_string());
assert_eq!(car.price, "$45000".to_string());
Ok(())
}

#[tokio::test]
async fn test_load_and_migrate() -> Result<(), SerdeFlowError> {
let car_v2 = CarV2 {
brand: "BMW".to_string(),
model: "x3".to_string(),
price: 45000,
};

let temp_dir = tempdir().unwrap();
let path = temp_dir.path().to_path_buf().join("car");

car_v2
.save_to_path_async::<bincode::Encoder>(path.as_path())
.await?;

// There is no migration for CarTest, loading unmigrated entity will cause an error
let result = CarTest::load_from_path_async::<bincode::Encoder>(path.as_path()).await;
assert!(result.is_err());

let migrated_car = Car::load_and_migrate_async::<bincode::Encoder>(path.as_path()).await?;
assert_eq!(migrated_car.name, "BMW x3".to_string());
assert_eq!(migrated_car.price, "$45000".to_string());

// load with enitty, that doesn't contain migration capabilities
let car = CarTest::load_from_path_async::<bincode::Encoder>(path.as_path()).await?;

assert_eq!(car.name, "BMW x3".to_string());
assert_eq!(car.price, "$45000".to_string());
Ok(())
}

#[tokio::test]
async fn test_load_from_file_not_found() -> Result<(), SerdeFlowError> {
let temp_dir = tempdir().unwrap();
let path = temp_dir.path().to_path_buf().join("not_found");

let result = Car::load_from_path_async::<bincode::Encoder>(path.as_path()).await;
let Err(SerdeFlowError::FileNotFound) = result else {
panic!("load_from_path without file, must return FileNotFound");
};
Ok(())
}

#[tokio::test]
async fn test_load_from_file_format_invalid() -> Result<(), SerdeFlowError> {
let temp_dir = tempdir().unwrap();
let path = temp_dir.path().to_path_buf().join("zero");
std::fs::write(path.as_path(), Vec::new());

let result = Car::load_from_path_async::<bincode::Encoder>(path.as_path()).await;
let Err(SerdeFlowError::FormatInvalid) = result else {
panic!("load_from_path with empty file, must return FormatInvalid");
};
Ok(())
}

#[tokio::test]
async fn test_migration_not_found() -> Result<(), SerdeFlowError> {
let temp_dir = tempdir().unwrap();
let path = temp_dir.path().to_path_buf().join("not_found");

let result = Car::migrate_async::<bincode::Encoder>(path.as_path()).await;
let Err(SerdeFlowError::FileNotFound) = result else {
panic!("Migrate without file, must return FileNotFound");
};
Ok(())
}

#[tokio::test]
async fn test_migration_format_invalid() -> Result<(), SerdeFlowError> {
let temp_dir = tempdir().unwrap();
let path = temp_dir.path().to_path_buf().join("not_found");
std::fs::write(path.as_path(), Vec::new());

let result = Car::migrate_async::<bincode::Encoder>(path.as_path()).await;
let Err(SerdeFlowError::FormatInvalid) = result else {
panic!("Migrate with empty file, must return FormatInvalid");
};
Ok(())
}

0 comments on commit e29a483

Please sign in to comment.