Skip to content

Commit

Permalink
#35: Add methods for deep and shallow copying DynamicObj, add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
kMutagene committed Nov 26, 2024
1 parent 9cd7d46 commit 8e6918e
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 0 deletions.
35 changes: 35 additions & 0 deletions src/DynamicObj/DynamicObj.fs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,36 @@ type DynamicObj() =
)

/// <summary>
///
/// </summary>
/// <param name="target">The target object to copy dynamic members to</param>
/// <param name="overWrite">Whether existing properties on the target object will be overwritten</param>
member this.DeepCopyDynamicPropertiesTo(target:#DynamicObj, ?overWrite) =
let overWrite = defaultArg overWrite false
let rec tryDeepCopyObj (o:obj) =
match o with
| :? DynamicObj as dyn ->
let newDyn = DynamicObj()
for kv in (dyn.GetProperties(false)) do
newDyn.SetProperty(kv.Key, tryDeepCopyObj kv.Value)
box newDyn
| :? array<DynamicObj> as dyns ->
box [|for dyn in dyns -> tryDeepCopyObj dyn :?> DynamicObj|]
| :? list<DynamicObj> as dyns ->
box [for dyn in dyns -> tryDeepCopyObj dyn :?> DynamicObj]
| :? ResizeArray<DynamicObj> as dyns ->
box (ResizeArray([for dyn in dyns -> tryDeepCopyObj dyn :?> DynamicObj]))
//| :? System.ICloneable as clonable -> clonable.Clone()
| _ -> o

this.GetProperties(false)
|> Seq.iter (fun kv ->
match target.TryGetPropertyHelper kv.Key with
| Some pi when overWrite -> pi.SetValue target (tryDeepCopyObj kv.Value)
| Some _ -> ()
| None -> target.SetProperty(kv.Key, tryDeepCopyObj kv.Value)
)
/// <summary>
/// Returns a new DynamicObj with only the dynamic properties of the original DynamicObj (sans instance properties).
///
/// Note that this function does not attempt to do any deep copying.
Expand All @@ -256,6 +286,11 @@ type DynamicObj() =
this.ShallowCopyDynamicPropertiesTo(target)
target

member this.DeepCopyDynamicProperties() =
let target = DynamicObj()
this.DeepCopyDynamicPropertiesTo(target)
target

#if !FABLE_COMPILER
// Some necessary overrides for methods inherited from System.Dynamic.DynamicObject()
//
Expand Down
1 change: 1 addition & 0 deletions tests/DynamicObject.Tests/DynamicObject.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
</PropertyGroup>

<ItemGroup>
<Compile Include="TestUtils.fs" />
<Compile Include="ReflectionUtils.fs" />
<Compile Include="Inheritance.fs" />
<Compile Include="Interface.fs" />
Expand Down
116 changes: 116 additions & 0 deletions tests/DynamicObject.Tests/DynamicObjs.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
open System
open Fable.Pyxpecto
open DynamicObj
open TestUtils

let tests_TryGetPropertyValue = testList "TryGetPropertyValue" [
testCase "NonExisting" <| fun _ ->
Expand Down Expand Up @@ -485,6 +486,120 @@ let tests_ShallowCopyDynamicProperties = testList "ShallowCopyDynamicProperties"
Expect.equal a b "copied value was not mutated via reference"
]

let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [

let constructClone (props: seq<string*obj>) =
let original = DynamicObj()
props
|> Seq.iter (fun (propertyName, propertyValue) -> original.SetProperty(propertyName, propertyValue))
let clone = original.DeepCopyDynamicProperties()
original, clone

let bulkMutate (props: seq<string*obj>) (dyn: DynamicObj) =
props |> Seq.iter (fun (propertyName, propertyValue) -> dyn.SetProperty(propertyName, propertyValue))

testList "DynamicObj" [
testList "Cloneable dynamic properties" [
testCase "primitives" <| fun _ ->
let originalProps = [
"int", box 1
"float", box 1.0
"bool", box true
"string", box "hello"
"char", box 'a'
"byte", box (byte 1)
"sbyte", box (sbyte -1)
"int16", box (int16 -1)
"uint16", box (uint16 1)
"int32", box (int32 -1)
"uint32", box (uint32 1u)
"int64", box (int64 -1L)
"uint64", box (uint64 1UL)
"single", box (single 1.0f)
"decimal", box (decimal 1M)
]
let original, clone = constructClone originalProps
let mutatedProps = [
"int", box 2
"float", box 2.0
"bool", box false
"string", box "bye"
"char", box 'b'
"byte", box (byte 2)
"sbyte", box (sbyte -2)
"int16", box (int16 -2)
"uint16", box (uint16 2)
"int32", box (int32 -2)
"uint32", box (uint32 2u)
"int64", box (int64 -2L)
"uint64", box (uint64 2UL)
"single", box (single 2.0f)
"decimal", box (decimal 2M)
]
bulkMutate mutatedProps original
Expect.notEqual original clone "Original and clone should not be equal after mutating primitive props on original"
Expect.sequenceEqual (original.GetProperties(true) |> Seq.map (fun p -> p.Key, p.Value)) mutatedProps "Original should have mutated properties"
Expect.sequenceEqual (clone.GetProperties(true) |> Seq.map (fun p -> p.Key, p.Value)) originalProps "Clone should have original properties"
testCase "DynamicObj" <| fun _ ->
let inner = DynamicObj() |> DynObj.withProperty "inner int" 2
let original, clone = constructClone ["dyn", inner]
inner.SetProperty("inner int", 1)
Expect.notEqual original clone "Original and clone should not be equal after mutating DynamicObj prop on original"
Expect.equal (original |> DynObj.getNestedPropAs<int> ["dyn";"inner int"]) 1 "Original should have mutated properties"
Expect.equal (clone |> DynObj.getNestedPropAs<int> ["dyn";"inner int"]) 2 "Clone should have original properties"
testCase "Nested DynamicObj" <| fun _ ->
let first_level = DynamicObj() |> DynObj.withProperty "lvl1" 1
let second_level = DynamicObj() |> DynObj.withProperty "lvl2" 2
first_level.SetProperty("second_level", second_level)
let original, clone = constructClone ["first_level", first_level]
second_level.SetProperty("lvl2", -1)
Expect.notEqual original clone "Original and clone should not be equal after mutating DynamicObj prop on original"
Expect.equal (original |> DynObj.getNestedPropAs<int> ["first_level";"second_level";"lvl2"]) -1 "Original should have mutated properties"
Expect.equal (clone |> DynObj.getNestedPropAs<int> ["first_level";"second_level";"lvl2"]) 2 "Clone should have original properties"
testCase "DynamicObj array" <| fun _ ->
let item1 = DynamicObj() |> DynObj.withProperty "item" 1
let item2 = DynamicObj() |> DynObj.withProperty "item" 2
let item3 = DynamicObj() |> DynObj.withProperty "item" 3
let arr = [|item1; item2; item3|]
let original, clone = constructClone ["arr", box arr]
item1.SetProperty("item", -1)
item2.SetProperty("item", -1)
item3.SetProperty("item", -1)
let originalProp = original |> DynObj.getNestedPropAs<DynamicObj array> ["arr"] |> Array.map (fun dyn -> DynObj.getNestedPropAs<int> ["item"] dyn)
let clonedProp = clone |> DynObj.getNestedPropAs<DynamicObj array> ["arr"] |> Array.map (fun dyn -> DynObj.getNestedPropAs<int> ["item"] dyn)
Expect.notEqual original clone "Original and clone should not be equal after mutating DynamicObj prop on original"
Expect.sequenceEqual originalProp [|-1; -1; -1|] "Original should have mutated properties"
Expect.equal clonedProp [|1; 2; 3|] "Clone should have original properties"
testCase "<DynamicObj list" <| fun _ ->
()
testCase "DynamicObj ResizeArray" <| fun _ ->
()
]
testList "Un-Cloneable dynamic properties" [
testCase "Class with mutable fields is reference equal" <| fun _ ->
()
]
]
testList "Derived class" [
testList "Cloneable dynamic properties" [
testCase "primitives" <| fun _ ->
()
testCase "DynamicObj" <| fun _ ->
()
testCase "DynamicObj array" <| fun _ ->
()
testCase "<DynamicObj list" <| fun _ ->
()
testCase "DynamicObj ResizeArray" <| fun _ ->
()
]
testList "Un-Cloneable dynamic properties" [
testCase "Class with mutable fields is reference equal" <| fun _ ->
()
]
]
]

let tests_Equals = testList "Equals" [
testCase "Same Object" <| fun _ ->
let a = DynamicObj()
Expand Down Expand Up @@ -583,6 +698,7 @@ let main = testList "DynamicObj (Class)" [
tests_GetProperties
tests_ShallowCopyDynamicPropertiesTo
tests_ShallowCopyDynamicProperties
tests_DeepCopyDynamicProperties
tests_Equals
tests_GetHashCode
]
34 changes: 34 additions & 0 deletions tests/DynamicObject.Tests/TestUtils.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
module TestUtils

open DynamicObj

let firstDiff s1 s2 =
let s1 = Seq.append (Seq.map Some s1) (Seq.initInfinite (fun _ -> None))
let s2 = Seq.append (Seq.map Some s2) (Seq.initInfinite (fun _ -> None))
Seq.mapi2 (fun i s p -> i,s,p) s1 s2
|> Seq.find (function |_,Some s,Some p when s=p -> false |_-> true)

module DynObj =
let inline getNestedPropAs<'T> (propTree: seq<string>) (dyn: DynamicObj) =
let props = propTree |> Seq.toList
let rec getProp (dyn: DynamicObj) (props: string list) : 'T=
match props with
| p::[] -> (dyn.GetPropertyValue(p)) |> unbox<'T>
| p::ps -> getProp (dyn.GetPropertyValue(p) |> unbox<DynamicObj>) ps
| _ -> failwith "Empty property list"
getProp dyn props

module Expect =
/// Expects the `actual` sequence to equal the `expected` one.
let sequenceEqual actual expected message =
match firstDiff actual expected with
| _,None,None -> ()
| i,Some a, Some e ->
failwithf "%s. Sequence does not match at position %i. Expected item: %O, but got %O."
message i e a
| i,None,Some e ->
failwithf "%s. Sequence actual shorter than expected, at pos %i for expected item %O."
message i e
| i,Some a,None ->
failwithf "%s. Sequence actual longer than expected, at pos %i found item %O."
message i a

0 comments on commit 8e6918e

Please sign in to comment.