diff --git a/DynamicObj.sln b/DynamicObj.sln
index b60139b..89c4a52 100644
--- a/DynamicObj.sln
+++ b/DynamicObj.sln
@@ -9,6 +9,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{39AA72A1
ProjectSection(SolutionItems) = preProject
build.cmd = build.cmd
build.sh = build.sh
+ global.json = global.json
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".ci", ".ci", "{F82A6F26-517C-4D5E-BD4F-BFC45B5867FE}"
@@ -20,14 +21,7 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".proj", ".proj", "{C3CF2F15-81C7-4C11-889E-5FCA2C8A981D}"
ProjectSection(SolutionItems) = preProject
.gitignore = .gitignore
- build.cmd = build.cmd
- build.sh = build.sh
- .config\dotnet-tools.json = .config\dotnet-tools.json
- global.json = global.json
- key.snk = key.snk
LICENSE = LICENSE
- package.json = package.json
- pyproject.toml = pyproject.toml
README.md = README.md
RELEASE_NOTES.md = RELEASE_NOTES.md
EndProjectSection
@@ -37,7 +31,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{42AA66FC-8
docs\index.fsx = docs\index.fsx
EndProjectSection
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSharpTests", "tests\CSharpTests\CSharpTests.csproj", "{D62D0901-DB69-4C64-AC63-FBBBDCF6BC7D}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSharpTests", "tests\CSharpTests\CSharpTests.csproj", "{D62D0901-DB69-4C64-AC63-FBBBDCF6BC7D}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{988D804A-3A42-4E46-B233-B64F5C22524B}"
EndProject
diff --git a/build/BasicTasks.fs b/build/BasicTasks.fs
index 93bc5b4..3b5ec8e 100644
--- a/build/BasicTasks.fs
+++ b/build/BasicTasks.fs
@@ -152,6 +152,8 @@ let clean = BuildTask.create "Clean" [] {
++ "src/**/obj"
++ "tests/**/bin"
++ "tests/**/obj"
+ ++ "tests/**/js"
+ ++ "tests/**/py"
++ "dist"
++ ProjectInfo.netPkgDir
|> Shell.cleanDirs
diff --git a/build/TestTasks.fs b/build/TestTasks.fs
index b5d712f..d95ce7e 100644
--- a/build/TestTasks.fs
+++ b/build/TestTasks.fs
@@ -22,7 +22,7 @@ module RunTests =
let runTestsJs = BuildTask.create "runTestsJS" [clean; build] {
for path in ProjectInfo.fableTestProjects do
// transpile js files from fsharp code
- run dotnet $"fable {path} -o {path}/js" ""
+ run dotnet $"fable {path} -o {path}/js --noCache" ""
// run mocha in target path to execute tests
// "--timeout 20000" is used, because json schema validation takes a bit of time.
run node $"{path}/js/Main.js" ""
@@ -40,7 +40,7 @@ module RunTests =
let runTestsPy = BuildTask.create "runTestsPy" [clean; build] {
for path in ProjectInfo.fableTestProjects do
//transpile py files from fsharp code
- run dotnet $"fable {path} -o {path}/py --lang python" ""
+ run dotnet $"fable {path} -o {path}/py --lang python --noCache" ""
// run pyxpecto in target path to execute tests in python
run python $"{path}/py/main.py" ""
}
diff --git a/src/DynamicObj/DynamicObj.fs b/src/DynamicObj/DynamicObj.fs
index 0b65f24..43bd318 100644
--- a/src/DynamicObj/DynamicObj.fs
+++ b/src/DynamicObj/DynamicObj.fs
@@ -224,32 +224,155 @@ type DynamicObj() =
|> Seq.map (fun kv -> kv.Key)
///
- /// Copies all dynamic members of the DynamicObj to the target DynamicObj.
+ /// Copies all dynamic members of the source DynamicObj to the target DynamicObj.
///
- /// If overWrite is set to true, existing properties on the target object will be overwritten.
+ /// Note that this function does not attempt to do any deep copying.
+ /// The dynamic properties of the source will be copied as references to the target.
+ /// If any of those properties are mutable or themselves DynamicObj instances, changes to the properties on the source will be reflected in the target.
///
- /// Note that this method will not perform nested checks, e.g. if a property is a DynamicObj itself, it will not be copied recursively.
+ /// If overWrite is set to true, existing properties on the target object will be overwritten.
///
/// The target object to copy dynamic members to
/// Whether existing properties on the target object will be overwritten
- member this.CopyDynamicPropertiesTo(target:#DynamicObj, ?overWrite) =
+ member this.ShallowCopyDynamicPropertiesTo(target:#DynamicObj, ?overWrite) =
let overWrite = defaultArg overWrite false
this.GetProperties(false)
|> Seq.iter (fun kv ->
match target.TryGetPropertyHelper kv.Key with
| Some pi when overWrite -> pi.SetValue target kv.Value
- | Some _ -> failwith $"Property \"{kv.Key}\" already exists on target object and overWrite was not set to true."
+ | Some _ -> ()
| None -> target.SetProperty(kv.Key,kv.Value)
)
///
/// 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.
+ /// The dynamic properties of the source will be copied as references to the target.
+ /// If any of those properties are mutable or themselves DynamicObj instances, changes to the properties on the source will be reflected in the target.
///
- member this.CopyDynamicProperties() =
+ member this.ShallowCopyDynamicProperties() =
let target = DynamicObj()
- this.CopyDynamicPropertiesTo(target)
+ this.ShallowCopyDynamicPropertiesTo(target, true)
target
+ // internal helper function to deep copy a boxed object (if possible)
+ static member internal tryDeepCopyObj (o:obj) =
+ let rec tryDeepCopyObj (o:obj) =
+ match o with
+
+ // might be that we do not need this case, however if we remove it, some types will match the
+ // ICloneable case in transpiled code, which we'd like to prevent, so well keep it for now.
+ | :? int | :? float | :? bool
+ | :? string | :? char | :? byte
+ | :? sbyte | :? int16 | :? uint16
+ | :? int32 | :? uint32 | :? int64
+ | :? uint64 | :? single
+ -> o
+
+ #if !FABLE_COMPILER_PYTHON
+ // https://github.com/fable-compiler/Fable/issues/3971
+ | :? decimal -> o
+ #endif
+
+ | :? array as dyns ->
+ box [|for dyn in dyns -> tryDeepCopyObj dyn :?> DynamicObj|]
+ | :? list as dyns ->
+ box [for dyn in dyns -> tryDeepCopyObj dyn :?> DynamicObj]
+ | :? ResizeArray as dyns ->
+ box (ResizeArray([for dyn in dyns -> tryDeepCopyObj dyn :?> DynamicObj]))
+ #if FABLE_COMPILER_JAVASCRIPT || FABLE_COMPILER_TYPESCRIPT
+ | o when FableJS.Interfaces.implementsICloneable o -> FableJS.Interfaces.cloneICloneable o
+ #endif
+ #if FABLE_COMPILER_PYTHON
+ // https://github.com/fable-compiler/Fable/issues/3972
+ | o when FablePy.Interfaces.implementsICloneable o -> FablePy.Interfaces.cloneICloneable o
+ #endif
+ #if !FABLE_COMPILER
+ | :? System.ICloneable as clonable -> clonable.Clone()
+ #endif
+
+ | :? DynamicObj as dyn ->
+ let newDyn = DynamicObj()
+ // might want to keep instance props as dynamic props on copy
+ for kv in (dyn.GetProperties(true)) do
+ newDyn.SetProperty(kv.Key, tryDeepCopyObj kv.Value)
+ box newDyn
+ | _ -> o
+
+ tryDeepCopyObj o
+
+ ///
+ /// Attempts to deep copy the properties of the DynamicObj onto the target.
+ ///
+ /// As many properties as possible are re-instantiated as new objects, meaning the
+ /// copy has as little reference equal properties as possible.
+ ///
+ /// The nature of DynamicObj however means that it is impossible to reliably deep copy all properties, as
+ /// their type is not known on runtime and the contructors of the types are not known.
+ ///
+ /// The following cases are handled (in this precedence):
+ ///
+ /// - Basic F# types (int, float, bool, string, char, byte, sbyte, int16, uint16, int32, uint32, int64, uint64, single, decimal)
+ ///
+ /// - array<DynamicObj>, list<DynamicObj>, ResizeArray<DynamicObj>: These collections of DynamicObj are copied as a new collection with recursively deep copied elements.
+ ///
+ /// - System.ICloneable: If the property implements ICloneable, the Clone() method is called on the property.
+ ///
+ /// - DynamicObj (and derived classes): properties that are themselves DynamicObj instances are deep copied recursively.
+ /// if a derived class has static properties (e.g. instance properties), these will be copied as dynamic properties on the new instance.
+ ///
+ /// Note on Classes that inherit from DynamicObj:
+ ///
+ /// Classes that inherit from DynamicObj will match the `DynamicObj` typecheck if they do not implement ICloneable.
+ /// The deep coopied instances will be cast to DynamicObj with static/instance properties AND dynamic properties all set as dynamic properties.
+ /// It should be possible to 'recover' the original type by checking if the needed properties exist as dynamic properties,
+ /// and then passing them to the class constructor if needed.
+ ///
+ /// The target object to copy dynamic members to
+ /// Whether existing properties on the target object will be overwritten
+ member this.DeepCopyDynamicPropertiesTo(target:#DynamicObj, ?overWrite) =
+ let overWrite = defaultArg overWrite false
+
+ this.GetProperties(true)
+ |> Seq.iter (fun kv ->
+ match target.TryGetPropertyHelper kv.Key with
+ | Some pi when overWrite -> pi.SetValue target (DynamicObj.tryDeepCopyObj kv.Value)
+ | Some _ -> ()
+ | None -> target.SetProperty(kv.Key, DynamicObj.tryDeepCopyObj kv.Value)
+ )
+
+ ///
+ /// Attempts to perform a deep copy of the DynamicObj.
+ ///
+ /// On the deep copy, as many properties as possible are re-instantiated as new objects, meaning the
+ /// copy has as little reference equal properties as possible.
+ ///
+ /// The nature of DynamicObj however means that it is impossible to reliably deep copy all properties, as
+ /// their type is not known on runtime and the contructors of the types are not known.
+ ///
+ /// The following cases are handled (in this precedence):
+ ///
+ /// - Basic F# types (int, float, bool, string, char, byte, sbyte, int16, uint16, int32, uint32, int64, uint64, single, decimal)
+ ///
+ /// - array<DynamicObj>, list<DynamicObj>, ResizeArray<DynamicObj>: These collections of DynamicObj are copied as a new collection with recursively deep copied elements.
+ ///
+ /// - System.ICloneable: If the property implements ICloneable, the Clone() method is called on the property.
+ ///
+ /// - DynamicObj (and derived classes): properties that are themselves DynamicObj instances are deep copied recursively.
+ /// if a derived class has static properties (e.g. instance properties), these will be copied as dynamic properties on the new instance.
+ ///
+ /// Note on Classes that inherit from DynamicObj:
+ ///
+ /// Classes that inherit from DynamicObj will match the `DynamicObj` typecheck if they do not implement ICloneable.
+ /// The deep coopied instances will be cast to DynamicObj with static/instance properties AND dynamic properties all set as dynamic properties.
+ /// It should be possible to 'recover' the original type by checking if the needed properties exist as dynamic properties,
+ /// and then passing them to the class constructor if needed.
+ ///
+ /// The target object to copy dynamic members to
+ /// Whether existing properties on the target object will be overwritten
+ member this.DeepCopyDynamicProperties() = DynamicObj.tryDeepCopyObj this
+
#if !FABLE_COMPILER
// Some necessary overrides for methods inherited from System.Dynamic.DynamicObject()
//
diff --git a/src/DynamicObj/FableJS.fs b/src/DynamicObj/FableJS.fs
index 7d889b7..ff457ff 100644
--- a/src/DynamicObj/FableJS.fs
+++ b/src/DynamicObj/FableJS.fs
@@ -158,5 +158,14 @@ module FableJS =
getPropertyHelpers o
|> Array.map (fun h -> h.Name)
+ module Interfaces =
+
+ []
+ let implementsICloneable (o:obj) : bool =
+ jsNative
+
+ []
+ let cloneICloneable (o:obj) : obj =
+ jsNative
#endif
\ No newline at end of file
diff --git a/src/DynamicObj/FablePy.fs b/src/DynamicObj/FablePy.fs
index 4d706b9..1bc575e 100644
--- a/src/DynamicObj/FablePy.fs
+++ b/src/DynamicObj/FablePy.fs
@@ -211,5 +211,13 @@ module FablePy =
getPropertyHelpers o
|> Array.map (fun h -> h.Name)
+ module Interfaces =
+
+ []
+ let implementsICloneable (o:obj) : bool =
+ nativeOnly
+ []
+ let cloneICloneable (o:obj) : obj =
+ nativeOnly
#endif
\ No newline at end of file
diff --git a/src/DynamicObj/Playground.fsx b/src/DynamicObj/Playground.fsx
index 620bbf8..1c5f40a 100644
--- a/src/DynamicObj/Playground.fsx
+++ b/src/DynamicObj/Playground.fsx
@@ -2,6 +2,10 @@
#r "nuget: Fable.Core"
#r "nuget: Fable.Pyxpecto"
+#load "./HashCodes.fs"
+#load "./PropertyHelper.fs"
+#load "./FablePy.fs"
+#load "./FableJS.fs"
#load "./ReflectionUtils.fs"
#load "./DynamicObj.fs"
#load "./DynObj.fs"
@@ -9,11 +13,18 @@
open Fable.Pyxpecto
open DynamicObj
+type T(dyn:string, stat:string) as this=
+ inherit DynamicObj()
-let a = DynamicObj ()
-a.SetValue("aaa", 5)
-let b = DynamicObj ()
-b.SetValue("aaa", 5)
+ do
+ this.SetProperty("Dyn", dyn)
+ member this.Stat = stat
-a.GetProperties(true)
\ No newline at end of file
+let first = T("dyn1", "stat1")
+let second = T("dyn2", "stat2")
+
+let _ = second.ShallowCopyDynamicPropertiesTo(first)
+
+first |> DynObj.print
+second |> DynObj.print
\ No newline at end of file
diff --git a/tests/DynamicObject.Tests/DynamicObject.Tests.fsproj b/tests/DynamicObject.Tests/DynamicObject.Tests.fsproj
index 53a9307..6ccf42b 100644
--- a/tests/DynamicObject.Tests/DynamicObject.Tests.fsproj
+++ b/tests/DynamicObject.Tests/DynamicObject.Tests.fsproj
@@ -8,10 +8,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
diff --git a/tests/DynamicObject.Tests/DynamicObjs.fs b/tests/DynamicObject.Tests/DynamicObjs.fs
deleted file mode 100644
index 168f384..0000000
--- a/tests/DynamicObject.Tests/DynamicObjs.fs
+++ /dev/null
@@ -1,575 +0,0 @@
-module DynamicObj.Tests
-
-open System
-open Fable.Pyxpecto
-open DynamicObj
-
-let tests_TryGetPropertyValue = testList "TryGetPropertyValue" [
- testCase "NonExisting" <| fun _ ->
- let a = DynamicObj()
- let b = a.TryGetPropertyValue "a"
- Expect.isNone b "Value should not exist"
-
- testCase "Correct boxed Int" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("a", 1)
- let b = a.TryGetPropertyValue "a"
- Expect.equal (b) (Some (box 1)) "Value should be 1"
-
- testCase "Correct unboxed Int" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("a", 1)
- let b = a.TryGetPropertyValue "a"
- Expect.equal (b |> Option.map unbox) (Some 1) "Value should be 1"
-
- testCase "Correct boxed String" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("a", "1")
- let b = a.TryGetPropertyValue "a"
- Expect.equal (b) (Some (box "1")) "Value should be '1'"
-
- testCase "Correct unboxed String" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("a", "1")
- let b = a.TryGetPropertyValue "a"
- Expect.equal (b |> Option.map unbox) (Some "1") "Value should be '1'"
-
- testCase "Correct boxed List" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("a", [1; 2; 3])
- let b = a.TryGetPropertyValue "a"
- Expect.equal (b) (Some (box [1; 2; 3])) "Value should be [1; 2; 3]"
-
- testCase "Correct unboxed List" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("a", [1; 2; 3])
- let b = a.TryGetPropertyValue "a"
- Expect.equal (b |> Option.map unbox) (Some [1; 2; 3]) "Value should be [1; 2; 3]"
-
- testCase "Correct boxed DynamicObj" <| fun _ ->
- let a = DynamicObj()
- let b = DynamicObj()
- a.SetProperty("a", b)
- let c = a.TryGetPropertyValue "a"
- Expect.equal (c) (Some (box b)) "Value should be a DynamicObj"
-
- testCase "Correct unboxed DynamicObj" <| fun _ ->
- let a = DynamicObj()
- let b = DynamicObj()
- a.SetProperty("a", b)
- let c = a.TryGetPropertyValue "a"
- Expect.equal (c |> Option.map unbox) (Some b) "Value should be a DynamicObj"
-
-]
-
-let tests_GetPropertyValue = testList "GetPropertyValue" [
- testCase "NonExisting" <| fun _ ->
- let a = DynamicObj()
- Expect.throws (fun () -> a.GetPropertyValue("b") |> ignore) "Value should not exist"
-
- testCase "Correct boxed Int" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("a", 1)
- let b = a.GetPropertyValue "a"
- Expect.equal (b) (box 1) "Value should be 1"
-
- testCase "Correct unboxed Int" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("a", 1)
- let b = a.GetPropertyValue "a"
- Expect.equal (b |> unbox) (1) "Value should be 1"
-
- testCase "Correct boxed String" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("a", "1")
- let b = a.GetPropertyValue "a"
- Expect.equal (b) (box "1") "Value should be '1'"
-
- testCase "Correct unboxed String" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("a", "1")
- let b = a.GetPropertyValue "a"
- Expect.equal (b |> unbox) ("1") "Value should be '1'"
-
- testCase "Correct boxed List" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("a", [1; 2; 3])
- let b = a.GetPropertyValue "a"
- Expect.equal (b) (box [1; 2; 3]) "Value should be [1; 2; 3]"
-
- testCase "Correct unboxed List" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("a", [1; 2; 3])
- let b = a.GetPropertyValue "a"
- Expect.equal (b |> unbox) ([1; 2; 3]) "Value should be [1; 2; 3]"
-
- testCase "Correct boxed DynamicObj" <| fun _ ->
- let a = DynamicObj()
- let b = DynamicObj()
- a.SetProperty("a", b)
- let c = a.GetPropertyValue "a"
- Expect.equal (c) (box b) "Value should be a DynamicObj"
-
- testCase "Correct unboxed DynamicObj" <| fun _ ->
- let a = DynamicObj()
- let b = DynamicObj()
- a.SetProperty("a", b)
- let c = a.GetPropertyValue "a"
- Expect.equal (c |> unbox) (b) "Value should be a DynamicObj"
-
-]
-
-#if !FABLE_COMPILER
-// instance method TryGetTypedPropertyValue is not Fable-compatible
-let tests_TryGetTypedPropertyValue = testList "TryGetTypedPropertyValue" [
-
- testCase "typeof" <| fun _ ->
- let a = typeof
- Expect.equal a.Name "Int32" "Type should be Int32"
-
- testCase "NonExisting" <| fun _ ->
- let a = DynamicObj()
- let b = a.TryGetTypedPropertyValue "a"
- Expect.isNone b "Value should not exist"
-
- testCase "Correct Int" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("a", 1)
- let b = a.TryGetTypedPropertyValue "a"
- Expect.equal b (Some 1) "Value should be 1"
-
- testCase "Incorrect Int" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("a", "1")
- let b = a.TryGetTypedPropertyValue "a"
- Expect.isNone b "Value should not be an int"
-
- testCase "Correct String" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("a", "1")
- let b = a.TryGetTypedPropertyValue "a"
- Expect.equal b (Some "1") "Value should be '1'"
-
- testCase "Incorrect String" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("a", 1)
- let b = a.TryGetTypedPropertyValue "a"
- Expect.isNone b "Value should not be a string"
-
- testCase "Correct List" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("a", [1; 2; 3])
- let b = a.TryGetTypedPropertyValue "a"
- Expect.equal b (Some [1; 2; 3]) "Value should be [1; 2; 3]"
-
- testCase "Incorrect List" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("a", [1; 2; 3])
- let b = a.TryGetTypedPropertyValue "a"
- Expect.isNone b "Value should not be a string list"
-
- testCase "Correct DynamicObj" <| fun _ ->
- let a = DynamicObj()
- let b = DynamicObj()
- a.SetProperty("a", b)
- let c = a.TryGetTypedPropertyValue "a"
- Expect.equal c (Some b) "Value should be a DynamicObj"
-
- testCase "Incorrect DynamicObj" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("a", 1)
- let b = a.TryGetTypedPropertyValue "a"
- Expect.isNone b "Value should not be a DynamicObj"
-]
-#endif
-
-let tests_TryGetStaticPropertyHelper = testList "TryGetStaticPropertyHelper" [
- testCase "NonExisting" <| fun _ ->
- let a = DynamicObj()
- let b = a.TryGetStaticPropertyHelper("a")
- Expect.isNone b "Value should not exist"
-
- testCase "Properties dictionary is static property" <| fun _ ->
- let a = DynamicObj()
- let b = Expect.wantSome (a.TryGetStaticPropertyHelper("Properties")) "Value should exist"
- Expect.isTrue b.IsStatic "Properties should be static"
- Expect.isFalse b.IsDynamic "Properties should not be dynamic"
- Expect.isTrue b.IsMutable "Properties should be mutable"
- Expect.isFalse b.IsImmutable "Properties should not be immutable"
-
- testCase "dynamic property not retrieved as static" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("a", 1)
- Expect.isNone (a.TryGetStaticPropertyHelper("a")) "dynamic property should not be retrieved via TryGetStaticPropertyInfo"
-]
-
-let tests_TryGetDynamicPropertyHelper = testList "TryGetDynamicPropertyHelper" [
- testCase "NonExisting" <| fun _ ->
- let a = DynamicObj()
- let b = a.TryGetDynamicPropertyHelper("a")
- Expect.isNone b "Value should not exist"
-
- testCase "Existing dynamic property" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("a", 1)
- let b = Expect.wantSome (a.TryGetDynamicPropertyHelper("a")) "Value should exist"
- Expect.isFalse b.IsStatic "Properties should be static"
- Expect.isTrue b.IsDynamic "Properties should not be dynamic"
- Expect.isTrue b.IsMutable "Properties should be mutable"
- Expect.isFalse b.IsImmutable "Properties should not be immutable"
-
- testCase "static property not retrieved as dynamic" <| fun _ ->
- let a = DynamicObj()
- Expect.isNone (a.TryGetDynamicPropertyHelper("Properties")) "static property should not be retrieved via TryGetDynamicPropertyInfo"
-]
-
-let tests_TryGetPropertyHelper = testList "TryGetPropertyHelper" [
- testCase "NonExisting" <| fun _ ->
- let a = DynamicObj()
- let b = a.TryGetPropertyHelper("a")
- Expect.isNone b "Value should not exist"
-
- testCase "Existing dynamic property" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("a", 1)
- let b = Expect.wantSome (a.TryGetPropertyHelper("a")) "Value should exist"
- Expect.isFalse b.IsStatic "Properties should be static"
- Expect.isTrue b.IsDynamic "Properties should not be dynamic"
- Expect.isTrue b.IsMutable "Properties should be mutable"
- Expect.isFalse b.IsImmutable "Properties should not be immutable"
-
- testCase "Existing static property" <| fun _ ->
- let a = DynamicObj()
- let b = Expect.wantSome (a.TryGetPropertyHelper("Properties")) "Value should exist"
- Expect.isTrue b.IsStatic "Properties should be static"
- Expect.isFalse b.IsDynamic "Properties should not be dynamic"
- Expect.isTrue b.IsMutable "Properties should be mutable"
- Expect.isFalse b.IsImmutable "Properties should not be immutable"
-]
-
-let tests_SetProperty = testList "SetProperty" [
-
- //TODO: static property accession!
-
- testCase "Same String" <| fun _ ->
- let a = DynamicObj ()
- a.SetProperty("aaa", 5)
- let b = DynamicObj ()
- b.SetProperty("aaa", 5)
- Expect.equal a b "Values should be equal"
- Expect.equal (a.GetHashCode()) (b.GetHashCode()) "Hash codes should be equal"
-
- testCase "Different Strings" <| fun _ ->
- let a = DynamicObj ()
- a.SetProperty("aaa", 1212)
- let b = DynamicObj ()
- b.SetProperty("aaa", 5)
- Expect.notEqual a b "Values should not be equal"
-
- testCase "String only on one" <| fun _ ->
- let a = DynamicObj ()
- let b = DynamicObj ()
- b.SetProperty("aaa", 5)
-
- Expect.notEqual a b "Values should not be equal"
- Expect.notEqual b a "Values should not be equal (Reversed equality)"
-
- testCase "Same lists different keys" <| fun _ ->
- let a' = DynamicObj ()
- let b' = DynamicObj ()
- a'.SetProperty("quack!", [1; 2; 3])
- b'.SetProperty("quack!1", [1; 2; 3])
- Expect.notEqual (a'.GetHashCode()) (b'.GetHashCode()) "Hash codes should not be equal"
-
- testCase "Different lists" <| fun _ ->
- let a' = DynamicObj ()
- let b' = DynamicObj ()
- a'.SetProperty("quack!", [1; 2; 3])
- b'.SetProperty("quack!", [1; 2; 3; 4; 34])
- Expect.notEqual (a'.GetHashCode()) (b'.GetHashCode()) "Hash codes should not be equal"
-
- testCase "Nested Same List Same String" <| fun _ ->
- let a = DynamicObj ()
- let b = DynamicObj ()
-
- let a' = DynamicObj ()
- let b' = DynamicObj ()
- a'.SetProperty("quack!", [1; 2; 3])
- b'.SetProperty("quack!", [1; 2; 3])
-
- a.SetProperty("aaa", a')
- b.SetProperty("aaa", b')
- Expect.equal a' b' "New Values should be equal"
- Expect.equal a b "Old Values should be equal"
- Expect.equal (a.GetHashCode()) (b.GetHashCode()) "Old Hash codes should be equal"
- Expect.equal (a'.GetHashCode()) (b'.GetHashCode()) "New Hash codes should be equal"
-
- testCase "Nested Same List Different Strings" <| fun _ ->
- let a = DynamicObj ()
- let b = DynamicObj ()
-
- let a' = DynamicObj ()
- let b' = DynamicObj ()
- a'.SetProperty("quack!", [1; 2; 3])
- b'.SetProperty("quack!", [1; 2; 3])
-
- a.SetProperty("aaa", a')
- b.SetProperty("aaa1", b')
- Expect.equal a' b' "New Values should be equal"
- Expect.notEqual a b "Old Values should not be equal"
- Expect.equal (a'.GetHashCode()) (b'.GetHashCode()) "New Hash codes should be equal"
- ]
-
-let tests_RemoveProperty = testList "RemoveProperty" [
-
- //TODO: static property removal!
-
- testCase "Remove" <| fun _ ->
- let a = DynamicObj ()
- let b = DynamicObj ()
-
- a.SetProperty("quack!", "hello")
-
- a.RemoveProperty "quack!" |> ignore
-
- Expect.equal a b "Values should be equal"
- Expect.equal (a.GetHashCode()) (b.GetHashCode()) "Hash codes should be equal"
-
- testCase "Remove Non-Existing" <| fun _ ->
- let a = DynamicObj ()
- let b = DynamicObj ()
-
- a.SetProperty("quack!", "hello")
- b.SetProperty("quack!", "hello")
-
- a.RemoveProperty "quecky!" |> ignore
-
- Expect.equal a b "Values should be equal"
- Expect.equal (a.GetHashCode()) (b.GetHashCode()) "Hash codes should be equal"
-
- testCase "Remove only on one" <| fun _ ->
- let a = DynamicObj ()
- let b = DynamicObj ()
-
- a.SetProperty("quack!", "hello")
- b.SetProperty("quack!", "hello")
-
- a.RemoveProperty "quack!" |> ignore
-
- Expect.notEqual a b "Values should be unequal"
- Expect.notEqual (a.GetHashCode()) (b.GetHashCode()) "Hash codes should be unequal"
-
- testCase "Nested Remove Non-Existing" <| fun _ ->
- let a = DynamicObj ()
- let b = DynamicObj ()
-
- let a' = DynamicObj ()
- let b' = DynamicObj ()
- a'.SetProperty("quack!", [1; 2; 3])
- b'.SetProperty("quack!", [1; 2; 3])
-
- a.SetProperty("aaa", a')
- a.RemoveProperty "quack!" |> ignore
- b.SetProperty("aaa", b')
-
- Expect.equal a b "Values should be equal"
- Expect.equal (a.GetHashCode()) (b.GetHashCode()) "Hash codes should be equal"
-
- testCase "Nested Remove only on one" <| fun _ ->
- let a = DynamicObj ()
- let b = DynamicObj ()
-
- let a' = DynamicObj ()
- let b' = DynamicObj ()
- a'.SetProperty("quack!", [1; 2; 3])
- b'.SetProperty("quack!", [1; 2; 3])
-
- a.SetProperty("aaa", a')
- a'.RemoveProperty "quack!" |> ignore
- b.SetProperty("aaa", b')
-
- Expect.notEqual a b "Values should be unequal"
- Expect.notEqual (a.GetHashCode()) (b.GetHashCode()) "Hash codes should be unequal"
-
- testCase "Nested Remove on both" <| fun _ ->
- let a = DynamicObj ()
- let b = DynamicObj ()
-
- let a' = DynamicObj ()
- let b' = DynamicObj ()
- a'.SetProperty("quack!", [1; 2; 3])
- b'.SetProperty("quack!", [1; 2; 3])
-
- a.SetProperty("aaa", a')
- a.RemoveProperty "quack!" |> ignore
- b.SetProperty("aaa", b')
- b.RemoveProperty "quack!" |> ignore
-
- Expect.equal a b "Values should be equal"
- Expect.equal (a.GetHashCode()) (b.GetHashCode()) "Hash codes should be equal"
-
-]
-
-let tests_GetPropertyHelpers = testList "GetPropertyHelpers" [
- testCase "GetPropertyHelpers" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("a", 1)
- a.SetProperty("b", 2)
- let properties = a.GetPropertyHelpers(true)
- let names = properties |> Seq.map (fun p -> p.Name)
- Expect.equal (Seq.toList names) ["a"; "b"] "Should have all properties"
-]
-
-let tests_GetProperties = testList "GetProperties" [
- testCase "GetProperties" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("a", 1)
- a.SetProperty("b", 2)
- let properties = a.GetProperties(true) |> List.ofSeq
- let expected = [
- System.Collections.Generic.KeyValuePair("a", box 1)
- System.Collections.Generic.KeyValuePair("b", box 2)
- ]
- Expect.equal properties expected "Should have all properties"
-]
-
-let tests_CopyDynamicPropertiesTo = testList "CopyDynamicPropertiesTo" [
- testCase "ExistingObject" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("a", 1)
- a.SetProperty("b", 2)
- let b = DynamicObj()
- b.SetProperty("c", 3)
- a.CopyDynamicPropertiesTo(b)
- Expect.equal (b.GetPropertyValue("a")) 1 "Value a should be copied"
- Expect.equal (b.GetPropertyValue("b")) 2 "Value b should be copied"
- Expect.equal (b.GetPropertyValue("c")) 3 "Value c should be unaffected"
-
- testCase "NoOverwrite throws" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("a", 1)
- let b = DynamicObj()
- b.SetProperty("a", 3)
- let f = fun () -> a.CopyDynamicPropertiesTo(b)
- Expect.throws f "Should throw because property exists"
-
- testCase "Overwrite" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("a", 1)
- let b = DynamicObj()
- b.SetProperty("a", 3)
- Expect.notEqual a b "Values should not be equal before copying"
- a.CopyDynamicPropertiesTo(b, true)
- Expect.equal a b "Values should be equal"
-]
-
-let tests_CopyDynamicProperties = testList "CopyDynamicProperties" [
- testCase "NewObject" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("a", 1)
- a.SetProperty("b", 2)
- let b = a.CopyDynamicProperties()
- Expect.equal a b "Values should be equal"
-]
-
-let tests_Equals = testList "Equals" [
- testCase "Same Object" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("b", 2)
- Expect.isTrue (a.Equals(a)) "Values should be equal"
-
- testCase "Different Equal Objects" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("b", 2)
- let a2 = DynamicObj()
- a2.SetProperty("b", 2)
- Expect.isTrue (a.Equals(a2)) "Values should be equal"
-
- testCase "Different Unequal Objects" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("b", 2)
- let a2 = DynamicObj()
- a2.SetProperty("b", 3)
- Expect.isFalse (a.Equals(a2)) "Values should not be equal"
-
- testCase "nested DynamicObjs" <| fun _ ->
- let a = DynamicObj()
- let b = DynamicObj()
- b.SetProperty("c", 2)
- a.SetProperty("b", b)
- let a2 = DynamicObj()
- let b2 = DynamicObj()
- b2.SetProperty("c", 2)
- a2.SetProperty("b", b2)
- Expect.isTrue (a.Equals(a2)) "Values should be equal"
-
-]
-
-let tests_GetHashCode = testList "GetHashCode" [
- testCase "Same Object" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("b", 2)
- Expect.equal (a.GetHashCode()) (a.GetHashCode()) "Values should be equal"
-
- testCase "Different Equal Objects" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("b", 2)
- let a2 = DynamicObj()
- a2.SetProperty("b", 2)
- Expect.equal (a.GetHashCode()) (a2.GetHashCode()) "Values should be equal"
-
- testCase "Different Unequal Objects" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("b", 2)
- let a2 = DynamicObj()
- a.SetProperty("b", 3)
- Expect.notEqual (a.GetHashCode()) (a2.GetHashCode()) "Values should not be equal"
-
- testCase "nested DynamicObjs" <| fun _ ->
- let a = DynamicObj()
- let b = DynamicObj()
- b.SetProperty("c", 2)
- a.SetProperty("b", b)
- let a2 = DynamicObj()
- let b2 = DynamicObj()
- b2.SetProperty("c", 2)
- a2.SetProperty("b", b2)
- Expect.equal (a.GetHashCode()) (a2.GetHashCode()) "Values should be equal"
-
- testCase "null Value same key" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("b", null)
- let b = DynamicObj()
- b.SetProperty("b", null)
- Expect.equal (a.GetHashCode()) (b.GetHashCode()) "Values should be equal"
-
- testCase "null Value different key" <| fun _ ->
- let a = DynamicObj()
- a.SetProperty("b", null)
- let b = DynamicObj()
- a.SetProperty("c", null)
- Expect.notEqual (a.GetHashCode()) (b.GetHashCode()) "Values should not be equal"
-
-]
-
-let main = testList "DynamicObj (Class)" [
- tests_TryGetPropertyValue
- tests_GetPropertyValue
-
- #if !FABLE_COMPILER
- // instance method TryGetTypedValue is not Fable-compatible
- tests_TryGetTypedPropertyValue
- #endif
-
- tests_TryGetStaticPropertyHelper
- tests_TryGetDynamicPropertyHelper
- tests_TryGetPropertyHelper
- tests_SetProperty
- tests_RemoveProperty
- tests_GetPropertyHelpers
- tests_GetProperties
- tests_CopyDynamicPropertiesTo
- tests_CopyDynamicProperties
- tests_Equals
- tests_GetHashCode
-]
\ No newline at end of file
diff --git a/tests/DynamicObject.Tests/DynamicObjs/DeepCopyDynamicProperties.fs b/tests/DynamicObject.Tests/DynamicObjs/DeepCopyDynamicProperties.fs
new file mode 100644
index 0000000..a0abd9a
--- /dev/null
+++ b/tests/DynamicObject.Tests/DynamicObjs/DeepCopyDynamicProperties.fs
@@ -0,0 +1,329 @@
+module DynamicObj.Tests.DeepCopyDynamicProperties
+
+open Fable.Pyxpecto
+open DynamicObj
+open TestUtils
+
+let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [
+
+ 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 = constructDeepCopiedClone 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 = constructDeepCopiedClone ["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 ["dyn";"inner int"]) 1 "Original should have mutated properties"
+ Expect.equal (clone |> DynObj.getNestedPropAs ["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 = constructDeepCopiedClone ["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 ["first_level";"second_level";"lvl2"]) -1 "Original should have mutated properties"
+ Expect.equal (clone |> DynObj.getNestedPropAs ["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 = constructDeepCopiedClone ["arr", box arr]
+ item1.SetProperty("item", -1)
+ item2.SetProperty("item", -1)
+ item3.SetProperty("item", -1)
+ let originalProp = original |> DynObj.getNestedPropAs ["arr"] |> Array.map (fun dyn -> DynObj.getNestedPropAs ["item"] dyn)
+ let clonedProp = clone |> DynObj.getNestedPropAs ["arr"] |> Array.map (fun dyn -> DynObj.getNestedPropAs ["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.sequenceEqual clonedProp [|1; 2; 3|] "Clone should have original properties"
+
+ testCase "DynamicObj list" <| fun _ ->
+ let item1 = DynamicObj() |> DynObj.withProperty "item" 1
+ let item2 = DynamicObj() |> DynObj.withProperty "item" 2
+ let item3 = DynamicObj() |> DynObj.withProperty "item" 3
+ let l = [item1; item2; item3]
+ let original, clone = constructDeepCopiedClone ["list", box l]
+ item1.SetProperty("item", -1)
+ item2.SetProperty("item", -1)
+ item3.SetProperty("item", -1)
+ let originalProp = original |> DynObj.getNestedPropAs ["list"] |> List.map (fun dyn -> DynObj.getNestedPropAs ["item"] dyn)
+ let clonedProp = clone |> DynObj.getNestedPropAs ["list"] |> List.map (fun dyn -> DynObj.getNestedPropAs ["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.sequenceEqual clonedProp [1; 2; 3] "Clone should have original properties"
+
+ testCase "DynamicObj ResizeArray" <| fun _ ->
+ let item1 = DynamicObj() |> DynObj.withProperty "item" 1
+ let item2 = DynamicObj() |> DynObj.withProperty "item" 2
+ let item3 = DynamicObj() |> DynObj.withProperty "item" 3
+ let r = ResizeArray([item1; item2; item3])
+ let original, clone = constructDeepCopiedClone ["resizeArr", box r]
+ item1.SetProperty("item", -1)
+ item2.SetProperty("item", -1)
+ item3.SetProperty("item", -1)
+ let originalProp = original |> DynObj.getNestedPropAs> ["resizeArr"] |> Seq.map (fun dyn -> DynObj.getNestedPropAs ["item"] dyn) |> ResizeArray
+ let clonedProp = clone |> DynObj.getNestedPropAs> ["resizeArr"] |> Seq.map (fun dyn -> DynObj.getNestedPropAs ["item"] dyn) |> ResizeArray
+ Expect.notEqual original clone "Original and clone should not be equal after mutating DynamicObj prop on original"
+ Expect.sequenceEqual originalProp (ResizeArray[-1; -1; -1]) "Original should have mutated properties"
+ Expect.sequenceEqual clonedProp (ResizeArray[1; 2; 3]) "Clone should have original properties"
+ ]
+ testList "Un-Cloneable dynamic properties" [
+ testCase "Class with mutable fields is reference equal" <| fun _ ->
+ let item = MutableClass("initial")
+ let original, clone = constructDeepCopiedClone ["item", box item]
+ item.stat <- "mutated"
+ let originalProp = original |> DynObj.getNestedPropAs["item"]
+ let clonedProp = clone |> DynObj.getNestedPropAs ["item"]
+ Expect.equal original clone "Original and clone should be equal after mutating mutable field on original"
+ Expect.equal originalProp.stat "mutated" "Original property has mutated value"
+ Expect.equal clonedProp.stat "mutated" "Cloned property has mutated value"
+ Expect.referenceEqual originalProp clonedProp "Original and cloned property should be reference equal"
+ ]
+ ]
+ testList "Derived class implementing ICloneable" [
+ testList "SpecialCases" [
+ testCase "can unbox copy as DerivedClassCloneable" <| fun _ ->
+ Expect.pass (
+ let original = DerivedClassCloneable(stat = "stat", dyn = "dyn")
+ let clone = original.DeepCopyDynamicProperties() |> unbox
+ ()
+ )
+ testCase "copy is of type DerivedClassCloneable" <| fun _ ->
+ let original = DerivedClassCloneable(stat = "stat", dyn = "dyn")
+ let clone = original.DeepCopyDynamicProperties() |> unbox
+ Expect.equal (clone.GetType()) typeof "Clone is of type DerivedClassCloneable"
+ ptestCase "copy has NO instance prop as dynamic prop" <| fun _ ->
+ let original = DerivedClassCloneable(stat = "stat", dyn = "dyn")
+ let clone = original.DeepCopyDynamicProperties() |> unbox
+ let clonedProps = clone.GetProperties(false) |> Seq.map (fun p -> p.Key, p.Value)
+ Expect.sequenceEqual clonedProps ["dyn", "dyn"] "Clone should have no dynamic properties"
+ testCase "copy has static and dynamic props of original" <| fun _ ->
+ let original = DerivedClassCloneable(stat = "stat", dyn = "dyn")
+ let clone = original.DeepCopyDynamicProperties() |> unbox
+ Expect.equal clone original "Clone and original should be equal"
+ Expect.equal (clone.stat) (original.stat) "Clone should have static prop from derived class"
+ Expect.equal (clone |> DynObj.getNestedPropAs ["dyn"]) (original |> DynObj.getNestedPropAs ["dyn"]) "Clone should have dynamic prop from derived class"
+ testCase "can use instance method on copied derived class" <| fun _ ->
+ let original = DerivedClassCloneable(stat = "stat", dyn = "dyn")
+ let clone = original.DeepCopyDynamicProperties() |> unbox
+ Expect.pass (clone.PrintStat())
+ testCase "instance method on copied derived class returns correct value" <| fun _ ->
+ let original = DerivedClassCloneable(stat = "stat", dyn = "dyn")
+ let clone = original.DeepCopyDynamicProperties() |> unbox
+ Expect.equal (clone.FormatStat()) "stat: stat" "instance method should return correct value"
+ ]
+ ]
+ testList "Derived class" [
+ testList "SpecialCases" [
+
+ #if !FABLE_COMPILER
+ // this test is transpiled as Expect_throws(() => {} and can never fail, so let's just test it in F# for now
+ testCase "Cannot unbox clone as original type" <| fun _ ->
+ let original = DerivedClass(stat = "stat", dyn = "dyn")
+ let clone = original.DeepCopyDynamicProperties()
+ let unboxMaybe() = clone |> unbox |> ignore
+ Expect.throws unboxMaybe "Clone cannot be unboxed as DerivedClass"
+ #endif
+
+ testCase "copy has instance prop as dynamic prop" <| fun _ ->
+ let original = DerivedClass(stat = "stat", dyn = "dyn")
+ let clone = original.DeepCopyDynamicProperties() |> unbox
+ let clonedProps = clone.GetProperties(false) |> Seq.map (fun p -> p.Key, p.Value)
+ Expect.containsAll clonedProps ["stat","stat"] "Clone should have static prop from derived class as dynamic prop"
+ testCase "mutable instance prop is reference equal on clone" <| fun _ ->
+ let original = DerivedClass(stat = "stat", dyn = "dyn")
+ let mut = MutableClass("initial")
+ original.SetProperty("mutable", mut)
+ let clone = original.DeepCopyDynamicProperties() |> unbox
+ mut.stat <- "mutated"
+ let originalProp = original |> DynObj.getNestedPropAs["mutable"]
+ let clonedProp = clone |> DynObj.getNestedPropAs ["mutable"]
+ Expect.equal originalProp clonedProp "Original and clone should be equal after mutating mutable field on original"
+ ]
+ 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 = DerivedClass(stat = "stat", dyn = "dyn")
+ bulkMutate originalProps original
+
+ let clone = original.DeepCopyDynamicProperties() |> unbox
+ 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.sequenceEqual
+ (
+ original.GetProperties(false) |> Seq.map (fun p -> p.Key, p.Value)
+ |> Seq.sortBy fst
+ )
+ (
+ Seq.append
+ mutatedProps
+ [("dyn", "dyn")]
+ |> Seq.sortBy fst
+ )
+ "Original should have mutated properties"
+ Expect.sequenceEqual
+ (
+ clone.GetProperties(false) |> Seq.map (fun p -> p.Key, p.Value)
+ |> Seq.sortBy fst
+ )
+ (
+ Seq.append
+ originalProps
+ [("dyn", "dyn"); ("stat", "stat")] // copy should have static prop as dynamic prop
+ |> Seq.sortBy fst
+ )
+ "Clone should have original and static properties"
+ Expect.isTrue (original.GetType() = typeof) "Original is of type DerivedClass"
+ Expect.isTrue (clone.GetType() = typeof) "Clone is of type DynamicObj"
+
+ testCase "DynamicObj" <| fun _ ->
+ let inner = DynamicObj() |> DynObj.withProperty "inner int" 2
+ let original = DerivedClass(stat = "stat", dyn = "dyn")
+ original.SetProperty("inner", inner)
+ let clone = original.DeepCopyDynamicProperties() |> unbox
+ inner.SetProperty("inner int", 1)
+
+ Expect.equal (original |> DynObj.getNestedPropAs ["inner";"inner int"]) 1 "Original should have mutated properties"
+ Expect.equal (clone |> DynObj.getNestedPropAs ["inner";"inner int"]) 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 = DerivedClass(stat = "stat", dyn = "dyn")
+ original.SetProperty("arr", arr)
+ let clone = original.DeepCopyDynamicProperties() |> unbox
+ item1.SetProperty("item", -1)
+ item2.SetProperty("item", -1)
+ item3.SetProperty("item", -1)
+ let originalProp = original |> DynObj.getNestedPropAs ["arr"] |> Array.map (fun dyn -> DynObj.getNestedPropAs ["item"] dyn)
+ let clonedProp = clone |> DynObj.getNestedPropAs ["arr"] |> Array.map (fun dyn -> DynObj.getNestedPropAs ["item"] dyn)
+ Expect.sequenceEqual originalProp [|-1; -1; -1|] "Original should have mutated properties"
+ Expect.sequenceEqual clonedProp [|1; 2; 3|] "Clone should have original properties"
+
+ testCase "DynamicObj list" <| fun _ ->
+ let item1 = DynamicObj() |> DynObj.withProperty "item" 1
+ let item2 = DynamicObj() |> DynObj.withProperty "item" 2
+ let item3 = DynamicObj() |> DynObj.withProperty "item" 3
+ let l = [item1; item2; item3]
+ let original = DerivedClass(stat = "stat", dyn = "dyn")
+ original.SetProperty("list", l)
+ let clone = original.DeepCopyDynamicProperties() |> unbox
+ item1.SetProperty("item", -1)
+ item2.SetProperty("item", -1)
+ item3.SetProperty("item", -1)
+ let originalProp = original |> DynObj.getNestedPropAs ["list"] |> List.map (fun dyn -> DynObj.getNestedPropAs ["item"] dyn)
+ let clonedProp = clone |> DynObj.getNestedPropAs ["list"] |> List.map (fun dyn -> DynObj.getNestedPropAs ["item"] dyn)
+ Expect.sequenceEqual originalProp [-1; -1; -1] "Original should have mutated properties"
+ Expect.sequenceEqual clonedProp [1; 2; 3] "Clone should have original properties"
+
+ testCase "DynamicObj ResizeArray" <| fun _ ->
+ let item1 = DynamicObj() |> DynObj.withProperty "item" 1
+ let item2 = DynamicObj() |> DynObj.withProperty "item" 2
+ let item3 = DynamicObj() |> DynObj.withProperty "item" 3
+ let r = ResizeArray([item1; item2; item3])
+ let original = DerivedClass(stat = "stat", dyn = "dyn")
+ original.SetProperty("resizeArr", r)
+ let clone = original.DeepCopyDynamicProperties() |> unbox
+ item1.SetProperty("item", -1)
+ item2.SetProperty("item", -1)
+ item3.SetProperty("item", -1)
+ let originalProp = original |> DynObj.getNestedPropAs> ["resizeArr"] |> Seq.map (fun dyn -> DynObj.getNestedPropAs ["item"] dyn) |> ResizeArray
+ let clonedProp = clone |> DynObj.getNestedPropAs> ["resizeArr"] |> Seq.map (fun dyn -> DynObj.getNestedPropAs ["item"] dyn) |> ResizeArray
+ Expect.sequenceEqual originalProp (ResizeArray[-1; -1; -1]) "Original should have mutated properties"
+ Expect.sequenceEqual clonedProp (ResizeArray[1; 2; 3]) "Clone should have original properties"
+ ]
+ testList "Un-Cloneable dynamic properties" [
+ testCase "Class with mutable fields is reference equal" <| fun _ ->
+ let item = MutableClass("initial")
+ let original = DerivedClass(stat = "stat", dyn = "dyn")
+ original.SetProperty("item", item)
+ let clone = original.DeepCopyDynamicProperties() |> unbox
+ item.stat <- "mutated"
+ let originalProp = original |> DynObj.getNestedPropAs["item"]
+ let clonedProp = clone |> DynObj.getNestedPropAs ["item"]
+ Expect.equal originalProp.stat "mutated" "Original property has mutated value"
+ Expect.equal clonedProp.stat "mutated" "Cloned property has mutated value"
+ Expect.referenceEqual originalProp clonedProp "Original and cloned property should be reference equal"
+ ]
+ ]
+]
\ No newline at end of file
diff --git a/tests/DynamicObject.Tests/DynamicObjs/Equals.fs b/tests/DynamicObject.Tests/DynamicObjs/Equals.fs
new file mode 100644
index 0000000..182a94a
--- /dev/null
+++ b/tests/DynamicObject.Tests/DynamicObjs/Equals.fs
@@ -0,0 +1,38 @@
+module DynamicObj.Tests.Equals
+
+open Fable.Pyxpecto
+open DynamicObj
+open TestUtils
+
+let tests_Equals = testList "Equals" [
+ testCase "Same Object" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("b", 2)
+ Expect.isTrue (a.Equals(a)) "Values should be equal"
+
+ testCase "Different Equal Objects" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("b", 2)
+ let a2 = DynamicObj()
+ a2.SetProperty("b", 2)
+ Expect.isTrue (a.Equals(a2)) "Values should be equal"
+
+ testCase "Different Unequal Objects" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("b", 2)
+ let a2 = DynamicObj()
+ a2.SetProperty("b", 3)
+ Expect.isFalse (a.Equals(a2)) "Values should not be equal"
+
+ testCase "nested DynamicObjs" <| fun _ ->
+ let a = DynamicObj()
+ let b = DynamicObj()
+ b.SetProperty("c", 2)
+ a.SetProperty("b", b)
+ let a2 = DynamicObj()
+ let b2 = DynamicObj()
+ b2.SetProperty("c", 2)
+ a2.SetProperty("b", b2)
+ Expect.isTrue (a.Equals(a2)) "Values should be equal"
+
+]
\ No newline at end of file
diff --git a/tests/DynamicObject.Tests/DynamicObjs/GetHashcode.fs b/tests/DynamicObject.Tests/DynamicObjs/GetHashcode.fs
new file mode 100644
index 0000000..5ded1a2
--- /dev/null
+++ b/tests/DynamicObject.Tests/DynamicObjs/GetHashcode.fs
@@ -0,0 +1,53 @@
+module DynamicObj.Tests.GetHashCode
+
+open Fable.Pyxpecto
+open DynamicObj
+open TestUtils
+
+
+let tests_GetHashCode = testList "GetHashCode" [
+ testCase "Same Object" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("b", 2)
+ Expect.equal (a.GetHashCode()) (a.GetHashCode()) "Values should be equal"
+
+ testCase "Different Equal Objects" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("b", 2)
+ let a2 = DynamicObj()
+ a2.SetProperty("b", 2)
+ Expect.equal (a.GetHashCode()) (a2.GetHashCode()) "Values should be equal"
+
+ testCase "Different Unequal Objects" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("b", 2)
+ let a2 = DynamicObj()
+ a.SetProperty("b", 3)
+ Expect.notEqual (a.GetHashCode()) (a2.GetHashCode()) "Values should not be equal"
+
+ testCase "nested DynamicObjs" <| fun _ ->
+ let a = DynamicObj()
+ let b = DynamicObj()
+ b.SetProperty("c", 2)
+ a.SetProperty("b", b)
+ let a2 = DynamicObj()
+ let b2 = DynamicObj()
+ b2.SetProperty("c", 2)
+ a2.SetProperty("b", b2)
+ Expect.equal (a.GetHashCode()) (a2.GetHashCode()) "Values should be equal"
+
+ testCase "null Value same key" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("b", null)
+ let b = DynamicObj()
+ b.SetProperty("b", null)
+ Expect.equal (a.GetHashCode()) (b.GetHashCode()) "Values should be equal"
+
+ testCase "null Value different key" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("b", null)
+ let b = DynamicObj()
+ a.SetProperty("c", null)
+ Expect.notEqual (a.GetHashCode()) (b.GetHashCode()) "Values should not be equal"
+
+]
\ No newline at end of file
diff --git a/tests/DynamicObject.Tests/DynamicObjs/GetProperties.fs b/tests/DynamicObject.Tests/DynamicObjs/GetProperties.fs
new file mode 100644
index 0000000..1cf3845
--- /dev/null
+++ b/tests/DynamicObject.Tests/DynamicObjs/GetProperties.fs
@@ -0,0 +1,28 @@
+module DynamicObj.Tests.GetProperties
+
+open Fable.Pyxpecto
+open DynamicObj
+open TestUtils
+
+let tests_GetProperties = testList "GetProperties" [
+ testCase "GetProperties" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("a", 1)
+ a.SetProperty("b", 2)
+ let properties = a.GetProperties(true) |> List.ofSeq
+ let expected = [
+ System.Collections.Generic.KeyValuePair("a", box 1)
+ System.Collections.Generic.KeyValuePair("b", box 2)
+ ]
+ Expect.sequenceEqual properties expected "Should have all properties"
+ testCase "returns static instance members of derived class when wanted" <| fun _ ->
+ let a = DerivedClass(stat = "stat", dyn = "dyn")
+ let properties = a.GetProperties(true) |> List.ofSeq |> List.sortBy (fun kv -> kv.Key)
+ let expected =
+ [
+ System.Collections.Generic.KeyValuePair("dyn", box "dyn")
+ System.Collections.Generic.KeyValuePair("stat", box "stat")
+ ]
+ |> Seq.sortBy (fun kv -> kv.Key)
+ Expect.sequenceEqual properties expected "Should have all properties"
+]
\ No newline at end of file
diff --git a/tests/DynamicObject.Tests/DynamicObjs/GetPropertyHelpers.fs b/tests/DynamicObject.Tests/DynamicObjs/GetPropertyHelpers.fs
new file mode 100644
index 0000000..4dca26d
--- /dev/null
+++ b/tests/DynamicObject.Tests/DynamicObjs/GetPropertyHelpers.fs
@@ -0,0 +1,15 @@
+module DynamicObj.Tests.GetPropertyHelpers
+
+open Fable.Pyxpecto
+open DynamicObj
+open TestUtils
+
+let tests_GetPropertyHelpers = testList "GetPropertyHelpers" [
+ testCase "GetPropertyHelpers" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("a", 1)
+ a.SetProperty("b", 2)
+ let properties = a.GetPropertyHelpers(true)
+ let names = properties |> Seq.map (fun p -> p.Name)
+ Expect.equal (Seq.toList names) ["a"; "b"] "Should have all properties"
+]
diff --git a/tests/DynamicObject.Tests/DynamicObjs/GetPropertyValue.fs b/tests/DynamicObject.Tests/DynamicObjs/GetPropertyValue.fs
new file mode 100644
index 0000000..abcf60a
--- /dev/null
+++ b/tests/DynamicObject.Tests/DynamicObjs/GetPropertyValue.fs
@@ -0,0 +1,62 @@
+module DynamicObj.Tests.GetPropertyValue
+
+open Fable.Pyxpecto
+open DynamicObj
+open TestUtils
+
+let tests_GetPropertyValue = testList "GetPropertyValue" [
+ testCase "NonExisting" <| fun _ ->
+ let a = DynamicObj()
+ Expect.throws (fun () -> a.GetPropertyValue("b") |> ignore) "Value should not exist"
+
+ testCase "Correct boxed Int" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("a", 1)
+ let b = a.GetPropertyValue "a"
+ Expect.equal (b) (box 1) "Value should be 1"
+
+ testCase "Correct unboxed Int" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("a", 1)
+ let b = a.GetPropertyValue "a"
+ Expect.equal (b |> unbox) (1) "Value should be 1"
+
+ testCase "Correct boxed String" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("a", "1")
+ let b = a.GetPropertyValue "a"
+ Expect.equal (b) (box "1") "Value should be '1'"
+
+ testCase "Correct unboxed String" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("a", "1")
+ let b = a.GetPropertyValue "a"
+ Expect.equal (b |> unbox) ("1") "Value should be '1'"
+
+ testCase "Correct boxed List" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("a", [1; 2; 3])
+ let b = a.GetPropertyValue "a"
+ Expect.equal (b) (box [1; 2; 3]) "Value should be [1; 2; 3]"
+
+ testCase "Correct unboxed List" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("a", [1; 2; 3])
+ let b = a.GetPropertyValue "a"
+ Expect.equal (b |> unbox) ([1; 2; 3]) "Value should be [1; 2; 3]"
+
+ testCase "Correct boxed DynamicObj" <| fun _ ->
+ let a = DynamicObj()
+ let b = DynamicObj()
+ a.SetProperty("a", b)
+ let c = a.GetPropertyValue "a"
+ Expect.equal (c) (box b) "Value should be a DynamicObj"
+
+ testCase "Correct unboxed DynamicObj" <| fun _ ->
+ let a = DynamicObj()
+ let b = DynamicObj()
+ a.SetProperty("a", b)
+ let c = a.GetPropertyValue "a"
+ Expect.equal (c |> unbox) (b) "Value should be a DynamicObj"
+
+]
\ No newline at end of file
diff --git a/tests/DynamicObject.Tests/DynamicObjs/Main.fs b/tests/DynamicObject.Tests/DynamicObjs/Main.fs
new file mode 100644
index 0000000..4b71c08
--- /dev/null
+++ b/tests/DynamicObject.Tests/DynamicObjs/Main.fs
@@ -0,0 +1,31 @@
+module DynamicObjs.Tests
+
+open Fable.Pyxpecto
+open DynamicObj.Tests
+
+let main = testList "DynamicObj (Class)" [
+
+ GetHashCode.tests_GetHashCode
+ Equals.tests_Equals
+
+ SetProperty.tests_SetProperty
+ RemoveProperty.tests_RemoveProperty
+
+ TryGetPropertyValue.tests_TryGetPropertyValue
+ GetPropertyValue.tests_GetPropertyValue
+
+ #if !FABLE_COMPILER
+ // instance method TryGetTypedValue is not Fable-compatible
+ TryGetTypedPropertyValue.tests_TryGetTypedPropertyValue
+ #endif
+
+ TryGetStaticPropertyHelper.tests_TryGetStaticPropertyHelper
+ TryGetDynamicPropertyHelper.tests_TryGetDynamicPropertyHelper
+ TryGetPropertyHelper.tests_TryGetPropertyHelper
+ GetPropertyHelpers.tests_GetPropertyHelpers
+ GetProperties.tests_GetProperties
+
+ ShallowCopyDynamicPropertiesTo.tests_ShallowCopyDynamicPropertiesTo
+ ShallowCopyDynamicProperties.tests_ShallowCopyDynamicProperties
+ DeepCopyDynamicProperties.tests_DeepCopyDynamicProperties
+]
\ No newline at end of file
diff --git a/tests/DynamicObject.Tests/DynamicObjs/RemoveProperty.fs b/tests/DynamicObject.Tests/DynamicObjs/RemoveProperty.fs
new file mode 100644
index 0000000..5cfa82b
--- /dev/null
+++ b/tests/DynamicObject.Tests/DynamicObjs/RemoveProperty.fs
@@ -0,0 +1,95 @@
+module DynamicObj.Tests.RemoveProperty
+
+open Fable.Pyxpecto
+open DynamicObj
+open TestUtils
+
+let tests_RemoveProperty = testList "RemoveProperty" [
+
+ //TODO: static property removal!
+
+ testCase "Remove" <| fun _ ->
+ let a = DynamicObj ()
+ let b = DynamicObj ()
+
+ a.SetProperty("quack!", "hello")
+
+ a.RemoveProperty "quack!" |> ignore
+
+ Expect.equal a b "Values should be equal"
+ Expect.equal (a.GetHashCode()) (b.GetHashCode()) "Hash codes should be equal"
+
+ testCase "Remove Non-Existing" <| fun _ ->
+ let a = DynamicObj ()
+ let b = DynamicObj ()
+
+ a.SetProperty("quack!", "hello")
+ b.SetProperty("quack!", "hello")
+
+ a.RemoveProperty "quecky!" |> ignore
+
+ Expect.equal a b "Values should be equal"
+ Expect.equal (a.GetHashCode()) (b.GetHashCode()) "Hash codes should be equal"
+
+ testCase "Remove only on one" <| fun _ ->
+ let a = DynamicObj ()
+ let b = DynamicObj ()
+
+ a.SetProperty("quack!", "hello")
+ b.SetProperty("quack!", "hello")
+
+ a.RemoveProperty "quack!" |> ignore
+
+ Expect.notEqual a b "Values should be unequal"
+ Expect.notEqual (a.GetHashCode()) (b.GetHashCode()) "Hash codes should be unequal"
+
+ testCase "Nested Remove Non-Existing" <| fun _ ->
+ let a = DynamicObj ()
+ let b = DynamicObj ()
+
+ let a' = DynamicObj ()
+ let b' = DynamicObj ()
+ a'.SetProperty("quack!", [1; 2; 3])
+ b'.SetProperty("quack!", [1; 2; 3])
+
+ a.SetProperty("aaa", a')
+ a.RemoveProperty "quack!" |> ignore
+ b.SetProperty("aaa", b')
+
+ Expect.equal a b "Values should be equal"
+ Expect.equal (a.GetHashCode()) (b.GetHashCode()) "Hash codes should be equal"
+
+ testCase "Nested Remove only on one" <| fun _ ->
+ let a = DynamicObj ()
+ let b = DynamicObj ()
+
+ let a' = DynamicObj ()
+ let b' = DynamicObj ()
+ a'.SetProperty("quack!", [1; 2; 3])
+ b'.SetProperty("quack!", [1; 2; 3])
+
+ a.SetProperty("aaa", a')
+ a'.RemoveProperty "quack!" |> ignore
+ b.SetProperty("aaa", b')
+
+ Expect.notEqual a b "Values should be unequal"
+ Expect.notEqual (a.GetHashCode()) (b.GetHashCode()) "Hash codes should be unequal"
+
+ testCase "Nested Remove on both" <| fun _ ->
+ let a = DynamicObj ()
+ let b = DynamicObj ()
+
+ let a' = DynamicObj ()
+ let b' = DynamicObj ()
+ a'.SetProperty("quack!", [1; 2; 3])
+ b'.SetProperty("quack!", [1; 2; 3])
+
+ a.SetProperty("aaa", a')
+ a.RemoveProperty "quack!" |> ignore
+ b.SetProperty("aaa", b')
+ b.RemoveProperty "quack!" |> ignore
+
+ Expect.equal a b "Values should be equal"
+ Expect.equal (a.GetHashCode()) (b.GetHashCode()) "Hash codes should be equal"
+
+]
diff --git a/tests/DynamicObject.Tests/DynamicObjs/SetProperty.fs b/tests/DynamicObject.Tests/DynamicObjs/SetProperty.fs
new file mode 100644
index 0000000..fb41979
--- /dev/null
+++ b/tests/DynamicObject.Tests/DynamicObjs/SetProperty.fs
@@ -0,0 +1,78 @@
+module DynamicObj.Tests.SetProperty
+
+open Fable.Pyxpecto
+open DynamicObj
+open TestUtils
+
+let tests_SetProperty = testList "SetProperty" [
+
+ //TODO: static property accession!
+
+ testCase "Same String" <| fun _ ->
+ let a = DynamicObj ()
+ a.SetProperty("aaa", 5)
+ let b = DynamicObj ()
+ b.SetProperty("aaa", 5)
+ Expect.equal a b "Values should be equal"
+ Expect.equal (a.GetHashCode()) (b.GetHashCode()) "Hash codes should be equal"
+
+ testCase "Different Strings" <| fun _ ->
+ let a = DynamicObj ()
+ a.SetProperty("aaa", 1212)
+ let b = DynamicObj ()
+ b.SetProperty("aaa", 5)
+ Expect.notEqual a b "Values should not be equal"
+
+ testCase "String only on one" <| fun _ ->
+ let a = DynamicObj ()
+ let b = DynamicObj ()
+ b.SetProperty("aaa", 5)
+
+ Expect.notEqual a b "Values should not be equal"
+ Expect.notEqual b a "Values should not be equal (Reversed equality)"
+
+ testCase "Same lists different keys" <| fun _ ->
+ let a' = DynamicObj ()
+ let b' = DynamicObj ()
+ a'.SetProperty("quack!", [1; 2; 3])
+ b'.SetProperty("quack!1", [1; 2; 3])
+ Expect.notEqual (a'.GetHashCode()) (b'.GetHashCode()) "Hash codes should not be equal"
+
+ testCase "Different lists" <| fun _ ->
+ let a' = DynamicObj ()
+ let b' = DynamicObj ()
+ a'.SetProperty("quack!", [1; 2; 3])
+ b'.SetProperty("quack!", [1; 2; 3; 4; 34])
+ Expect.notEqual (a'.GetHashCode()) (b'.GetHashCode()) "Hash codes should not be equal"
+
+ testCase "Nested Same List Same String" <| fun _ ->
+ let a = DynamicObj ()
+ let b = DynamicObj ()
+
+ let a' = DynamicObj ()
+ let b' = DynamicObj ()
+ a'.SetProperty("quack!", [1; 2; 3])
+ b'.SetProperty("quack!", [1; 2; 3])
+
+ a.SetProperty("aaa", a')
+ b.SetProperty("aaa", b')
+ Expect.equal a' b' "New Values should be equal"
+ Expect.equal a b "Old Values should be equal"
+ Expect.equal (a.GetHashCode()) (b.GetHashCode()) "Old Hash codes should be equal"
+ Expect.equal (a'.GetHashCode()) (b'.GetHashCode()) "New Hash codes should be equal"
+
+ testCase "Nested Same List Different Strings" <| fun _ ->
+ let a = DynamicObj ()
+ let b = DynamicObj ()
+
+ let a' = DynamicObj ()
+ let b' = DynamicObj ()
+ a'.SetProperty("quack!", [1; 2; 3])
+ b'.SetProperty("quack!", [1; 2; 3])
+
+ a.SetProperty("aaa", a')
+ b.SetProperty("aaa1", b')
+ Expect.equal a' b' "New Values should be equal"
+ Expect.notEqual a b "Old Values should not be equal"
+ Expect.equal (a'.GetHashCode()) (b'.GetHashCode()) "New Hash codes should be equal"
+ ]
diff --git a/tests/DynamicObject.Tests/DynamicObjs/ShallowCopyDynamicProperties.fs b/tests/DynamicObject.Tests/DynamicObjs/ShallowCopyDynamicProperties.fs
new file mode 100644
index 0000000..59464f3
--- /dev/null
+++ b/tests/DynamicObject.Tests/DynamicObjs/ShallowCopyDynamicProperties.fs
@@ -0,0 +1,24 @@
+module DynamicObj.Tests.ShallowCopyDynamicProperties
+
+open Fable.Pyxpecto
+open DynamicObj
+open TestUtils
+
+let tests_ShallowCopyDynamicProperties = testList "ShallowCopyDynamicProperties" [
+ testCase "NewObject" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("a", 1)
+ a.SetProperty("b", 2)
+ let b = a.ShallowCopyDynamicProperties()
+ Expect.equal a b "Values should be equal"
+
+ testCase "copies are only references" <| fun _ ->
+ let a = DynamicObj()
+ let inner = DynamicObj()
+ inner.SetProperty("inner", 1)
+ a.SetProperty("nested", inner)
+ let b = a.ShallowCopyDynamicProperties()
+ Expect.equal a b "Value should be copied"
+ inner.SetProperty("another", 2)
+ Expect.equal a b "copied value was not mutated via reference"
+]
\ No newline at end of file
diff --git a/tests/DynamicObject.Tests/DynamicObjs/ShallowCopyDynamicPropertiesTo.fs b/tests/DynamicObject.Tests/DynamicObjs/ShallowCopyDynamicPropertiesTo.fs
new file mode 100644
index 0000000..5df8dc7
--- /dev/null
+++ b/tests/DynamicObject.Tests/DynamicObjs/ShallowCopyDynamicPropertiesTo.fs
@@ -0,0 +1,38 @@
+module DynamicObj.Tests.ShallowCopyDynamicPropertiesTo
+
+open Fable.Pyxpecto
+open DynamicObj
+open TestUtils
+
+let tests_ShallowCopyDynamicPropertiesTo = testList "ShallowCopyDynamicPropertiesTo" [
+ testCase "ExistingObject" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("a", 1)
+ a.SetProperty("b", 2)
+ let b = DynamicObj()
+ b.SetProperty("c", 3)
+ a.ShallowCopyDynamicPropertiesTo(b)
+ Expect.equal (b.GetPropertyValue("a")) 1 "Value a should be copied"
+ Expect.equal (b.GetPropertyValue("b")) 2 "Value b should be copied"
+ Expect.equal (b.GetPropertyValue("c")) 3 "Value c should be unaffected"
+
+ testCase "Overwrite" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("a", 1)
+ let b = DynamicObj()
+ b.SetProperty("a", 3)
+ Expect.notEqual a b "Values should not be equal before copying"
+ a.ShallowCopyDynamicPropertiesTo(b, true)
+ Expect.equal a b "Values should be equal"
+
+ testCase "copies are only references" <| fun _ ->
+ let a = DynamicObj()
+ let inner = DynamicObj()
+ inner.SetProperty("inner", 1)
+ a.SetProperty("nested", inner)
+ let b = DynamicObj()
+ a.ShallowCopyDynamicPropertiesTo(b)
+ Expect.equal a b "Value should be copied"
+ inner.SetProperty("another", 2)
+ Expect.equal a b "copied value was not mutated via reference"
+]
diff --git a/tests/DynamicObject.Tests/DynamicObjs/TryGetDynamicPropertyHelper.fs b/tests/DynamicObject.Tests/DynamicObjs/TryGetDynamicPropertyHelper.fs
new file mode 100644
index 0000000..77b8a2b
--- /dev/null
+++ b/tests/DynamicObject.Tests/DynamicObjs/TryGetDynamicPropertyHelper.fs
@@ -0,0 +1,25 @@
+module DynamicObj.Tests.TryGetDynamicPropertyHelper
+
+open Fable.Pyxpecto
+open DynamicObj
+open TestUtils
+
+let tests_TryGetDynamicPropertyHelper = testList "TryGetDynamicPropertyHelper" [
+ testCase "NonExisting" <| fun _ ->
+ let a = DynamicObj()
+ let b = a.TryGetDynamicPropertyHelper("a")
+ Expect.isNone b "Value should not exist"
+
+ testCase "Existing dynamic property" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("a", 1)
+ let b = Expect.wantSome (a.TryGetDynamicPropertyHelper("a")) "Value should exist"
+ Expect.isFalse b.IsStatic "Properties should be static"
+ Expect.isTrue b.IsDynamic "Properties should not be dynamic"
+ Expect.isTrue b.IsMutable "Properties should be mutable"
+ Expect.isFalse b.IsImmutable "Properties should not be immutable"
+
+ testCase "static property not retrieved as dynamic" <| fun _ ->
+ let a = DynamicObj()
+ Expect.isNone (a.TryGetDynamicPropertyHelper("Properties")) "static property should not be retrieved via TryGetDynamicPropertyInfo"
+]
\ No newline at end of file
diff --git a/tests/DynamicObject.Tests/DynamicObjs/TryGetPropertyHelper.fs b/tests/DynamicObject.Tests/DynamicObjs/TryGetPropertyHelper.fs
new file mode 100644
index 0000000..89091c0
--- /dev/null
+++ b/tests/DynamicObject.Tests/DynamicObjs/TryGetPropertyHelper.fs
@@ -0,0 +1,29 @@
+module DynamicObj.Tests.TryGetPropertyHelper
+
+open Fable.Pyxpecto
+open DynamicObj
+open TestUtils
+
+let tests_TryGetPropertyHelper = testList "TryGetPropertyHelper" [
+ testCase "NonExisting" <| fun _ ->
+ let a = DynamicObj()
+ let b = a.TryGetPropertyHelper("a")
+ Expect.isNone b "Value should not exist"
+
+ testCase "Existing dynamic property" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("a", 1)
+ let b = Expect.wantSome (a.TryGetPropertyHelper("a")) "Value should exist"
+ Expect.isFalse b.IsStatic "Properties should be static"
+ Expect.isTrue b.IsDynamic "Properties should not be dynamic"
+ Expect.isTrue b.IsMutable "Properties should be mutable"
+ Expect.isFalse b.IsImmutable "Properties should not be immutable"
+
+ testCase "Existing static property" <| fun _ ->
+ let a = DynamicObj()
+ let b = Expect.wantSome (a.TryGetPropertyHelper("Properties")) "Value should exist"
+ Expect.isTrue b.IsStatic "Properties should be static"
+ Expect.isFalse b.IsDynamic "Properties should not be dynamic"
+ Expect.isTrue b.IsMutable "Properties should be mutable"
+ Expect.isFalse b.IsImmutable "Properties should not be immutable"
+]
\ No newline at end of file
diff --git a/tests/DynamicObject.Tests/DynamicObjs/TryGetPropertyValue.fs b/tests/DynamicObject.Tests/DynamicObjs/TryGetPropertyValue.fs
new file mode 100644
index 0000000..7c86a66
--- /dev/null
+++ b/tests/DynamicObject.Tests/DynamicObjs/TryGetPropertyValue.fs
@@ -0,0 +1,63 @@
+module DynamicObj.Tests.TryGetPropertyValue
+
+open Fable.Pyxpecto
+open DynamicObj
+open TestUtils
+
+let tests_TryGetPropertyValue = testList "TryGetPropertyValue" [
+ testCase "NonExisting" <| fun _ ->
+ let a = DynamicObj()
+ let b = a.TryGetPropertyValue "a"
+ Expect.isNone b "Value should not exist"
+
+ testCase "Correct boxed Int" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("a", 1)
+ let b = a.TryGetPropertyValue "a"
+ Expect.equal (b) (Some (box 1)) "Value should be 1"
+
+ testCase "Correct unboxed Int" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("a", 1)
+ let b = a.TryGetPropertyValue "a"
+ Expect.equal (b |> Option.map unbox) (Some 1) "Value should be 1"
+
+ testCase "Correct boxed String" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("a", "1")
+ let b = a.TryGetPropertyValue "a"
+ Expect.equal (b) (Some (box "1")) "Value should be '1'"
+
+ testCase "Correct unboxed String" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("a", "1")
+ let b = a.TryGetPropertyValue "a"
+ Expect.equal (b |> Option.map unbox) (Some "1") "Value should be '1'"
+
+ testCase "Correct boxed List" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("a", [1; 2; 3])
+ let b = a.TryGetPropertyValue "a"
+ Expect.equal (b) (Some (box [1; 2; 3])) "Value should be [1; 2; 3]"
+
+ testCase "Correct unboxed List" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("a", [1; 2; 3])
+ let b = a.TryGetPropertyValue "a"
+ Expect.equal (b |> Option.map unbox) (Some [1; 2; 3]) "Value should be [1; 2; 3]"
+
+ testCase "Correct boxed DynamicObj" <| fun _ ->
+ let a = DynamicObj()
+ let b = DynamicObj()
+ a.SetProperty("a", b)
+ let c = a.TryGetPropertyValue "a"
+ Expect.equal (c) (Some (box b)) "Value should be a DynamicObj"
+
+ testCase "Correct unboxed DynamicObj" <| fun _ ->
+ let a = DynamicObj()
+ let b = DynamicObj()
+ a.SetProperty("a", b)
+ let c = a.TryGetPropertyValue "a"
+ Expect.equal (c |> Option.map unbox) (Some b) "Value should be a DynamicObj"
+
+]
\ No newline at end of file
diff --git a/tests/DynamicObject.Tests/DynamicObjs/TryGetStaticPropertyHelper.fs b/tests/DynamicObject.Tests/DynamicObjs/TryGetStaticPropertyHelper.fs
new file mode 100644
index 0000000..2fcfc11
--- /dev/null
+++ b/tests/DynamicObject.Tests/DynamicObjs/TryGetStaticPropertyHelper.fs
@@ -0,0 +1,25 @@
+module DynamicObj.Tests.TryGetStaticPropertyHelper
+
+open Fable.Pyxpecto
+open DynamicObj
+open TestUtils
+
+let tests_TryGetStaticPropertyHelper = testList "TryGetStaticPropertyHelper" [
+ testCase "NonExisting" <| fun _ ->
+ let a = DynamicObj()
+ let b = a.TryGetStaticPropertyHelper("a")
+ Expect.isNone b "Value should not exist"
+
+ testCase "Properties dictionary is static property" <| fun _ ->
+ let a = DynamicObj()
+ let b = Expect.wantSome (a.TryGetStaticPropertyHelper("Properties")) "Value should exist"
+ Expect.isTrue b.IsStatic "Properties should be static"
+ Expect.isFalse b.IsDynamic "Properties should not be dynamic"
+ Expect.isTrue b.IsMutable "Properties should be mutable"
+ Expect.isFalse b.IsImmutable "Properties should not be immutable"
+
+ testCase "dynamic property not retrieved as static" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("a", 1)
+ Expect.isNone (a.TryGetStaticPropertyHelper("a")) "dynamic property should not be retrieved via TryGetStaticPropertyInfo"
+]
\ No newline at end of file
diff --git a/tests/DynamicObject.Tests/DynamicObjs/TryGetTypedPropertyValue.fs b/tests/DynamicObject.Tests/DynamicObjs/TryGetTypedPropertyValue.fs
new file mode 100644
index 0000000..38d835f
--- /dev/null
+++ b/tests/DynamicObject.Tests/DynamicObjs/TryGetTypedPropertyValue.fs
@@ -0,0 +1,69 @@
+module DynamicObj.Tests.TryGetTypedPropertyValue
+
+open Fable.Pyxpecto
+open DynamicObj
+open TestUtils
+
+#if !FABLE_COMPILER
+// instance method TryGetTypedPropertyValue is not Fable-compatible
+let tests_TryGetTypedPropertyValue = testList "TryGetTypedPropertyValue" [
+
+ testCase "typeof" <| fun _ ->
+ let a = typeof
+ Expect.equal a.Name "Int32" "Type should be Int32"
+
+ testCase "NonExisting" <| fun _ ->
+ let a = DynamicObj()
+ let b = a.TryGetTypedPropertyValue "a"
+ Expect.isNone b "Value should not exist"
+
+ testCase "Correct Int" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("a", 1)
+ let b = a.TryGetTypedPropertyValue "a"
+ Expect.equal b (Some 1) "Value should be 1"
+
+ testCase "Incorrect Int" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("a", "1")
+ let b = a.TryGetTypedPropertyValue "a"
+ Expect.isNone b "Value should not be an int"
+
+ testCase "Correct String" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("a", "1")
+ let b = a.TryGetTypedPropertyValue "a"
+ Expect.equal b (Some "1") "Value should be '1'"
+
+ testCase "Incorrect String" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("a", 1)
+ let b = a.TryGetTypedPropertyValue "a"
+ Expect.isNone b "Value should not be a string"
+
+ testCase "Correct List" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("a", [1; 2; 3])
+ let b = a.TryGetTypedPropertyValue "a"
+ Expect.equal b (Some [1; 2; 3]) "Value should be [1; 2; 3]"
+
+ testCase "Incorrect List" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("a", [1; 2; 3])
+ let b = a.TryGetTypedPropertyValue "a"
+ Expect.isNone b "Value should not be a string list"
+
+ testCase "Correct DynamicObj" <| fun _ ->
+ let a = DynamicObj()
+ let b = DynamicObj()
+ a.SetProperty("a", b)
+ let c = a.TryGetTypedPropertyValue "a"
+ Expect.equal c (Some b) "Value should be a DynamicObj"
+
+ testCase "Incorrect DynamicObj" <| fun _ ->
+ let a = DynamicObj()
+ a.SetProperty("a", 1)
+ let b = a.TryGetTypedPropertyValue "a"
+ Expect.isNone b "Value should not be a DynamicObj"
+]
+#endif
\ No newline at end of file
diff --git a/tests/DynamicObject.Tests/Main.fs b/tests/DynamicObject.Tests/Main.fs
index 8b79a73..a067e8c 100644
--- a/tests/DynamicObject.Tests/Main.fs
+++ b/tests/DynamicObject.Tests/Main.fs
@@ -4,7 +4,7 @@ open Fable.Pyxpecto
let all = testSequenced <| testList "DynamicObj" [
ReflectionUtils.Tests.main
- DynamicObj.Tests.main
+ DynamicObjs.Tests.main
DynObj.Tests.main
Inheritance.Tests.main
Interface.Tests.main
diff --git a/tests/DynamicObject.Tests/TestUtils.fs b/tests/DynamicObject.Tests/TestUtils.fs
new file mode 100644
index 0000000..4d86ece
--- /dev/null
+++ b/tests/DynamicObject.Tests/TestUtils.fs
@@ -0,0 +1,76 @@
+module TestUtils
+
+open System
+open DynamicObj
+open Fable.Core
+
+[]
+type MutableClass(stat:string) =
+ let mutable s = stat
+ member this.stat with get() = s and set v = s <- v
+
+[]
+type DerivedClass(stat: string, dyn: string) as this =
+ inherit DynamicObj()
+ do
+ this.SetProperty("dyn", dyn)
+ member this.stat = stat
+
+[]
+type DerivedClassCloneable(stat: string, dyn: string) as this =
+ inherit DynamicObj()
+ do
+ this.SetProperty("dyn", dyn)
+ member this.stat = stat
+ member this.FormatStat() = $"stat: {this.stat}"
+ member this.PrintStat() = this.FormatStat() |> printfn "%s"
+ interface ICloneable with
+ member this.Clone() =
+ let dyn = this.GetPropertyValue("dyn") |> unbox
+ DerivedClassCloneable(stat, dyn)
+
+let constructDeepCopiedClone<'T> (props: seq) =
+ let original = DynamicObj()
+ props
+ |> Seq.iter (fun (propertyName, propertyValue) -> original.SetProperty(propertyName, propertyValue))
+ let clone = original.DeepCopyDynamicProperties()
+ original, clone |> unbox<'T>
+
+let bulkMutate (props: seq) (dyn: #DynamicObj) =
+ props |> Seq.iter (fun (propertyName, propertyValue) -> dyn.SetProperty(propertyName, propertyValue))
+
+
+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) (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) 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
+
+ let referenceEqual actual expected message =
+ if not (LanguagePrimitives.PhysicalEquality actual expected) then
+ failwith message
\ No newline at end of file