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 5c13094..43bd318 100644 --- a/src/DynamicObj/DynamicObj.fs +++ b/src/DynamicObj/DynamicObj.fs @@ -245,36 +245,19 @@ type DynamicObj() = ) /// - /// 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: + /// Returns a new DynamicObj with only the dynamic properties of the original DynamicObj (sans instance properties). /// - /// 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. + /// 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. /// - /// 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 + member this.ShallowCopyDynamicProperties() = + let target = DynamicObj() + 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 @@ -298,9 +281,14 @@ type DynamicObj() = box [for dyn in dyns -> tryDeepCopyObj dyn :?> DynamicObj] | :? ResizeArray as dyns -> box (ResizeArray([for dyn in dyns -> tryDeepCopyObj dyn :?> DynamicObj])) - - #if !FABLE_COMPILER_PYTHON + #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 @@ -312,24 +300,47 @@ type DynamicObj() = 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 (tryDeepCopyObj kv.Value) + | Some pi when overWrite -> pi.SetValue target (DynamicObj.tryDeepCopyObj kv.Value) | Some _ -> () - | None -> target.SetProperty(kv.Key, tryDeepCopyObj kv.Value) + | None -> target.SetProperty(kv.Key, DynamicObj.tryDeepCopyObj 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.ShallowCopyDynamicProperties() = - let target = DynamicObj() - this.ShallowCopyDynamicPropertiesTo(target) - target /// /// Attempts to perform a deep copy of the DynamicObj. @@ -360,10 +371,7 @@ type DynamicObj() = /// /// The target object to copy dynamic members to /// Whether existing properties on the target object will be overwritten - member this.DeepCopyDynamicProperties() = - let target = DynamicObj() - this.DeepCopyDynamicPropertiesTo(target) - target + 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/tests/DynamicObject.Tests/DynamicObjs/DeepCopyDynamicProperties.fs b/tests/DynamicObject.Tests/DynamicObjs/DeepCopyDynamicProperties.fs index 3c50956..a0abd9a 100644 --- a/tests/DynamicObject.Tests/DynamicObjs/DeepCopyDynamicProperties.fs +++ b/tests/DynamicObject.Tests/DynamicObjs/DeepCopyDynamicProperties.fs @@ -26,7 +26,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ "single", box (single 1.0f) "decimal", box (decimal 1M) ] - let original, clone = constructDeepCopiedClone originalProps + let original, clone = constructDeepCopiedClone originalProps let mutatedProps = [ "int", box 2 "float", box 2.0 @@ -51,7 +51,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ testCase "DynamicObj" <| fun _ -> let inner = DynamicObj() |> DynObj.withProperty "inner int" 2 - let original, clone = constructDeepCopiedClone ["dyn", inner] + 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" @@ -61,7 +61,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ 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] + 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" @@ -72,7 +72,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ 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] + let original, clone = constructDeepCopiedClone ["arr", box arr] item1.SetProperty("item", -1) item2.SetProperty("item", -1) item3.SetProperty("item", -1) @@ -87,7 +87,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ 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] + let original, clone = constructDeepCopiedClone ["list", box l] item1.SetProperty("item", -1) item2.SetProperty("item", -1) item3.SetProperty("item", -1) @@ -102,7 +102,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ 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] + let original, clone = constructDeepCopiedClone ["resizeArr", box r] item1.SetProperty("item", -1) item2.SetProperty("item", -1) item3.SetProperty("item", -1) @@ -115,7 +115,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ 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] + let original, clone = constructDeepCopiedClone ["item", box item] item.stat <- "mutated" let originalProp = original |> DynObj.getNestedPropAs["item"] let clonedProp = clone |> DynObj.getNestedPropAs ["item"] @@ -126,39 +126,60 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ ] ] testList "Derived class implementing ICloneable" [ - 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 _ -> - () - ] - testList "static properties" [ - testCase "Class with mutable fields is reference equal" <| fun _ -> - () + 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() + 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() + let clone = original.DeepCopyDynamicProperties() |> unbox mut.stat <- "mutated" let originalProp = original |> DynObj.getNestedPropAs["mutable"] let clonedProp = clone |> DynObj.getNestedPropAs ["mutable"] @@ -186,7 +207,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ let original = DerivedClass(stat = "stat", dyn = "dyn") bulkMutate originalProps original - let clone = original.DeepCopyDynamicProperties() + let clone = original.DeepCopyDynamicProperties() |> unbox let mutatedProps = [ "int", box 2 "float", box 2.0 @@ -237,7 +258,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ let inner = DynamicObj() |> DynObj.withProperty "inner int" 2 let original = DerivedClass(stat = "stat", dyn = "dyn") original.SetProperty("inner", inner) - let clone = original.DeepCopyDynamicProperties() + let clone = original.DeepCopyDynamicProperties() |> unbox inner.SetProperty("inner int", 1) Expect.equal (original |> DynObj.getNestedPropAs ["inner";"inner int"]) 1 "Original should have mutated properties" @@ -250,7 +271,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ let arr = [|item1; item2; item3|] let original = DerivedClass(stat = "stat", dyn = "dyn") original.SetProperty("arr", arr) - let clone = original.DeepCopyDynamicProperties() + let clone = original.DeepCopyDynamicProperties() |> unbox item1.SetProperty("item", -1) item2.SetProperty("item", -1) item3.SetProperty("item", -1) @@ -264,9 +285,9 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ 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") + let original = DerivedClass(stat = "stat", dyn = "dyn") original.SetProperty("list", l) - let clone = original.DeepCopyDynamicProperties() + let clone = original.DeepCopyDynamicProperties() |> unbox item1.SetProperty("item", -1) item2.SetProperty("item", -1) item3.SetProperty("item", -1) @@ -282,7 +303,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ let r = ResizeArray([item1; item2; item3]) let original = DerivedClass(stat = "stat", dyn = "dyn") original.SetProperty("resizeArr", r) - let clone = original.DeepCopyDynamicProperties() + let clone = original.DeepCopyDynamicProperties() |> unbox item1.SetProperty("item", -1) item2.SetProperty("item", -1) item3.SetProperty("item", -1) @@ -296,7 +317,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [ let item = MutableClass("initial") let original = DerivedClass(stat = "stat", dyn = "dyn") original.SetProperty("item", item) - let clone = original.DeepCopyDynamicProperties() + let clone = original.DeepCopyDynamicProperties() |> unbox item.stat <- "mutated" let originalProp = original |> DynObj.getNestedPropAs["item"] let clonedProp = clone |> DynObj.getNestedPropAs ["item"] diff --git a/tests/DynamicObject.Tests/TestUtils.fs b/tests/DynamicObject.Tests/TestUtils.fs index 0c08606..4d86ece 100644 --- a/tests/DynamicObject.Tests/TestUtils.fs +++ b/tests/DynamicObject.Tests/TestUtils.fs @@ -22,17 +22,19 @@ type DerivedClassCloneable(stat: string, dyn: string) as this = 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 (props: seq) = +let constructDeepCopiedClone<'T> (props: seq) = let original = DynamicObj() props |> Seq.iter (fun (propertyName, propertyValue) -> original.SetProperty(propertyName, propertyValue)) let clone = original.DeepCopyDynamicProperties() - original, clone + original, clone |> unbox<'T> let bulkMutate (props: seq) (dyn: #DynamicObj) = props |> Seq.iter (fun (propertyName, propertyValue) -> dyn.SetProperty(propertyName, propertyValue))