Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion patchable/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,24 @@ serde_json.workspace = true
anyhow.workspace = true

[features]
default = ["serde", "cloneable"]
default = []
common = ["serde", "cloneable"]
full = ["serde", "cloneable", "impl_from"]
serde = ["patchable-macro/serde"]
cloneable = ["patchable-macro/cloneable"]
impl_from = ["patchable-macro/impl_from"]

[dev-dependencies]
thiserror.workspace = true
serde.workspace = true

[[test]]
name = "basic"

[[test]]
name = "serde"
required-features = ["serde"]

[[test]]
name = "impl_from"
required-features = ["impl_from"]
216 changes: 0 additions & 216 deletions patchable/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,219 +184,3 @@ impl<T: Patch> TryPatch for T {
Ok(())
}
}

#[cfg(test)]
pub(crate) mod test {
use std::fmt::Debug;

use super::*;
use patchable_macro::patchable_model;
use serde::{Deserialize, Serialize};

#[patchable_model]
#[derive(Clone, Default, Debug, PartialEq)]
struct FakeMeasurement<T, ClosureType> {
v: T,
#[patchable(skip)]
how: ClosureType,
}

#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
struct MeasurementResult<T>(pub T);

#[patchable_model]
#[derive(Clone, Debug)]
struct ScopedMeasurement<ScopeType, MeasurementType, MeasurementOutput> {
current_control_level: ScopeType,
#[patchable]
inner: MeasurementType,
current_base: MeasurementResult<MeasurementOutput>,
}

#[test]
fn test_scoped_peek() -> anyhow::Result<()> {
fn identity(x: &i32) -> i32 {
*x
}

let fake_measurement: FakeMeasurement<i32, fn(&i32) -> i32> = FakeMeasurement {
v: 42,
how: identity,
};
let scoped_peek0 = ScopedMeasurement {
current_control_level: 33u32,
inner: fake_measurement.clone(),
current_base: MeasurementResult(20i32),
};
let mut scoped_peek1 = ScopedMeasurement {
current_control_level: 0u32,
inner: fake_measurement.clone(),
current_base: MeasurementResult(0i32),
};
let state0 = serde_json::to_string(&scoped_peek0)?;
scoped_peek1.patch(serde_json::from_str(&state0)?);
let state1 = serde_json::to_string(&scoped_peek0)?;
assert!(state0 == state1);
Ok(())
}

#[patchable_model]
#[derive(Clone, Default, Debug)]
struct SimpleStruct {
val: i32,
}

#[test]
fn test_try_patch_blanket_impl() {
let mut s = SimpleStruct { val: 10 };
// The derived patch struct is compatible with serde.
// We use from_str to create the patch value.
let patch: <SimpleStruct as Patchable>::Patch =
serde_json::from_str(r#"{"val": 20}"#).unwrap();

// Should always succeed for `Patch` types due to the blanket impl.
let result = s.try_patch(patch);
assert!(result.is_ok());
assert_eq!(s.val, 20);
}

#[allow(dead_code)]
#[patchable_model]
#[derive(Clone, Debug, PartialEq)]
struct Inner {
value: i32,
}

#[allow(dead_code)]
#[patchable_model]
#[derive(Clone, Debug, PartialEq)]
struct Outer<InnerType> {
#[patchable]
inner: InnerType,
extra: u32,
}

// TODO: Not testing `impl_from` feature. Need fix.
#[cfg(feature = "impl_from")]
#[test]
fn test_from_struct_to_patch() {
let original = Outer {
inner: Inner { value: 42 },
extra: 7,
};

let patch: <Outer<Inner> as Patchable>::Patch = original.clone().into();
let mut target = Outer {
inner: Inner { value: 0 },
extra: 0,
};

target.patch(patch);
assert_eq!(target, original);
}

#[patchable_model]
#[derive(Clone, Debug, PartialEq, Eq)]
struct TupleStruct(i32, u32);

#[test]
fn test_tuple_struct_patch() {
let mut s = TupleStruct(1, 2);
let patch: <TupleStruct as Patchable>::Patch = serde_json::from_str(r#"[10, 20]"#).unwrap();
s.patch(patch);
assert_eq!(s, TupleStruct(10, 20));
}

#[patchable_model]
#[derive(Clone, Debug, PartialEq, Eq)]
struct UnitStruct;

#[test]
fn test_unit_struct_patch() {
let mut s = UnitStruct;
let patch: <UnitStruct as Patchable>::Patch = serde_json::from_str("null").unwrap();
s.patch(patch);
assert_eq!(s, UnitStruct);
}

#[patchable_model]
#[derive(Clone, Debug)]
struct SkipSerializingStruct {
#[patchable(skip)]
skipped: i32,
value: i32,
}

#[test]
fn test_skip_serializing_field_is_excluded() {
let mut s = SkipSerializingStruct {
skipped: 5,
value: 10,
};
let patch: <SkipSerializingStruct as Patchable>::Patch =
serde_json::from_str(r#"{"value": 42}"#).unwrap();
s.patch(patch);
assert_eq!(s.skipped, 5);
assert_eq!(s.value, 42);
}

#[derive(Debug)]
struct FallibleStruct {
value: i32,
}

#[derive(Debug, Clone)]
struct FalliblePatch(i32);

#[derive(Debug)]
struct PatchError(String);

impl std::fmt::Display for PatchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "PatchError: {}", self.0)
}
}

impl std::error::Error for PatchError {}

impl Patchable for FallibleStruct {
type Patch = FalliblePatch;
}

impl From<FallibleStruct> for FalliblePatch {
fn from(s: FallibleStruct) -> Self {
FalliblePatch(s.value)
}
}

impl TryPatch for FallibleStruct {
type Error = PatchError;

fn try_patch(&mut self, patch: Self::Patch) -> Result<(), Self::Error> {
if patch.0 < 0 {
return Err(PatchError("Value cannot be negative".to_string()));
}
self.value = patch.0;
Ok(())
}
}

#[test]
fn test_try_patch_custom_error() {
let mut s = FallibleStruct { value: 0 };

// Valid patch
assert!(s.try_patch(FalliblePatch(10)).is_ok());
assert_eq!(s.value, 10);

// Invalid patch
let result = s.try_patch(FalliblePatch(-5));
assert!(result.is_err());
assert_eq!(s.value, 10); // Should not have changed

match result {
Err(e) => assert_eq!(e.to_string(), "PatchError: Value cannot be negative"),
_ => panic!("Expected error"),
}
}
}
61 changes: 61 additions & 0 deletions patchable/tests/basic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use patchable::{Patchable, TryPatch};

#[derive(Debug)]
struct FallibleStruct {
value: i32,
}

#[derive(Debug, Clone)]
struct FallibleStructPatch(i32);

#[derive(Debug)]
struct PatchError(String);

impl std::fmt::Display for PatchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "PatchError: {}", self.0)
}
}

impl std::error::Error for PatchError {}

impl Patchable for FallibleStruct {
type Patch = FallibleStructPatch;
}

impl From<FallibleStruct> for FallibleStructPatch {
fn from(s: FallibleStruct) -> Self {
FallibleStructPatch(s.value)
}
}

impl TryPatch for FallibleStruct {
type Error = PatchError;

fn try_patch(&mut self, patch: Self::Patch) -> Result<(), Self::Error> {
if patch.0 < 0 {
return Err(PatchError("Value cannot be negative".to_string()));
}
self.value = patch.0;
Ok(())
}
}

#[test]
fn test_try_patch_custom_error() {
let mut s = FallibleStruct { value: 0 };

// Valid patch
assert!(s.try_patch(FallibleStructPatch(10)).is_ok());
assert_eq!(s.value, 10);

// Invalid patch
let result = s.try_patch(FallibleStructPatch(-5));
assert!(result.is_err());
assert_eq!(s.value, 10); // Should not have changed

match result {
Err(e) => assert_eq!(e.to_string(), "PatchError: Value cannot be negative"),
_ => panic!("Expected error"),
}
}
32 changes: 32 additions & 0 deletions patchable/tests/impl_from.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use patchable::{Patch, Patchable, patchable_model};

#[patchable_model]
#[derive(Clone, Debug, PartialEq)]
struct Inner {
value: i32,
}

#[patchable_model]
#[derive(Clone, Debug, PartialEq)]
struct Outer<InnerType> {
#[patchable]
inner: InnerType,
extra: u32,
}

#[test]
fn test_from_struct_to_patch() {
let original = Outer {
inner: Inner { value: 42 },
extra: 7,
};

let patch: <Outer<Inner> as Patchable>::Patch = original.clone().into();
let mut target = Outer {
inner: Inner { value: 0 },
extra: 0,
};

target.patch(patch);
assert_eq!(target, original);
}
Loading