diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index aedc80c..f2c8940 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -7,6 +7,12 @@ "commands": [ "fsdocs" ] + }, + "fable": { + "version": "4.19.3", + "commands": [ + "fable" + ] } } } \ No newline at end of file diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 2a04338..9fdbb19 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -6,34 +6,64 @@ on: pull_request: branches: [ main ] - jobs: - build-and-test-linux: + test: + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 + + # SETUP .NET - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v3 with: dotnet-version: 6.x.x - - name: make script executable - run: chmod u+x build.sh - - name: Build and test - working-directory: ./ - run: ./build.sh runtests + - name: Restore fable + run: dotnet tool restore - build-and-test-windows: - - runs-on: windows-latest + # SETUP NODE + - name: Setup Node.js environment + uses: actions/setup-node@v3 + with: + node-version: 16 + - name: install node modules + working-directory: ./ + run: npm install --ignore-scripts - steps: - - uses: actions/checkout@v2 - - name: Setup .NET - uses: actions/setup-dotnet@v1 + # SETUP PYTHON + - name: Setup Python + uses: actions/setup-python@v5 with: - dotnet-version: 6.x.x - - name: Build and test + python-version: '3.12' + - name: Setup Virtual Environment + run: python -m venv .venv + - name: Setup Poetry Windows + if: matrix.os == 'windows-latest' + run: | + .\.venv\Scripts\python.exe -m pip install -U pip setuptools + .\.venv\Scripts\python.exe -m pip install poetry + .\.venv\Scripts\python.exe -m poetry install --no-root + - name: Setup Poetry Unix + if: matrix.os == 'ubuntu-latest' + run: | + ./.venv/bin/python -m pip install -U pip setuptools + ./.venv/bin/python -m pip install poetry + ./.venv/bin/python -m poetry install --no-root + + # BUILD + - name: make script executable + if: matrix.os == 'ubuntu-latest' + run: chmod u+x build.sh + - name: Test (Unix) + if: matrix.os == 'ubuntu-latest' working-directory: ./ - run: ./build.cmd runtests \ No newline at end of file + run: ./build.sh runtests + - name: Test (Windows) + if: matrix.os == 'windows-latest' + run: .\build.cmd runtests \ No newline at end of file diff --git a/.gitignore b/.gitignore index 05696af..75f8e3a 100644 --- a/.gitignore +++ b/.gitignore @@ -194,4 +194,7 @@ docsrc/tools/FSharp.Formatting.svclog /tests/FSharp.Stats.Tests/coverage.xml .ionide -pkg \ No newline at end of file +pkg +/tests/**/js +/tests/**/py +/.venv \ No newline at end of file diff --git a/DynamicObj.sln b/DynamicObj.sln index a64b218..6b45628 100644 --- a/DynamicObj.sln +++ b/DynamicObj.sln @@ -5,8 +5,6 @@ VisualStudioVersion = 17.0.31521.260 MinimumVisualStudioVersion = 10.0.40219.1 Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "DynamicObj", "src\DynamicObj\DynamicObj.fsproj", "{B8BF1554-AAC3-434E-9502-FC83B43F3704}" EndProject -Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "UnitTests", "tests\UnitTests\UnitTests.fsproj", "{D009964D-9408-4344-B610-B73F54FE2A86}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{39AA72A1-A628-481B-A2B5-94E2BD163061}" ProjectSection(SolutionItems) = preProject build.cmd = build.cmd @@ -26,17 +24,19 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".proj", ".proj", "{C3CF2F15 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 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{42AA66FC-8928-4029-BF41-52C1B49DEEDF}" ProjectSection(SolutionItems) = preProject + docs\content\fsdocs-custom.css = docs\content\fsdocs-custom.css + docs\index.fsx = docs\index.fsx docs\_template.fsx = docs\_template.fsx docs\_template.html = docs\_template.html docs\_template.ipynb = docs\_template.ipynb - docs\content\fsdocs-custom.css = docs\content\fsdocs-custom.css - docs\index.fsx = docs\index.fsx EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSharpTests", "tests\CSharpTests\CSharpTests.csproj", "{D62D0901-DB69-4C64-AC63-FBBBDCF6BC7D}" @@ -45,6 +45,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{988D804A EndProject Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "build", "build\build.fsproj", "{C73AB951-91F2-4668-B2E0-B58298E5F664}" EndProject +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "DynamicObj.Immutable", "src\DynamicObj.Immutable\DynamicObj.Immutable.fsproj", "{5E7DAC28-7752-4209-B3BB-6DCE54C28AD8}" +EndProject +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "DynamicObject.Tests", "tests\DynamicObject.Tests\DynamicObject.Tests.fsproj", "{39192F2D-164B-4905-A7D7-5C5B0FFCD2BB}" +EndProject +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "DynamicObject.Immutable.Tests", "tests\DynamicObject.Immutable.Tests\DynamicObject.Immutable.Tests.fsproj", "{0F6A539F-82D2-4BDC-8BF0-F2D261873B92}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -55,10 +61,6 @@ Global {B8BF1554-AAC3-434E-9502-FC83B43F3704}.Debug|Any CPU.Build.0 = Debug|Any CPU {B8BF1554-AAC3-434E-9502-FC83B43F3704}.Release|Any CPU.ActiveCfg = Release|Any CPU {B8BF1554-AAC3-434E-9502-FC83B43F3704}.Release|Any CPU.Build.0 = Release|Any CPU - {D009964D-9408-4344-B610-B73F54FE2A86}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D009964D-9408-4344-B610-B73F54FE2A86}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D009964D-9408-4344-B610-B73F54FE2A86}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D009964D-9408-4344-B610-B73F54FE2A86}.Release|Any CPU.Build.0 = Release|Any CPU {D62D0901-DB69-4C64-AC63-FBBBDCF6BC7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D62D0901-DB69-4C64-AC63-FBBBDCF6BC7D}.Debug|Any CPU.Build.0 = Debug|Any CPU {D62D0901-DB69-4C64-AC63-FBBBDCF6BC7D}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -67,14 +69,27 @@ Global {C73AB951-91F2-4668-B2E0-B58298E5F664}.Debug|Any CPU.Build.0 = Debug|Any CPU {C73AB951-91F2-4668-B2E0-B58298E5F664}.Release|Any CPU.ActiveCfg = Release|Any CPU {C73AB951-91F2-4668-B2E0-B58298E5F664}.Release|Any CPU.Build.0 = Release|Any CPU + {5E7DAC28-7752-4209-B3BB-6DCE54C28AD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5E7DAC28-7752-4209-B3BB-6DCE54C28AD8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5E7DAC28-7752-4209-B3BB-6DCE54C28AD8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5E7DAC28-7752-4209-B3BB-6DCE54C28AD8}.Release|Any CPU.Build.0 = Release|Any CPU + {39192F2D-164B-4905-A7D7-5C5B0FFCD2BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {39192F2D-164B-4905-A7D7-5C5B0FFCD2BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {39192F2D-164B-4905-A7D7-5C5B0FFCD2BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {39192F2D-164B-4905-A7D7-5C5B0FFCD2BB}.Release|Any CPU.Build.0 = Release|Any CPU + {0F6A539F-82D2-4BDC-8BF0-F2D261873B92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0F6A539F-82D2-4BDC-8BF0-F2D261873B92}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0F6A539F-82D2-4BDC-8BF0-F2D261873B92}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0F6A539F-82D2-4BDC-8BF0-F2D261873B92}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {D009964D-9408-4344-B610-B73F54FE2A86} = {988D804A-3A42-4E46-B233-B64F5C22524B} {D62D0901-DB69-4C64-AC63-FBBBDCF6BC7D} = {988D804A-3A42-4E46-B233-B64F5C22524B} {C73AB951-91F2-4668-B2E0-B58298E5F664} = {39AA72A1-A628-481B-A2B5-94E2BD163061} + {39192F2D-164B-4905-A7D7-5C5B0FFCD2BB} = {988D804A-3A42-4E46-B233-B64F5C22524B} + {0F6A539F-82D2-4BDC-8BF0-F2D261873B92} = {988D804A-3A42-4E46-B233-B64F5C22524B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6F5C3597-4524-4A4E-94EC-44857BD0BCEC} diff --git a/README.md b/README.md index 03d2797..90c70f1 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,112 @@ # DynamicObj F# library supporting Dynamic Objects including inheritance in functional style. -It builds on ´System.Dynamic´ but adds object inheritance. + +The library is compatible with [Fable](https://github.com/fable-compiler/Fable), allowing transpilation to `javascript` and `python`. + + + +## Usage example + +### Get started + +```fsharp +#r "nuget: DynamicObj" +#r "nuget: Fable.Core" // Needed if working with Fable + +open DynamicObj +open Fable.Core // Needed if working with Fable + +[] // AttachMembers needed if working with Fable +type Person(id : int, name : string) = + + // Include this in your class + inherit DynamicObj() + + let mutable name = name + + // Mutable property + member this.Name + with get() = name + and set(value) = name <- value + + // Immutable property + member this.ID + with get() = id + +let p = Person(1337,"John") +``` + +### Accessing static and dynamic properties + +```fsharp + +// Access Static Properties +p.GetValue("Name") // val it: obj = "John" +p.GetValue("ID") // val it: obj = 1337 + + +// Overwrite mutable static property +p.SetValue("Name","Jane") // val it: unit = () +// Overwrite immutable static property +p.SetValue("ID",1234) // System.Exception: Cannot set value for static, immutable property "ID" +// Set dynamic property +p.SetValue("Address","FunStreet") // val it: unit = () + + +// Access Properties +p.GetValue("Name") // val it: obj = "Jane" +p.Name // val it: string = "Jane" +p.GetValue("ID") // val it: obj = 1337 +p.ID // val it: int = 1337 +p.GetValue("Address") // val it: obj = "FunStreet" +``` + +### Practical helpers + +```fsharp +DynObj.format p +|> printfn "%s" +``` +-> +``` +Name: Jane +ID: 1337 +?Address: FunStreet +``` + +## Development + +#### Requirements + +- [nodejs and npm](https://nodejs.org/en/download) + - verify with `node --version` (Tested with v18.16.1) + - verify with `npm --version` (Tested with v9.2.0) +- [.NET SDK](https://dotnet.microsoft.com/en-us/download) + - verify with `dotnet --version` (Tested with 7.0.306) +- [Python](https://www.python.org/downloads/) + - verify with `py --version` (Tested with 3.12.2, known to work only for >=3.11) + +#### Local Setup + +On windows you can use the `setup.cmd` to run the following steps automatically! + +1. Setup dotnet tools + + `dotnet tool restore` + + +2. Install NPM dependencies + + `npm install` + +3. Setup python environment + + `py -m venv .venv` + +4. Install [Poetry](https://python-poetry.org/) and dependencies + + 1. `.\.venv\Scripts\python.exe -m pip install -U pip setuptools` + 2. `.\.venv\Scripts\python.exe -m pip install poetry` + 3. `.\.venv\Scripts\python.exe -m poetry install --no-root` + +Verify correct setup with `./build.cmd runtests` ✨ \ No newline at end of file diff --git a/build/BasicTasks.fs b/build/BasicTasks.fs index a15af69..7a439bc 100644 --- a/build/BasicTasks.fs +++ b/build/BasicTasks.fs @@ -4,13 +4,146 @@ open BlackFox.Fake open Fake.IO open Fake.DotNet open Fake.IO.Globbing.Operators +open Helpers + open ProjectInfo +[] +module Helper = + + open Fake + open Fake.Core + + let createProcess exe arg dir = + CreateProcess.fromRawCommandLine exe arg + |> CreateProcess.withWorkingDirectory dir + |> CreateProcess.ensureExitCode + + module Proc = + + module Parallel = + + open System + + let locker = obj() + + let colors = [| + ConsoleColor.DarkYellow + ConsoleColor.DarkCyan + ConsoleColor.Magenta + ConsoleColor.Blue + ConsoleColor.Cyan + ConsoleColor.DarkMagenta + ConsoleColor.DarkBlue + ConsoleColor.Yellow + |] + + let print color (colored: string) (line: string) = + lock locker + (fun () -> + let currentColor = Console.ForegroundColor + Console.ForegroundColor <- color + Console.Write colored + Console.ForegroundColor <- currentColor + Console.WriteLine line) + + let onStdout index name (line: string) = + let color = colors.[index % colors.Length] + if isNull line then + print color $"{name}: --- END ---" "" + else if String.isNotNullOrEmpty line then + print color $"{name}: " line + + let onStderr name (line: string) = + let color = ConsoleColor.Red + if isNull line |> not then + print color $"{name}: " line + + let redirect (index, (name, createProcess)) = + createProcess + |> CreateProcess.redirectOutputIfNotRedirected + |> CreateProcess.withOutputEvents (onStdout index name) (onStderr name) + + let printStarting indexed = + for (index, (name, c: CreateProcess<_>)) in indexed do + let color = colors.[index % colors.Length] + let wd = + c.WorkingDirectory + |> Option.defaultValue "" + let exe = c.Command.Executable + let args = c.Command.Arguments.ToStartInfo + print color $"{name}: {wd}> {exe} {args}" "" + + let run cs = + cs + |> Seq.toArray + |> Array.indexed + |> fun x -> printStarting x; x + |> Array.map redirect + |> Array.Parallel.map Proc.run + + let dotnet = createProcess "dotnet" + + let node = + let nodePath = + match ProcessUtils.tryFindFileOnPath "node" with + | Some path -> path + | None -> + "node was not found in path. Please install it and make sure it's available from your path. " + + "See https://safe-stack.github.io/docs/quickstart/#install-pre-requisites for more info" + |> failwith + + createProcess nodePath + + let npx = + let npmPath = + match ProcessUtils.tryFindFileOnPath "npx" with + | Some path -> path + | None -> + "npm was not found in path. Please install it and make sure it's available from your path. " + + "See https://safe-stack.github.io/docs/quickstart/#install-pre-requisites for more info" + |> failwith + + createProcess npmPath + + let npm = + let npmPath = + match ProcessUtils.tryFindFileOnPath "npm" with + | Some path -> path + | None -> + "npm was not found in path. Please install it and make sure it's available from your path. " + + "See https://safe-stack.github.io/docs/quickstart/#install-pre-requisites for more info" + |> failwith + + createProcess npmPath + + let python = + if System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows) then + Fake.Core.Trace.log "Detected Windows System." + createProcess (__SOURCE_DIRECTORY__.Replace(@"\build",@"\.venv\Scripts\python.exe")) + else + Fake.Core.Trace.log "Detected Unix System." + createProcess (__SOURCE_DIRECTORY__.Replace(@"/build",@"/.venv/bin/python")) + + let run proc arg dir = + proc arg dir + |> Proc.run + |> ignore + + let runParallel processes = + processes + |> Proc.Parallel.run + |> ignore + + + let setPrereleaseTag = BuildTask.create "SetPrereleaseTag" [] { - printfn "Please enter pre-release package suffix" - let suffix = System.Console.ReadLine() - prereleaseSuffix <- suffix - prereleaseTag <- (sprintf "%s-%s" release.NugetVersion suffix) + printfn "Please enter pre-release package suffix option: (a/b/rc)" + let suffixTag = System.Console.ReadLine() |> PreReleaseFlag.fromInput + printfn "Plrease enter pre-release package version number" + let suffixNumber = System.Console.ReadLine() |> int + prereleaseSuffix <- suffixTag + prereleaseSuffixNumber <- suffixNumber isPrerelease <- true } @@ -19,24 +152,12 @@ let clean = BuildTask.create "Clean" [] { ++ "src/**/obj" ++ "tests/**/bin" ++ "tests/**/obj" - ++ "pkg" + ++ "dist" + ++ ProjectInfo.netPkgDir |> Shell.cleanDirs } let build = BuildTask.create "Build" [clean] { - !! "src/**/*.*proj" - -- "src/bin/*" - |> Seq.iter (DotNet.build (fun p -> - let msBuildParams = - {p.MSBuildParams with - Properties = ([ - "AssemblyVersion", assemblyVersion - "AssemblyInformationalVersion", stableVersionTag - ] @ p.MSBuildParams.Properties) - } - { - p with - MSBuildParams = msBuildParams - } -)) -} + solutionFile + |> DotNet.build id +} \ No newline at end of file diff --git a/build/DocumentationTasks.fs b/build/DocumentationTasks.fs index ddc31bf..fdc152e 100644 --- a/build/DocumentationTasks.fs +++ b/build/DocumentationTasks.fs @@ -14,6 +14,7 @@ let buildDocs = BuildTask.create "BuildDocs" [build] { } let buildDocsPrerelease = BuildTask.create "BuildDocsPrerelease" [setPrereleaseTag; build] { + let prereleaseTag = PreReleaseFlag.toNugetTag release.SemVer prereleaseSuffix prereleaseSuffixNumber printfn "building docs with prerelease version %s" prereleaseTag runDotNet (sprintf "fsdocs build --eval --clean --properties Configuration=Release --parameters fsdocs-package-version %s" prereleaseTag) @@ -28,6 +29,7 @@ let watchDocs = BuildTask.create "WatchDocs" [build] { } let watchDocsPrerelease = BuildTask.create "WatchDocsPrerelease" [setPrereleaseTag; build] { + let prereleaseTag = PreReleaseFlag.toNugetTag release.SemVer prereleaseSuffix prereleaseSuffixNumber printfn "watching docs with prerelease version %s" prereleaseTag runDotNet (sprintf "fsdocs watch --eval --clean --properties Configuration=Release --parameters fsdocs-package-version %s" prereleaseTag) diff --git a/build/Helpers.fs b/build/Helpers.fs index 96d3290..b45e010 100644 --- a/build/Helpers.fs +++ b/build/Helpers.fs @@ -25,4 +25,37 @@ let runOrDefault defaultTarget args = 0 with e -> printfn "%A" e - 1 \ No newline at end of file + 1 + + +type PreReleaseFlag = + | Alpha + | Beta + | ReleaseCandidate + + static member fromInput (input: string) = + match input with + | "a" -> Alpha + | "b" -> Beta + | "rc" -> ReleaseCandidate + | _ -> failwith "Invalid input" + + static member toNugetTag (semVer : SemVerInfo) (flag: PreReleaseFlag) (number : int) = + let suffix = + match flag with + | Alpha -> $"alpha.{number}" + | Beta -> $"beta.{number}" + | ReleaseCandidate -> $"rc.{number}" + sprintf "%i.%i.%i-%s" semVer.Major semVer.Minor semVer.Patch suffix + + + static member toNPMTag (semVer : SemVerInfo) (flag: PreReleaseFlag) (number : int) = + PreReleaseFlag.toNugetTag semVer flag number + + static member toPyPITag (semVer : SemVerInfo) (tag: PreReleaseFlag) (number : int) = + let suffix = + match tag with + | Alpha -> $"a{number}" + | Beta -> $"b{number}" + | ReleaseCandidate -> $"rc{number}" + sprintf "%i.%i.%i%s" semVer.Major semVer.Minor semVer.Patch suffix diff --git a/build/PackageTasks.fs b/build/PackageTasks.fs index 527cdd2..8b532d4 100644 --- a/build/PackageTasks.fs +++ b/build/PackageTasks.fs @@ -5,57 +5,111 @@ open ProjectInfo open MessagePrompts open BasicTasks open TestTasks +open Helpers open BlackFox.Fake open Fake.Core open Fake.IO.Globbing.Operators -let pack = BuildTask.create "Pack" [clean; build; runTests] { - if promptYesNo (sprintf "creating stable package with version %s OK?" stableVersionTag ) - then - !! "src/**/*.*proj" - -- "src/bin/*" - |> Seq.iter (Fake.DotNet.DotNet.pack (fun p -> - let msBuildParams = - {p.MSBuildParams with - Properties = ([ - "Version",stableVersionTag - "AssemblyVersion", assemblyVersion - "AssemblyInformationalVersion", stableVersionTag - "PackageReleaseNotes", (release.Notes |> String.concat "\r\n") - ] @ p.MSBuildParams.Properties) - } - { - p with - MSBuildParams = msBuildParams - OutputPath = Some pkgDir +open System.Text.RegularExpressions + +/// https://github.com/Freymaurer/Fake.Extensions.Release#release-notes-in-nuget +let private replaceCommitLink input = + let commitLinkPattern = @"\[\[#[a-z0-9]*\]\(.*\)\] " + Regex.Replace(input,commitLinkPattern,"") + +module BundleDotNet = + let bundle (versionTag : string) (versionSuffix : string option) = + System.IO.Directory.CreateDirectory(ProjectInfo.netPkgDir) |> ignore + !! "src/**/*.*proj" + -- "src/bin/*" + |> Seq.iter (Fake.DotNet.DotNet.pack (fun p -> + let msBuildParams = + {p.MSBuildParams with + Properties = ([ + "Version",versionTag + "PackageReleaseNotes", (ProjectInfo.release.Notes |> List.map replaceCommitLink |> String.toLines ) + ] @ p.MSBuildParams.Properties) } - )) - else failwith "aborted" + { + p with + VersionSuffix = versionSuffix + MSBuildParams = msBuildParams + OutputPath = Some ProjectInfo.netPkgDir + } + )) + +let packDotNet = BuildTask.create "PackDotNet" [clean; build; runTests] { + BundleDotNet.bundle ProjectInfo.stableVersionTag None +} + +let packDotNetPrerelease = BuildTask.create "PackDotNetPrerelease" [setPrereleaseTag; clean; build; runTests] { + let prereleaseTag = PreReleaseFlag.toNugetTag release.SemVer prereleaseSuffix prereleaseSuffixNumber + BundleDotNet.bundle prereleaseTag (Some prereleaseTag) +} + +module BundleJs = + let bundle (versionTag: string) = + Fake.JavaScript.Npm.run "bundlejs" (fun o -> o) + //GenerateIndexJs.DynamicObj_generate ProjectInfo.npmPkgDir + failwith "implement IndexJs generation" + Fake.IO.File.readAsString "build/release_package.json" + |> fun t -> + let t = t.Replace(ProjectInfo.stableVersionTag, versionTag) + Fake.IO.File.writeString false $"{ProjectInfo.npmPkgDir}/package.json" t + + Fake.IO.File.readAsString "README.md" + |> Fake.IO.File.writeString false $"{ProjectInfo.npmPkgDir}/README.md" + + "" // "fable-library.**/**" + |> Fake.IO.File.writeString false $"{ProjectInfo.npmPkgDir}/fable_modules/.npmignore" + + Fake.JavaScript.Npm.exec "pack" (fun o -> + { o with + WorkingDirectory = ProjectInfo.npmPkgDir + }) + +let packJS = BuildTask.create "PackJS" [clean; build; runTests] { + BundleJs.bundle ProjectInfo.stableVersionTag +} + +let packJSPrerelease = BuildTask.create "PackJSPrerelease" [setPrereleaseTag; clean; build; runTests] { + let prereleaseTag = PreReleaseFlag.toNPMTag release.SemVer prereleaseSuffix prereleaseSuffixNumber + BundleJs.bundle prereleaseTag +} + +module BundlePy = + let bundle (versionTag: string) = + + run dotnet $"fable src/DynamicObj -o {ProjectInfo.pyPkgDir}/DynamicObj --lang python" "" + run python "-m poetry install --no-root" ProjectInfo.pyPkgDir + //GenerateIndexPy.DynamicObj_generate (ProjectInfo.pyPkgDir + "/dynamicObj") + failwith "implement IndexJs generation" + Fake.IO.File.readAsString "pyproject.toml" + |> fun t -> + let t = t.Replace(ProjectInfo.stableVersionTag, versionTag) + Fake.IO.File.writeString false $"{ProjectInfo.pyPkgDir}/pyproject.toml" t + + Fake.IO.File.readAsString "README.md" + |> Fake.IO.File.writeString false $"{ProjectInfo.pyPkgDir}/README.md" + + //"" // "fable-library.**/**" + //|> Fake.IO.File.writeString false $"{ProjectInfo.npmPkgDir}/fable_modules/.npmignore" + + run python "-m poetry build" ProjectInfo.pyPkgDir //Remove "-o ." because not compatible with publish + + +let packPy = BuildTask.create "PackPy" [clean; build; runTests] { + BundlePy.bundle ProjectInfo.stableVersionTag + } -let packPrerelease = BuildTask.create "PackPrerelease" [setPrereleaseTag; clean; build; runTests] { - if promptYesNo (sprintf "package tag will be %s OK?" prereleaseTag ) - then - !! "src/**/*.*proj" - -- "src/bin/*" - |> Seq.iter (Fake.DotNet.DotNet.pack (fun p -> - let msBuildParams = - {p.MSBuildParams with - Properties = ([ - "Version", prereleaseTag - "AssemblyVersion", assemblyVersion - "AssemblyInformationalVersion", stableVersionTag - "PackageReleaseNotes", (release.Notes |> String.toLines ) - ] @ p.MSBuildParams.Properties) - } - { - p with - VersionSuffix = Some prereleaseSuffix - OutputPath = Some pkgDir - MSBuildParams = msBuildParams - } - )) - else - failwith "aborted" -} \ No newline at end of file +let packPyPrerelease = BuildTask.create "PackPyPrerelease" [setPrereleaseTag; clean; build; runTests] { + let prereleaseTag = PreReleaseFlag.toPyPITag release.SemVer prereleaseSuffix prereleaseSuffixNumber + BundlePy.bundle prereleaseTag + } + + +let pack = BuildTask.createEmpty "Pack" [packDotNet; packJS; packPy] + +let packPrerelease = BuildTask.createEmpty "PackPrerelease" [packDotNetPrerelease;packJSPrerelease;packPyPrerelease] \ No newline at end of file diff --git a/build/ProjectInfo.fs b/build/ProjectInfo.fs index 0ab6745..0418ce6 100644 --- a/build/ProjectInfo.fs +++ b/build/ProjectInfo.fs @@ -1,15 +1,22 @@ module ProjectInfo open Fake.Core +open Helpers let project = "DynamicObj" let summary = "F# library supporting Dynamic Objects including inheritance in functional style." +let fableTestProjects = + [ + "tests/DynamicObject.Tests" + ] + let testProjects = [ - "tests/UnitTests/UnitTests.fsproj" - "tests/CSharpTests/CSharpTests.csproj" + "tests/DynamicObject.Tests" + "tests/DynamicObject.Immutable.Tests" + "tests/CSharpTests" ] let solutionFile = $"{project}.sln" @@ -22,7 +29,9 @@ let gitHome = $"https://github.com/{gitOwner}" let projectRepo = $"https://github.com/{gitOwner}/{project}" -let pkgDir = "pkg" +let netPkgDir = "./dist/net" +let npmPkgDir = "./dist/js" +let pyPkgDir = "./dist/py" let release = ReleaseNotes.load "RELEASE_NOTES.md" @@ -34,8 +43,8 @@ let assemblyVersion = $"{stableVersion.Major}.0.0" let assemblyInformationalVersion = $"{stableVersion.Major}.{stableVersion.Minor}.{stableVersion.Patch}" -let mutable prereleaseSuffix = "" +let mutable prereleaseSuffix = PreReleaseFlag.Alpha -let mutable prereleaseTag = "" +let mutable prereleaseSuffixNumber = 0 let mutable isPrerelease = false \ No newline at end of file diff --git a/build/ReleaseTasks.fs b/build/ReleaseTasks.fs index c292e51..e574d30 100644 --- a/build/ReleaseTasks.fs +++ b/build/ReleaseTasks.fs @@ -6,6 +6,7 @@ open BasicTasks open TestTasks open PackageTasks open DocumentationTasks +open Helpers open BlackFox.Fake open Fake.Core @@ -15,7 +16,7 @@ open Fake.Tools open Fake.IO open Fake.IO.Globbing.Operators -let createTag = BuildTask.create "CreateTag" [clean; build; runTests; pack] { +let createTag = BuildTask.create "CreateTag" [clean; build; runTests; packDotNet] { if promptYesNo (sprintf "tagging branch with %s OK?" stableVersionTag ) then Git.Branches.tag "" stableVersionTag Git.Branches.pushTag "" projectRepo stableVersionTag @@ -23,7 +24,9 @@ let createTag = BuildTask.create "CreateTag" [clean; build; runTests; pack] { failwith "aborted" } -let createPrereleaseTag = BuildTask.create "CreatePrereleaseTag" [setPrereleaseTag; clean; build; runTests; packPrerelease] { +let createPrereleaseTag = BuildTask.create "CreatePrereleaseTag" [setPrereleaseTag; clean; build; runTests; packDotNetPrerelease] { + let prereleaseTag = PreReleaseFlag.toNugetTag release.SemVer prereleaseSuffix prereleaseSuffixNumber + if promptYesNo (sprintf "tagging branch with %s OK?" prereleaseTag ) then Git.Branches.tag "" prereleaseTag Git.Branches.pushTag "" projectRepo prereleaseTag @@ -31,33 +34,91 @@ let createPrereleaseTag = BuildTask.create "CreatePrereleaseTag" [setPrereleaseT failwith "aborted" } - -let publishNuget = BuildTask.create "PublishNuget" [clean; build; runTests; pack] { - let targets = (!! (sprintf "%s/*.*pkg" pkgDir )) +let publishNuget = BuildTask.create "PublishNuget" [clean; build; runTests; packDotNet] { + let targets = (!! (sprintf "%s/*.*pkg" netPkgDir )) for target in targets do printfn "%A" target - let msg = sprintf "release package with version %s?" stableVersionTag + let msg = sprintf "[NUGET] release package with version %s?" stableVersionTag if promptYesNo msg then let source = "https://api.nuget.org/v3/index.json" - let apikey = Environment.environVar "NUGET_KEY_CSB" + let apikey = Environment.environVar "NUGET_KEY" for artifact in targets do let result = DotNet.exec id "nuget" (sprintf "push -s %s -k %s %s --skip-duplicate" source apikey artifact) if not result.OK then failwith "failed to push packages" else failwith "aborted" } -let publishNugetPrerelease = BuildTask.create "PublishNugetPrerelease" [clean; build; runTests; packPrerelease] { - let targets = (!! (sprintf "%s/*.*pkg" pkgDir )) +let publishNugetPrerelease = BuildTask.create "PublishNugetPrerelease" [clean; build; runTests; packDotNetPrerelease] { + let targets = (!! (sprintf "%s/*.*pkg" netPkgDir )) for target in targets do printfn "%A" target - let msg = sprintf "release package with version %s?" prereleaseTag + let prereleaseTag = PreReleaseFlag.toNugetTag release.SemVer prereleaseSuffix prereleaseSuffixNumber + let msg = sprintf "[NUGET] release package with version %s?" prereleaseTag if promptYesNo msg then let source = "https://api.nuget.org/v3/index.json" - let apikey = Environment.environVar "NUGET_KEY_CSB" + let apikey = Environment.environVar "NUGET_KEY" for artifact in targets do let result = DotNet.exec id "nuget" (sprintf "push -s %s -k %s %s --skip-duplicate" source apikey artifact) if not result.OK then failwith "failed to push packages" else failwith "aborted" } +let publishNPM = BuildTask.create "PublishNPM" [clean; build; runTests; packJS] { + let target = + (!! (sprintf "%s/*.tgz" npmPkgDir )) + |> Seq.head + printfn "%A" target + let msg = sprintf "[NPM] release package with version %s?" stableVersionTag + if promptYesNo msg then + let apikey = Environment.environVarOrNone "NPM_KEY" + let otp = if apikey.IsSome then $" --otp {apikey.Value}" else "" + Fake.JavaScript.Npm.exec $"publish {target} --access public{otp}" (fun o -> + { o with + WorkingDirectory = "./dist/js/" + }) + else failwith "aborted" +} + +let publishNPMPrerelease = BuildTask.create "PublishNPMPrerelease" [clean; build; runTests; packJSPrerelease] { + let target = + (!! (sprintf "%s/*.tgz" npmPkgDir )) + |> Seq.head + printfn "%A" target + let prereleaseTag = PreReleaseFlag.toNPMTag release.SemVer prereleaseSuffix prereleaseSuffixNumber + let msg = sprintf "[NPM] release package with version %s?" prereleaseTag + if promptYesNo msg then + let apikey = Environment.environVarOrNone "NPM_KEY" + let otp = if apikey.IsSome then $" --otp {apikey.Value}" else "" + Fake.JavaScript.Npm.exec $"publish {target} --access public --tag next{otp}" (fun o -> + { o with + WorkingDirectory = "./dist/js/" + }) + else failwith "aborted" +} + +let publishPyPi = BuildTask.create "PublishPyPi" [clean; build; runTests; packPy] { + let msg = sprintf "[PyPi] release package with version %s?" stableVersionTag + if promptYesNo msg then + let apikey = Environment.environVarOrNone "PYPI_KEY" + match apikey with + | Some key -> + run python $"-m poetry config pypi-token.pypi {key}" ProjectInfo.pyPkgDir + | None -> () + run python "-m poetry publish" ProjectInfo.pyPkgDir + else failwith "aborted" +} + +let publishPyPiPrerelease = BuildTask.create "PublishPyPiPrerelease" [clean; build; runTests; packPyPrerelease] { + let prereleaseTag = PreReleaseFlag.toPyPITag release.SemVer prereleaseSuffix prereleaseSuffixNumber + let msg = sprintf "[PyPi] release package with version %s?" prereleaseTag + if promptYesNo msg then + let apikey = Environment.environVarOrNone "PYPI_KEY" + match apikey with + | Some key -> + run python $"-m poetry config pypi-token.pypi {key}" ProjectInfo.pyPkgDir + | None -> () + run python "-m poetry publish --build" ProjectInfo.pyPkgDir + else failwith "aborted" +} + let releaseDocs = BuildTask.create "ReleaseDocs" [buildDocs] { let msg = sprintf "release docs for version %s?" stableVersionTag if promptYesNo msg then @@ -72,6 +133,7 @@ let releaseDocs = BuildTask.create "ReleaseDocs" [buildDocs] { } let prereleaseDocs = BuildTask.create "PrereleaseDocs" [buildDocsPrerelease] { + let prereleaseTag = PreReleaseFlag.toPyPITag release.SemVer prereleaseSuffix prereleaseSuffixNumber let msg = sprintf "release docs for version %s?" prereleaseTag if promptYesNo msg then Shell.cleanDir "temp" diff --git a/build/TestTasks.fs b/build/TestTasks.fs index 7055e8b..360f9fb 100644 --- a/build/TestTasks.fs +++ b/build/TestTasks.fs @@ -5,21 +5,57 @@ open Fake.DotNet open ProjectInfo open BasicTasks +open Fake.Core -let runTests = BuildTask.create "RunTests" [clean; build] { - testProjects - |> Seq.iter (fun testProject -> - Fake.DotNet.DotNet.test(fun testParams -> - { - testParams with - Logger = Some "console;verbosity=detailed" - Configuration = DotNet.BuildConfiguration.fromString configuration - NoBuild = true - } - ) testProject - ) +module RunTests = + + + //let runTestsJsNative = BuildTask.create "runTestsJSNative" [clean; build] { + // Trace.traceImportant "Start native JavaScript tests" + // for path in ProjectInfo.jsTestProjects do + // // transpile library for native access + // run dotnet $"fable src/ARCtrl -o {path}/ARCtrl" "" + // GenerateIndexJs.ARCtrl_generate($"{path}/ARCtrl") + // run npx $"mocha {path} --timeout 20000" "" + //} + + 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 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" "" + } + + //let runTestsPyNative = BuildTask.create "runTestsPyNative" [clean; build] { + // Trace.traceImportant "Start native Python tests" + // for path in ProjectInfo.pyTestProjects do + // // transpile library for native access + // run dotnet $"fable src/ARCtrl -o {path}/ARCtrl --lang python" "" + // GenerateIndexPy.ARCtrl_generate($"{path}/ARCtrl") + // run python $"-m pytest {path}" "" + //} + + 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 pyxpecto in target path to execute tests in python + run python $"{path}/py/main.py" "" + } + + let runTestsDotnet = BuildTask.create "runTestsDotnet" [clean; build] { + let dotnetRun = run dotnet "run" + testProjects + |> Seq.iter dotnetRun + } + +let runTests = BuildTask.create "RunTests" [clean; build; RunTests.runTestsJs; (*RunTests.runTestsJsNative; *)RunTests.runTestsPy;(*RunTests.runTestsPyNative; *)RunTests.runTestsDotnet] { + () } + // to do: use this once we have actual tests let runTestsWithCodeCov = BuildTask.create "RunTestsWithCodeCov" [clean; build] { let standardParams = Fake.DotNet.MSBuild.CliArguments.Create () diff --git a/build/build.fsproj b/build/build.fsproj index 4c3e5c1..c836ff6 100644 --- a/build/build.fsproj +++ b/build/build.fsproj @@ -19,14 +19,19 @@ - - - - - - - - + + + + + + + + + + + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..7a950e4 --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "type": "module", + "scripts": { + "mocha": "mocha" + }, + "dependencies": { + "fable-library": "^1.1.1" + }, + "devDependencies": { + "mkdirp": "3.0.1", + "mocha": "^10.2.0" + } +} diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..5e4e520 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,174 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "certifi" +version = "2024.7.4" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, +] + +[[package]] +name = "charset-normalizer" +version = "2.1.1" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, + {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, +] + +[package.extras] +unicode-backport = ["unicodedata2"] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "idna" +version = "3.7" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "packaging" +version = "24.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pytest" +version = "8.3.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, + {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "requests" +version = "2.28.1" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7, <4" +files = [ + {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, + {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<3" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "urllib3" +version = "1.26.19" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "urllib3-1.26.19-py2.py3-none-any.whl", hash = "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3"}, + {file = "urllib3-1.26.19.tar.gz", hash = "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429"}, +] + +[package.extras] +brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "b71d5769d70eac536444eb6d8a3f12e57d59113029b8de56c9bf24e9a8598dc8" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9da229c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,20 @@ +[tool.poetry] +name = "DynamicObj" +version = "3.0.0" +description = "Fable compatible library supporting Dynamic Objects including inheritance in functional style." +authors = ["Kevin Schneider", "WhiteBlackGoose", "Heinrich Lukas Weil", "Timo Muehlhaus", "Kevin Frey", "David Zimmer"] +maintainers = ["Heinrich Lukas Weil", "Timo Muehlhaus", "Kevin Schneider"] +readme = "README.md" +repository = "https://github.com/CSBiology/DynamicObj" +keywords = ["Dynamic Object", "Fable", "FSharp", "Javascript", "Python"] + +[tool.poetry.dependencies] +python = "^3.10" +requests = "2.28.1" + +[tool.poetry.group.dev.dependencies] +pytest = "^8.1.1" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" \ No newline at end of file diff --git a/src/DynamicObj.Immutable/DynamicObj.Immutable.fsproj b/src/DynamicObj.Immutable/DynamicObj.Immutable.fsproj new file mode 100644 index 0000000..39fd78e --- /dev/null +++ b/src/DynamicObj.Immutable/DynamicObj.Immutable.fsproj @@ -0,0 +1,41 @@ + + + + netstandard2.0 + true + true + true + true + snupkg + true + ../../key.snk + + + + Timo Mühlhaus, Kevin Schneider, F# open source contributors + F# library supporting Dynamic Objects including inheritance in functional style. + MIT + https://csbiology.github.io/DynamicObj/ + F# FSharp dotnet dynamic object + https://github.com/CSBiology/DynamicObj + git + https://github.com/CSBiology/DynamicObj/blob/main/LICENSE + https://github.com/CSBiology/DynamicObj/blob/main/RELEASE_NOTES.md + + + + + + + + + + + + + + + + + + diff --git a/src/DynamicObj/ImmutableDynamicObj.fs b/src/DynamicObj.Immutable/ImmutableDynamicObj.fs similarity index 99% rename from src/DynamicObj/ImmutableDynamicObj.fs rename to src/DynamicObj.Immutable/ImmutableDynamicObj.fs index b624208..94d4d41 100644 --- a/src/DynamicObj/ImmutableDynamicObj.fs +++ b/src/DynamicObj.Immutable/ImmutableDynamicObj.fs @@ -13,7 +13,7 @@ do() /// Represents an DynamicObj's counterpart /// with immutability enabled only. [)>] -type ImmutableDynamicObj internal (map : Map) = +type ImmutableDynamicObj (map : Map) = let mutable properties = map diff --git a/src/DynamicObj/Operators.fs b/src/DynamicObj.Immutable/Operators.fs similarity index 100% rename from src/DynamicObj/Operators.fs rename to src/DynamicObj.Immutable/Operators.fs diff --git a/src/DynamicObj/DynObj.fs b/src/DynamicObj/DynObj.fs index 6df46f2..685e631 100644 --- a/src/DynamicObj/DynObj.fs +++ b/src/DynamicObj/DynObj.fs @@ -5,26 +5,26 @@ open System.Collections.Generic module DynObj = /// New DynamicObj of Dictionary - let ofDict dict = DynamicObj(dict) + let ofDict dict = DynamicObj.fromDict dict /// New DynamicObj of a sequence of key value let ofSeq kv = let dict = new Dictionary() kv |> Seq.iter (fun (k,v) -> dict.Add(k,v)) - DynamicObj(dict) + DynamicObj.fromDict dict /// New DynamicObj of a list of key value let ofList kv = let dict = new Dictionary() kv |> List.iter (fun (k,v) -> dict.Add(k,v)) - DynamicObj(dict) + DynamicObj.fromDict dict /// New DynamicObj of an array of key value let ofArray kv = let dict = new Dictionary() kv |> Array.iter (fun (k,v) -> dict.Add(k,v)) - DynamicObj(dict) + DynamicObj.fromDict dict // @@ -79,30 +79,30 @@ module DynObj = | None -> () let tryGetValue (dyn:DynamicObj) name = - match dyn.TryGetMember name with - | true,value -> Some value - | _ -> None + dyn.TryGetValue name let remove (dyn:DynamicObj) propName = - DynamicObj.Remove (dyn, propName) |> ignore + DynamicObj.remove (dyn, propName) |> ignore let format (d:DynamicObj) = - let members = d.GetDynamicMemberNames() |> Seq.cast |> List.ofSeq + let members = d.GetPropertyHelpers(true) |> List.ofSeq - let rec loop (object:DynamicObj) (identationLevel:int) (membersLeft:string list) (acc:string list) = - let ident = [for i in 0 .. identationLevel-1 do yield " "] |> String.concat "" + let rec loop (object:DynamicObj) (indentationLevel:int) (membersLeft:PropertyHelper list) (acc:string list) = + let indent = [for i in 0 .. indentationLevel-1 do yield " "] |> String.concat "" match membersLeft with | [] -> acc |> List.rev |> String.concat System.Environment.NewLine | m::rest -> - let item = object?(``m``) + let item = m.GetValue object + let dynamicIndicator = if m.IsDynamic then "?" else "" + let name = m.Name match item with | :? DynamicObj as item -> - let innerMembers = item.GetDynamicMemberNames() |> Seq.cast |> List.ofSeq - let innerPrint = (loop item (identationLevel + 1) innerMembers []) - loop object identationLevel rest ($"{ident}?{m}:{System.Environment.NewLine}{innerPrint}" :: acc) - | _ -> - loop object identationLevel rest ($"{ident}?{m}: {item}"::acc) + let innerMembers = item.GetPropertyHelpers(true) |> List.ofSeq + let innerPrint = (loop item (indentationLevel + 1) innerMembers []) + loop object indentationLevel rest ($"{indent}{dynamicIndicator}{name}:{System.Environment.NewLine}{innerPrint}" :: acc) + | item -> + loop object indentationLevel rest ($"{indent}{dynamicIndicator}{name}: {item}"::acc) loop d 0 members [] diff --git a/src/DynamicObj/DynamicObj.fs b/src/DynamicObj/DynamicObj.fs index 5685e1d..8f6ac2f 100644 --- a/src/DynamicObj/DynamicObj.fs +++ b/src/DynamicObj/DynamicObj.fs @@ -1,19 +1,22 @@ namespace DynamicObj -open System.Dynamic +//open System.Dynamic open System.Collections.Generic +open Fable.Core - -type DynamicObj internal (dict:Dictionary) = - - inherit DynamicObject () +[] +type DynamicObj() = - let properties = dict//new Dictionary() + let mutable properties = new Dictionary() - member private this.Properties = properties + member this.Properties + with get() = properties + and internal set(value) = properties <- value - /// - new () = DynamicObj(new Dictionary()) + static member fromDict dict = + let obj = DynamicObj() + obj.Properties <- dict + obj /// Gets property value member this.TryGetValue name = @@ -23,6 +26,9 @@ type DynamicObj internal (dict:Dictionary) = // Next check for Public properties via Reflection | _ -> ReflectionUtils.tryGetPropertyValue this name + + member this.GetValue (name) = + this.TryGetValue(name).Value /// Gets property value member this.TryGetTypedValue<'a> name = @@ -37,25 +43,26 @@ type DynamicObj internal (dict:Dictionary) = /// Sets property value, creating a new property if none exists member this.SetValue (name,value) = // private // first check to see if there's a native property to set - - match ReflectionUtils.tryGetPropertyInfo this name with - | Some property -> - try - // let t = property.ReflectedType - // t.InvokeMember(name,Reflection.BindingFlags.SetProperty,null,this,[|value|]) |> ignore - - //let tmp = Convert.ChangeType(this, property.ReflectedType) - //let tmp = downcast this : (typeof) - property.SetValue(this, value, null) - with - | :? System.ArgumentException -> raise <| System.ArgumentException("Readonly property - Property set method not found.") - | :? System.NullReferenceException -> raise <| System.NullReferenceException() + match ReflectionUtils.tryGetPropertyInfo this name with + | Some pi -> + if pi.IsMutable then + pi.SetValue this value + else + failwith $"Cannot set value for static, immutable property \"{name}\"" | None -> + #if FABLE_COMPILER_JAVASCRIPT || FABLE_COMPILER_TYPESCRIPT + FableJS.setPropertyValue this name value + #endif + #if FABLE_COMPILER_PYTHON + FablePy.setPropertyValue this name value + #endif + #if !FABLE_COMPILER // Next check the Properties collection for member match properties.TryGetValue name with | true,_ -> properties.[name] <- value | _ -> properties.Add(name,value) + #endif member this.Remove name = match ReflectionUtils.removeProperty this name with @@ -64,75 +71,124 @@ type DynamicObj internal (dict:Dictionary) = | false -> properties.Remove(name) - override this.TryGetMember(binder:GetMemberBinder,result:obj byref ) = - match this.TryGetValue binder.Name with - | Some value -> result <- value; true - | None -> false - - - override this.TrySetMember(binder:SetMemberBinder, value:obj) = - this.SetValue(binder.Name,value) - true + member this.GetPropertyHelpers (includeInstanceProperties) = + #if FABLE_COMPILER_JAVASCRIPT || FABLE_COMPILER_TYPESCRIPT + FableJS.getPropertyHelpers this + |> Seq.filter (fun pd -> + includeInstanceProperties || pd.IsDynamic + ) + #endif + #if FABLE_COMPILER_PYTHON + FablePy.getPropertyHelpers this + |> Seq.filter (fun pd -> + includeInstanceProperties || pd.IsDynamic + ) + #endif + #if !FABLE_COMPILER + seq [ + if includeInstanceProperties then + yield! ReflectionUtils.getStaticProperties (this) + for key in properties.Keys -> + { + Name = key + IsStatic = false + IsDynamic = true + IsMutable = true + IsImmutable = false + GetValue = fun o -> properties.[key] + SetValue = fun o v -> properties.[key] <- v + RemoveValue = fun o -> properties.Remove(key) |> ignore + } + ] + #endif + |> Seq.filter (fun p -> p.Name.ToLower() <> "properties") /// Returns both instance and dynamic properties when passed true, only dynamic properties otherwise. /// Properties are returned as a key value pair of the member names and the boxed values - member this.GetProperties includeInstanceProperties = + member this.GetProperties includeInstanceProperties : seq> = + #if FABLE_COMPILER_JAVASCRIPT || FABLE_COMPILER_TYPESCRIPT + FableJS.getPropertyHelpers this + |> Seq.choose (fun pd -> + if includeInstanceProperties || pd.IsDynamic then + new KeyValuePair(pd.Name, pd.GetValue this) + |> Some + else + None + ) + #endif + #if FABLE_COMPILER_PYTHON + FablePy.getPropertyHelpers this + |> Seq.choose (fun pd -> + if includeInstanceProperties || pd.IsDynamic then + new KeyValuePair(pd.Name, pd.GetValue this) + |> Some + else + None + ) + #endif + #if !FABLE_COMPILER seq [ if includeInstanceProperties then - for prop in ReflectionUtils.getPublicProperties (this.GetType()) -> - new KeyValuePair(prop.Name, prop.GetValue(this, null)) + for prop in ReflectionUtils.getStaticProperties (this) -> + new KeyValuePair(prop.Name, prop.GetValue(this)) for key in properties.Keys -> new KeyValuePair(key, properties.[key]); ] + #endif + |> Seq.filter (fun kv -> kv.Key.ToLower() <> "properties") - /// Returns both instance and dynamic member names. - /// Important to return both so JSON serialization with Json.NET works. - override this.GetDynamicMemberNames() = - this.GetProperties(true) |> Seq.map (fun pair -> pair.Key) + member this.GetPropertyNames(includeInstanceProperties) = + this.GetProperties(includeInstanceProperties) + |> Seq.map (fun kv -> kv.Key) + + /// /// Operator to access a dynamic member by name + /// + /// This operator is not Fable-compatible static member (?) (lookup:#DynamicObj,name:string) = match lookup.TryGetValue name with | Some(value) -> value | None -> raise <| System.MemberAccessException() + /// /// Operator to set a dynamic member + /// + /// This operator is not Fable-compatible static member (?<-) (lookup:#DynamicObj,name:string,value:'v) = lookup.SetValue (name,value) - /// Copies all dynamic members of the DynamicObj to the target DynamicObj. - member this.CopyDynamicPropertiesTo(target:#DynamicObj) = - this.GetProperties(false) - |> Seq.iter (fun kv -> - target?(kv.Key) <- kv.Value - ) + ///// Copies all dynamic members of the DynamicObj to the target DynamicObj. + //member this.CopyDynamicPropertiesTo(target:#DynamicObj) = + // this.GetProperties(false) + // |> Seq.iter (fun kv -> + // target?(kv.Key) <- kv.Value + // ) - /// Returns a new DynamicObj with only the dynamic properties of the original DynamicObj (sans instance properties). - member this.CopyDynamicProperties() = - let target = DynamicObj() - this.CopyDynamicPropertiesTo(target) - target + ///// Returns a new DynamicObj with only the dynamic properties of the original DynamicObj (sans instance properties). + //member this.CopyDynamicProperties() = + // let target = DynamicObj() + // this.CopyDynamicPropertiesTo(target) + // target - static member GetValue (lookup:DynamicObj,name) = - lookup.TryGetValue(name).Value - static member Remove (lookup:DynamicObj,name) = + static member getValue (lookup:DynamicObj,name) = + lookup.GetValue(name) + + static member remove (lookup:DynamicObj,name) = lookup.Remove(name) override this.Equals o = match o with | :? DynamicObj as other -> - let subdictOf (super : Dictionary<'a, 'b>) (dict : Dictionary<'a, 'b>) = - dict - |> Seq.forall (fun pair -> - let (contains, value) = super.TryGetValue pair.Key - contains && value.Equals(pair.Value)) - subdictOf this.Properties other.Properties + this.GetHashCode() = other.GetHashCode() | _ -> false override this.GetHashCode () = - this.Properties - |> List.ofSeq - |> List.sortBy (fun pair -> pair.Key) - |> List.map (fun pair -> struct (pair.Key, pair.Value)) - |> (fun l -> l.GetHashCode()) \ No newline at end of file + this.GetProperties(true) + |> Seq.map (fun kv -> + kv + ) + |> Seq.sortBy (fun pair -> pair.Key) + |> HashCodes.boxHashKeyValSeq + |> fun x -> x :?> int \ No newline at end of file diff --git a/src/DynamicObj/DynamicObj.fsproj b/src/DynamicObj/DynamicObj.fsproj index 0d4eb44..f58a274 100644 --- a/src/DynamicObj/DynamicObj.fsproj +++ b/src/DynamicObj/DynamicObj.fsproj @@ -12,7 +12,7 @@ - Timo Mühlhaus, Kevin Schneider, F# open source contributors + Timo Mühlhaus, Kevin Schneider, Heinrich Lukas Weil, F# open source contributors F# library supporting Dynamic Objects including inheritance in functional style. MIT https://csbiology.github.io/DynamicObj/ @@ -24,17 +24,23 @@ + + + + - - + + + + diff --git a/src/DynamicObj/FableJS.fs b/src/DynamicObj/FableJS.fs new file mode 100644 index 0000000..b8a5e3e --- /dev/null +++ b/src/DynamicObj/FableJS.fs @@ -0,0 +1,148 @@ +namespace DynamicObj + + +open Fable.Core + +module FableJS = + + module PropertyDescriptor = + + [] + let tryGetPropertyValue (o:obj) (propName:string) : obj option = + jsNative + + let tryGetIsWritable (o:obj) : bool option = + tryGetPropertyValue o "writable" + |> Option.map (fun v -> v :?> bool) + + let containsGetter (o:obj) : bool = + match tryGetPropertyValue o "get" with + | Some _ -> true + | None -> false + + let containsSetter (o:obj) : bool = + match tryGetPropertyValue o "set" with + | Some _ -> true + | None -> false + + let isWritable (o:obj) : bool = + match tryGetIsWritable o with + | Some v -> v + | None -> containsSetter o + + [] + let valueIsFunction (o:obj) : bool = + jsNative + + let isFunction (o:obj) : bool = + match tryGetPropertyValue o "value" with + | Some v -> valueIsFunction v + | None -> false + + [] + let getOwnPropertyNames (o:obj) : string [] = + jsNative + + [] + let getPrototype (o:obj) : obj = + jsNative + + let getStaticPropertyNames (o:obj) = + getPrototype o + |> getOwnPropertyNames + |> Array.filter (fun n -> n <> "constructor") + + [] + let setPropertyValue (o:obj) (propName:string) (value:obj) = + jsNative + + let createSetter (propName:string) = + fun (o:obj) (value:obj) -> + setPropertyValue o propName value + + let removeStaticPropertyValue (o:obj) (propName:string) = + setPropertyValue o propName null + + [] + let deleteDynamicPropertyValue (o:obj) (propName:string) = + jsNative + + let createRemover (propName:string) (isStatic : bool) = + if isStatic then + fun (o:obj) -> + removeStaticPropertyValue o propName + else + fun (o:obj) -> + deleteDynamicPropertyValue o propName + + + [] + let getPropertyValue (o:obj) (propName:string) = + jsNative + + let createGetter (propName:string) = + fun (o:obj) -> + getPropertyValue o propName + + [] + let getPropertyDescriptor (o:obj) (propName:string) = + jsNative + + let getStaticPropertyDescriptor (o:obj) (propName:string) = + getPropertyDescriptor (getPrototype o) propName + + let getStaticPropertyHelpers (o:obj) : PropertyHelper [] = + getStaticPropertyNames o + |> Array.choose (fun n -> + let pd = getStaticPropertyDescriptor o n + if PropertyDescriptor.isFunction pd then + None + else + let isWritable = PropertyDescriptor.isWritable pd + { + Name = n + IsStatic = true + IsDynamic = false + IsMutable = isWritable + IsImmutable = not isWritable + GetValue = createGetter n + SetValue = createSetter n + RemoveValue = createRemover n true + } + |> Some + ) + + let transpiledPropertyRegex = "^[a-zA-Z]+@[0-9]+$" + + let isTranspiledPropertyHelper (propertyName : string) = + System.Text.RegularExpressions.Regex.IsMatch(propertyName, transpiledPropertyRegex) + + let getDynamicPropertyHelpers (o:obj) : PropertyHelper [] = + getOwnPropertyNames o + |> Array.choose (fun n -> + let pd = getPropertyDescriptor o n + if PropertyDescriptor.isFunction pd || isTranspiledPropertyHelper n then + None + else + let isWritable = PropertyDescriptor.isWritable pd + { + Name = n + IsStatic = false + IsDynamic = true + IsMutable = isWritable + IsImmutable = not isWritable + GetValue = createGetter n + SetValue = createSetter n + RemoveValue = createRemover n false + } + |> Some + ) + + let getPropertyHelpers (o:obj) = + getDynamicPropertyHelpers o + |> Array.append (getStaticPropertyHelpers o) + + let getPropertyNames (o:obj) = + getPropertyHelpers o + |> Array.map (fun h -> h.Name) + diff --git a/src/DynamicObj/FablePy.fs b/src/DynamicObj/FablePy.fs new file mode 100644 index 0000000..0e43cca --- /dev/null +++ b/src/DynamicObj/FablePy.fs @@ -0,0 +1,211 @@ +namespace DynamicObj + + +open Fable.Core +open System.Collections.Generic + +module FablePy = + + module Dictionary = + + let ofSeq (s:seq>) = + let d = new System.Collections.Generic.Dictionary<_,_>() + s |> Seq.iter (fun kv -> d.Add(kv.Key, kv.Value)) + d + + let choose (f: 'T -> 'U option) (d:System.Collections.Generic.Dictionary<_,'T>) = + let nd = new System.Collections.Generic.Dictionary<_,'U>() + for kv in d do + match f kv.Value with + | Some v -> nd.Add(kv.Key, v) + | None -> () + nd + + type PropertyObject = + abstract fget : obj + abstract fset : obj + + module PropertyObject = + + [] + let tryGetGetter (o:PropertyObject) : (obj -> obj) option = + nativeOnly + + [] + let tryGetSetter (o:PropertyObject) : (obj -> obj -> unit) option = + nativeOnly + + let getGetter (o : PropertyObject) : obj -> obj = + match tryGetGetter o with + | Some f -> f + | None -> fun o -> failwith ("Property does not contain getter") + + let getSetter (o:PropertyObject) : obj -> obj -> unit = + match tryGetSetter o with + | Some f -> f + | None -> fun s o -> failwith ("Property does not contain setter") + + let containsGetter (o:PropertyObject) : bool = + match tryGetGetter o with + | Some _ -> true + | None -> false + + let containsSetter (o:PropertyObject) : bool = + match tryGetSetter o with + | Some _ -> true + | None -> false + + let isWritable (o:PropertyObject) : bool = + containsSetter o + + [] + let isProperty (o:obj) : bool = + nativeOnly + + let tryProperty (o:obj) : PropertyObject option = + if isProperty o then + Some (o :?> PropertyObject) + else + None + + [] + let getPropertyValue (o:obj) (propName:string) = + nativeOnly + + let createGetter (propName:string) = + fun (o:obj) -> + getPropertyValue o propName + + [] + let setPropertyValue (o:obj) (propName:string) (value:obj) : unit = + nativeOnly + + let createSetter (propName:string) = + fun (o:obj) (value:obj) -> + setPropertyValue o propName value + + + [] + let getOwnMemberObjects (o:obj) : Dictionary = + nativeOnly + + [] + let getClass (o:obj) : obj = + nativeOnly + + let getStaticPropertyObjects (o:obj) : Dictionary = + getClass o + |> getOwnMemberObjects + |> Dictionary.choose PropertyObject.tryProperty + + let removeStaticPropertyValue (o:obj) (propName:string) = + setPropertyValue o propName null + + [] + let deleteDynamicPropertyValue (o:obj) (propName:string) = + nativeOnly + + let createRemover (propName:string) (isStatic : bool) = + if isStatic then + fun (o:obj) -> + removeStaticPropertyValue o propName + else + fun (o:obj) -> + deleteDynamicPropertyValue o propName + + + + [] + let getMemberObject (o:obj) (propName:string) = + nativeOnly + + let tryGetPropertyObject (o:obj) (propName:string) : PropertyObject option = + match PropertyObject.tryProperty (getMemberObject o propName) with + | Some po -> Some po + | None -> None + + let tryGetDynamicPropertyHelper (o:obj) (propName:string) : PropertyHelper option = + match getMemberObject o propName with + | Some _ -> + Some { + Name = propName + IsStatic = false + IsDynamic = true + IsMutable = true + IsImmutable = false + GetValue = createGetter propName + SetValue = createSetter propName + RemoveValue = fun o -> deleteDynamicPropertyValue o propName + } + | None -> None + + let tryGetStaticPropertyHelper (o:obj) (propName:string) : PropertyHelper option = + match tryGetPropertyObject (getClass o) propName with + | Some po -> + let isWritable = PropertyObject.isWritable po + Some { + Name = propName + IsStatic = true + IsDynamic = false + IsMutable = isWritable + IsImmutable = not isWritable + GetValue = createGetter propName + SetValue = createSetter propName + RemoveValue = fun o -> removeStaticPropertyValue o propName + } + | None -> None + + let transpiledPropertyRegex = "^[a-zA-Z]+_[0-9]+$" + + let isTranspiledPropertyHelper (propertyName : string) = + System.Text.RegularExpressions.Regex.IsMatch(propertyName, transpiledPropertyRegex) + + + let getDynamicPropertyHelpers (o:obj) : PropertyHelper [] = + getOwnMemberObjects o + |> Seq.choose (fun kv -> + let n = kv.Key + if isTranspiledPropertyHelper n then + None + else + { + Name = n + IsStatic = false + IsDynamic = true + IsMutable = true + IsImmutable = false + GetValue = createGetter n + SetValue = createSetter n + RemoveValue = fun o -> deleteDynamicPropertyValue o n + } + |> Some + ) + |> Seq.toArray + + + let getStaticPropertyHelpers (o:obj) : PropertyHelper [] = + getStaticPropertyObjects o + |> Seq.map (fun kv -> + let n = kv.Key + let po = kv.Value + { + Name = n + IsStatic = true + IsDynamic = false + IsMutable = PropertyObject.isWritable po + IsImmutable = not (PropertyObject.isWritable po) + GetValue = createGetter n + SetValue = createSetter n + RemoveValue = fun o -> removeStaticPropertyValue o n + } + ) + |> Seq.toArray + + let getPropertyHelpers (o:obj) = + getDynamicPropertyHelpers o + |> Array.append (getStaticPropertyHelpers o) + + let getPropertyNames (o:obj) = + getPropertyHelpers o + |> Array.map (fun h -> h.Name) + diff --git a/src/DynamicObj/HashCodes.fs b/src/DynamicObj/HashCodes.fs new file mode 100644 index 0000000..96bd4b4 --- /dev/null +++ b/src/DynamicObj/HashCodes.fs @@ -0,0 +1,46 @@ +module DynamicObj.HashCodes + +let mergeHashes (hash1 : int) (hash2 : int) : int = + 0x9e3779b9 + hash2 + (hash1 <<< 6) + (hash1 >>> 2) + +let hashDateTime (dt : System.DateTime) : int = + let mutable acc = 0 + acc <- mergeHashes acc dt.Year + acc <- mergeHashes acc dt.Month + acc <- mergeHashes acc dt.Day + acc <- mergeHashes acc dt.Hour + acc <- mergeHashes acc dt.Minute + acc <- mergeHashes acc dt.Second + acc + + +let hash obj = + obj.GetHashCode() + +let boxHashOption (a: 'a option) : obj = + if a.IsSome then a.Value.GetHashCode() else (0).GetHashCode() + |> box + +let boxHashArray (a: 'a []) : obj = + a + // from https://stackoverflow.com/a/53507559 + |> Array.fold (fun acc o -> + hash o + |> mergeHashes acc) 0 + |> box + +let boxHashSeq (a: seq<'a>) : obj = + a + // from https://stackoverflow.com/a/53507559 + |> Seq.fold (fun acc o -> + hash o + |> mergeHashes acc) 0 + |> box + +let boxHashKeyValSeq (a: seq>) : obj = + a + // from https://stackoverflow.com/a/53507559 + |> Seq.fold (fun acc o -> + mergeHashes (hash o.Key) (hash o.Value) + |> mergeHashes acc) 0 + |> box \ No newline at end of file diff --git a/src/DynamicObj/Playground.fsx b/src/DynamicObj/Playground.fsx index 0682437..620bbf8 100644 --- a/src/DynamicObj/Playground.fsx +++ b/src/DynamicObj/Playground.fsx @@ -1,67 +1,19 @@ #r "nuget: Newtonsoft.Json" +#r "nuget: Fable.Core" +#r "nuget: Fable.Pyxpecto" + #load "./ReflectionUtils.fs" -#load "./ImmutableDynamicObj.fs" #load "./DynamicObj.fs" #load "./DynObj.fs" -#load "./Operators.fs" +open Fable.Pyxpecto open DynamicObj -open DynamicObj.Operators - -let target = DynamicObj() - -target.SetValue("target-unique", [42]) -target.SetValue("will-be-overridden", "WAS_NOT_OVERRIDDEN!") - -let source = DynamicObj() - -source.SetValue("source-unique", [|42|]) -source.SetValue("will-be-overridden", "WAS_OVERRIDDEN =)") - -let combined = DynObj.combine target source - -let expected = DynamicObj() - -expected.SetValue("target-unique", [42]) -expected.SetValue("will-be-overridden", "WAS_OVERRIDDEN =)") -expected.SetValue("source-unique", [|42|]) - -combined = expected - -combined |> DynObj.print -expected |> DynObj.print - - -let foo = DynamicObj() -foo?bar <- [1;2;3;4] - -(DynObj.print foo) - -let fooIDO = - ImmutableDynamicObj() - |> ImmutableDynamicObj.add "foo" "bar" - |> ImmutableDynamicObj.add "inner" (ImmutableDynamicObj() |> ImmutableDynamicObj.add "innerfoo" "innerbar" ) - |> ImmutableDynamicObj.add "inner2" (ImmutableDynamicObj() |> ImmutableDynamicObj.add "innerinner" (ImmutableDynamicObj() |> ImmutableDynamicObj.add "innerinnerfoo" "innerinnerbar" )) - -printfn "%s" (fooIDO |> ImmutableDynamicObj.format) -let o2 = - ImmutableDynamicObj.empty - ++ ("aaa", 5) - ++ ("ohno", 10) - ++ ("quack", "tt") - ++ ("hh", [1; 2; 3]) - -let o = - ImmutableDynamicObj.empty - ++ ("aaa", 5) - ++ ("ohno", 10) - ++ ("quack", "tt") - ++ ("hh", [1; 2; 3]) - ++ ("inner", o2) -open Newtonsoft.Json +let a = DynamicObj () +a.SetValue("aaa", 5) +let b = DynamicObj () +b.SetValue("aaa", 5) -let actual = JsonConvert.SerializeObject o -printfn "%s" actual \ No newline at end of file +a.GetProperties(true) \ No newline at end of file diff --git a/src/DynamicObj/PropertyHelper.fs b/src/DynamicObj/PropertyHelper.fs new file mode 100644 index 0000000..9c5f564 --- /dev/null +++ b/src/DynamicObj/PropertyHelper.fs @@ -0,0 +1,33 @@ +namespace DynamicObj + +open System.Reflection + +type PropertyHelper = + + { + Name : string + IsStatic : bool + IsDynamic : bool + IsMutable : bool + IsImmutable : bool + GetValue : obj -> obj + SetValue : obj -> obj -> unit + RemoveValue : obj -> unit + } + + #if !FABLE_COMPILER + + static member fromPropertyInfo (pI : PropertyInfo) = + { + Name = pI.Name + IsStatic = true + IsDynamic = false + IsMutable = pI.CanWrite + IsImmutable = not pI.CanWrite + GetValue = fun(o) -> pI.GetValue(o) + SetValue = fun o v -> pI.SetValue(o, v) + RemoveValue = fun o -> pI.SetValue(o, null) + } + + #endif + diff --git a/src/DynamicObj/ReflectionUtils.fs b/src/DynamicObj/ReflectionUtils.fs index a81afd2..fdddbaa 100644 --- a/src/DynamicObj/ReflectionUtils.fs +++ b/src/DynamicObj/ReflectionUtils.fs @@ -1,105 +1,72 @@ namespace DynamicObj + module ReflectionUtils = open System open System.Reflection - // Gets public properties including interface propterties - let getPublicProperties (t:Type) = - [| - for propInfo in t.GetProperties() -> propInfo - for i in t.GetInterfaces() do yield! i.GetProperties() - |] - - /// Creates an instance of the Object according to applyStyle and applies the function.. - let buildApply (applyStyle:'a -> 'a) = - let instance = - System.Activator.CreateInstance<'a>() - applyStyle instance - - /// Applies 'applyStyle' to item option. If None it creates a new instance. - let optBuildApply (applyStyle:'a -> 'a) (item:'a option) = - match item with - | Some item' -> applyStyle item' - | None -> buildApply applyStyle - - /// Applies Some 'applyStyle' to item. If None it returns 'item' unchanged. - let optApply (applyStyle:('a -> 'a) option) (item:'a ) = - match applyStyle with - | Some apply -> apply item - | None -> item - - /// Returns the proptery name from quotation expression - let tryGetPropertyName (expr : Microsoft.FSharp.Quotations.Expr) = - match expr with - | Microsoft.FSharp.Quotations.Patterns.PropertyGet (_,pInfo,_) -> Some pInfo.Name - | _ -> None + // Gets public, static properties including interface propterties + let getStaticProperties (o : obj) = + #if FABLE_COMPILER_JAVASCRIPT || FABLE_COMPILER_TYPESCRIPT + FableJS.getStaticPropertyHelpers o + #endif + #if FABLE_COMPILER_PYTHON + FablePy.getStaticPropertyHelpers o + #endif + #if !FABLE_COMPILER + let t = o.GetType() + [| + for propInfo in t.GetProperties() -> propInfo + for i in t.GetInterfaces() do yield! i.GetProperties() + |] + |> Array.map PropertyHelper.fromPropertyInfo + #endif /// Try to get the PropertyInfo by name using reflection let tryGetPropertyInfo (o:obj) (propName:string) = - getPublicProperties (o.GetType()) + #if FABLE_COMPILER_JAVASCRIPT || FABLE_COMPILER_TYPESCRIPT + FableJS.getPropertyHelpers o + #endif + #if FABLE_COMPILER_PYTHON + FablePy.getPropertyHelpers o + #endif + #if !FABLE_COMPILER + getStaticProperties (o) + #endif |> Array.tryFind (fun n -> n.Name = propName) - /// Sets property value using reflection let trySetPropertyValue (o:obj) (propName:string) (value:obj) = match tryGetPropertyInfo o propName with - | Some property -> - try - property.SetValue(o, value, null) - Some o - with - | :? System.ArgumentException -> None - | :? System.NullReferenceException -> None - | None -> None + | Some property when property.IsMutable -> + property.SetValue o value + true + | _ -> false - /// Gets property value as option using reflection let tryGetPropertyValue (o:obj) (propName:string) = try match tryGetPropertyInfo o propName with - | Some v -> Some (v.GetValue(o,null)) + | Some v -> Some (v.GetValue(o)) | None -> None with | :? System.Reflection.TargetInvocationException -> None | :? System.NullReferenceException -> None + /// Gets property value as 'a option using reflection. Cast to 'a let tryGetPropertyValueAs<'a> (o:obj) (propName:string) = try - match tryGetPropertyInfo o propName with - | Some v -> Some (v.GetValue(o,null) :?> 'a) - | None -> None + tryGetPropertyValue o propName + |> Option.map (fun v -> v :?> 'a) + with | :? System.Reflection.TargetInvocationException -> None | :? System.NullReferenceException -> None - /// Updates property value by given function - let tryUpdatePropertyValueFromName (o:obj) (propName:string) (f: 'a -> 'a) = - let v = optBuildApply f (tryGetPropertyValueAs<'a> o propName) - trySetPropertyValue o propName v - //o - - /// Updates property value by given function - let tryUpdatePropertyValue (o:obj) (expr : Microsoft.FSharp.Quotations.Expr) (f: 'a -> 'a) = - let propName = tryGetPropertyName expr - let g = (tryGetPropertyValueAs<'a> o propName.Value) - let v = optBuildApply f g - trySetPropertyValue o propName.Value v - //o + let removeProperty (o:obj) (propName:string) = - let updatePropertyValueAndIgnore (o:obj) (expr : Microsoft.FSharp.Quotations.Expr) (f: 'a -> 'a) = - tryUpdatePropertyValue o expr f |> ignore - - - /// Removes property - let removeProperty (o:obj) (propName:string) = match tryGetPropertyInfo o propName with - | Some property -> - try - property.SetValue(o, null, null) - true - with - | :? System.ArgumentException -> false - | :? System.NullReferenceException -> false - | None -> false - + | Some property when property.IsMutable -> + property.RemoveValue(o) + true + | _ -> false diff --git a/tests/CSharpTests/CSharpTests.csproj b/tests/CSharpTests/CSharpTests.csproj index 430a89f..faf0826 100644 --- a/tests/CSharpTests/CSharpTests.csproj +++ b/tests/CSharpTests/CSharpTests.csproj @@ -19,6 +19,7 @@ + diff --git a/tests/UnitTests/UnitTests.fsproj b/tests/DynamicObject.Immutable.Tests/DynamicObject.Immutable.Tests.fsproj similarity index 92% rename from tests/UnitTests/UnitTests.fsproj rename to tests/DynamicObject.Immutable.Tests/DynamicObject.Immutable.Tests.fsproj index 52aeff6..d14214a 100644 --- a/tests/UnitTests/UnitTests.fsproj +++ b/tests/DynamicObject.Immutable.Tests/DynamicObject.Immutable.Tests.fsproj @@ -9,7 +9,6 @@ - @@ -28,6 +27,7 @@ + diff --git a/tests/UnitTests/ImmTests.fs b/tests/DynamicObject.Immutable.Tests/ImmTests.fs similarity index 100% rename from tests/UnitTests/ImmTests.fs rename to tests/DynamicObject.Immutable.Tests/ImmTests.fs diff --git a/tests/UnitTests/OperatorsTests.fs b/tests/DynamicObject.Immutable.Tests/OperatorsTests.fs similarity index 100% rename from tests/UnitTests/OperatorsTests.fs rename to tests/DynamicObject.Immutable.Tests/OperatorsTests.fs diff --git a/tests/UnitTests/Program.fs b/tests/DynamicObject.Immutable.Tests/Program.fs similarity index 100% rename from tests/UnitTests/Program.fs rename to tests/DynamicObject.Immutable.Tests/Program.fs diff --git a/tests/DynamicObject.Tests/DynamicObj.fs b/tests/DynamicObject.Tests/DynamicObj.fs new file mode 100644 index 0000000..7c180ab --- /dev/null +++ b/tests/DynamicObject.Tests/DynamicObj.fs @@ -0,0 +1,277 @@ +module DynamicObj.Tests + +open System +open Fable.Pyxpecto +open DynamicObj + + +let tests_set = testList "Set" [ + + testCase "Same String" <| fun _ -> + let a = DynamicObj () + a.SetValue("aaa", 5) + let b = DynamicObj () + b.SetValue("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.SetValue("aaa", 1212) + let b = DynamicObj () + b.SetValue("aaa", 5) + Expect.notEqual a b "Values should not be equal" + + testCase "String only on one" <| fun _ -> + let a = DynamicObj () + let b = DynamicObj () + b.SetValue("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'.SetValue("quack!", [1; 2; 3]) + b'.SetValue("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'.SetValue("quack!", [1; 2; 3]) + b'.SetValue("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'.SetValue("quack!", [1; 2; 3]) + b'.SetValue("quack!", [1; 2; 3]) + + a.SetValue("aaa", a') + b.SetValue("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'.SetValue("quack!", [1; 2; 3]) + b'.SetValue("quack!", [1; 2; 3]) + + a.SetValue("aaa", a') + b.SetValue("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_remove = testList "Remove" [ + + testCase "Remove" <| fun _ -> + let a = DynamicObj () + let b = DynamicObj () + + a.SetValue("quack!", "hello") + + a.Remove "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.SetValue("quack!", "hello") + b.SetValue("quack!", "hello") + + a.Remove "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.SetValue("quack!", "hello") + b.SetValue("quack!", "hello") + + a.Remove "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'.SetValue("quack!", [1; 2; 3]) + b'.SetValue("quack!", [1; 2; 3]) + + a.SetValue("aaa", a') + a.Remove "quack!" |> ignore + b.SetValue("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'.SetValue("quack!", [1; 2; 3]) + b'.SetValue("quack!", [1; 2; 3]) + + a.SetValue("aaa", a') + a'.Remove "quack!" |> ignore + b.SetValue("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'.SetValue("quack!", [1; 2; 3]) + b'.SetValue("quack!", [1; 2; 3]) + + a.SetValue("aaa", a') + a.Remove "quack!" |> ignore + b.SetValue("aaa", b') + b.Remove "quack!" |> ignore + + Expect.equal a b "Values should be equal" + Expect.equal (a.GetHashCode()) (b.GetHashCode()) "Hash codes should be equal" + +] + + +let tests_formatString = testList "FormatString" [ + + testCase "Format string 1" <| fun _ -> + let foo = DynamicObj() + let list = [1;2;3;4] + foo.SetValue("bar", list) + let expected = $"?bar: {list}" + Expect.equal (foo |> DynObj.format) expected "Format string 1 failed" + + testCase "Format string 2" <| fun _ -> + let foo = DynamicObj() + let corgi = "corgi" + foo.SetValue("corgi", corgi) + let inner = DynamicObj() + let baz = "baz" + inner.SetValue("bar", baz) + foo.SetValue("foo", inner) + let expected = $"""?corgi: {corgi}{Environment.NewLine}?foo:{Environment.NewLine} ?bar: {baz}""" + Expect.equal (foo |> DynObj.format) expected "Format string 2 failed" + +] + + +let tests_combine = testList "Combine" [ + + testCase "Combine flat DOs" <| fun _ -> + let target = DynamicObj() + + target.SetValue("target-unique", [42]) + target.SetValue("will-be-overridden", "WAS_NOT_OVERRIDDEN!") + + let source = DynamicObj() + + source.SetValue("source-unique", [42; 32]) + source.SetValue("will-be-overridden", "WAS_OVERRIDDEN =)") + + let combined = DynObj.combine target source + + let expected = DynamicObj() + + expected.SetValue("target-unique", [42]) + expected.SetValue("source-unique", [42; 32]) + expected.SetValue("will-be-overridden", "WAS_OVERRIDDEN =)") + + Expect.equal expected combined "Combine flat DOs failed" + + testCase "Combine nested DOs" <| fun _ -> + let target = DynamicObj() + + target.SetValue("target-unique", 1337) + target.SetValue("will-be-overridden", -42) + let something2BeCombined = DynamicObj() + something2BeCombined.SetValue("inner","I Am") + let something2BeOverriden = DynamicObj() + something2BeOverriden.SetValue("inner","NOT_OVERRIDDEN") + target.SetValue("nested-will-be-combined", something2BeCombined) + target.SetValue("nested-will-be-overridden", something2BeOverriden) + + let source = DynamicObj() + + source.SetValue("source-unique", 69) + source.SetValue("will-be-overridden", "WAS_OVERRIDDEN") + let alsoSomething2BeCombined = DynamicObj() + alsoSomething2BeCombined.SetValue("inner_combined","Complete") + source.SetValue("nested-will-be-combined", alsoSomething2BeCombined) + source.SetValue("nested-will-be-overridden", "WAS_OVERRIDDEN") + + let combined = DynObj.combine target source + + let expected = DynamicObj() + + expected.SetValue("source-unique", 69) + expected.SetValue("target-unique", 1337) + expected.SetValue("will-be-overridden", "WAS_OVERRIDDEN") + expected.SetValue("nested-will-be-overridden", "WAS_OVERRIDDEN") + expected.SetValue("nested-will-be-combined", + let inner = DynamicObj() + inner.SetValue("inner","I Am") + inner.SetValue("inner_combined","Complete") + inner + ) + + Expect.equal expected combined "Combine nested DOs failed" +] + +let tests_print = testList "Print" [ + + testCase "Test Print For Issue 14" <| fun _ -> + let outer = DynamicObj() + let inner = DynamicObj() + inner.SetValue("Level", "Information") + inner.SetValue("MessageTemplate","{Method} Request at {Path}") + outer.SetValue("serilog", inner) + + let print = + try + outer |> DynObj.print + true + with + | e -> false + + Expect.isTrue print "Print failed for issue 14" +] + +let main = testList "DynamicObj" [ + tests_set + tests_remove + tests_formatString + tests_combine +] \ No newline at end of file diff --git a/tests/DynamicObject.Tests/DynamicObject.Tests.fsproj b/tests/DynamicObject.Tests/DynamicObject.Tests.fsproj new file mode 100644 index 0000000..33d1bbd --- /dev/null +++ b/tests/DynamicObject.Tests/DynamicObject.Tests.fsproj @@ -0,0 +1,35 @@ + + + + net6.0 + + false + false + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/tests/DynamicObject.Tests/Inheritance.fs b/tests/DynamicObject.Tests/Inheritance.fs new file mode 100644 index 0000000..8b509d4 --- /dev/null +++ b/tests/DynamicObject.Tests/Inheritance.fs @@ -0,0 +1,137 @@ +module Inheritance.Tests + +open System +open Fable.Pyxpecto +open DynamicObj +open Fable.Core + +[] +type Person(name : string) = + + inherit DynamicObj() + + let mutable name = name + + member this.Name + with get() = name + and set(value) = name <- value + +[] +type PersonImmutable(name : string) = + + inherit DynamicObj() + + member this.Name + with get() = name + +let tests_set = testList "Set" [ + + testCase "Static Property" <| fun _ -> + let p = Person("John") + p.SetValue("Name", "Jane") + Expect.equal p.Name "Jane" "Static property should be set" + Expect.equal (p.TryGetValue("Name")) (Some "Jane") "Static property should be retreivable dynamically" + + testCase "Static Immutable Property" <| fun _ -> + let p = PersonImmutable("John") + let f = fun () -> p.SetValue("Name", "Jane") + Expect.throws f "Cannot set static property" + + testCase "Dynamic Property" <| fun _ -> + let p = Person("John") + p.SetValue("Age", 42) + Expect.equal (p.TryGetValue("Age")) (Some 42) "Dynamic property should be set" + Expect.equal (p.TryGetValue("Name")) (Some "John") "Static property should be retreivable dynamically" + + testCase "Dynamic Property Equality" <| fun _ -> + let p1 = Person("John") + let p2 = Person("John") + + p1.SetValue("Age", 42) + p2.SetValue("Age", 42) + + Expect.equal p1 p2 "Values should be equal" + Expect.equal (p1.GetHashCode()) (p2.GetHashCode()) "Hash codes should be equal" + + testCase "Dynamic Property Only on one" <| fun _ -> + let p1 = Person("John") + let p2 = Person("John") + + p1.SetValue("Age", 42) + + Expect.notEqual p1 p2 "Values should not be equal" + Expect.notEqual p2 p1 "Values should not be equal (Reversed equality)" + ] + +let tests_remove = testList "Remove" [ + + testCase "Remove Static" <| fun _ -> + let p = Person("John") + + p.Remove("Name") |> ignore + + Expect.equal p.Name null "Static property should " + + testCase "Remove Dynamic" <| fun _ -> + let p = Person("John") + + p.SetValue("Age", 42) + + p.Remove "Age" |> ignore + + let r = p.TryGetValue("Age") + + Expect.isNone r "Dynamic property should be removed" + + testCase "Remove only on one" <| fun _ -> + let p1 = Person("John") + let p2 = Person("John") + + p1.SetValue("Age", 42) + p2.SetValue("Age", 42) + + p1.Remove "Age" |> ignore + + Expect.notEqual p1 p2 "Values should be unequal" + Expect.notEqual (p1.GetHashCode()) (p2.GetHashCode()) "Hash codes should be unequal" + +] + + +let tests_formatString = testList "FormatString" [ + + testCase "Format string 1" <| fun _ -> + + let name = "John" + let age = 20 + let p = Person("John") + p.SetValue("age", age) + let expected = $"Name: {name}{System.Environment.NewLine}?age: {age}" + Expect.equal (p |> DynObj.format) expected "Format string 1 failed" +] + + +let tests_print = testList "Print" [ + + testCase "Test Print For Issue 14" <| fun _ -> + let outer = DynamicObj() + let inner = DynamicObj() + inner.SetValue("Level", "Information") + inner.SetValue("MessageTemplate","{Method} Request at {Path}") + outer.SetValue("serilog", inner) + + let print = + try + outer |> DynObj.print + true + with + | e -> false + + Expect.isTrue print "Print failed for issue 14" +] + +let main = testList "Inheritance" [ + tests_set + tests_remove + tests_formatString +] \ No newline at end of file diff --git a/tests/DynamicObject.Tests/Main.fs b/tests/DynamicObject.Tests/Main.fs new file mode 100644 index 0000000..50759b3 --- /dev/null +++ b/tests/DynamicObject.Tests/Main.fs @@ -0,0 +1,12 @@ +module Main.Tests + +open Fable.Pyxpecto + +let all = testSequenced <| testList "DynamicObj" [ + ReflectionUtils.Tests.main + DynamicObj.Tests.main + Inheritance.Tests.main +] + +[] +let main argv = Pyxpecto.runTests [||] all \ No newline at end of file diff --git a/tests/DynamicObject.Tests/ReflectionUtils.fs b/tests/DynamicObject.Tests/ReflectionUtils.fs new file mode 100644 index 0000000..6667721 --- /dev/null +++ b/tests/DynamicObject.Tests/ReflectionUtils.fs @@ -0,0 +1,111 @@ +module ReflectionUtils.Tests + +open System +open Fable.Pyxpecto +open DynamicObj +open Fable.Core + +[] +type TestObject(id : string, name : string) = + + let id = id + let mutable name = name + + member this.Id + with get() = id + + member this.Name + with get() = name + and set(value) = name <- value + + +let tests_PropertyHelper = testList "PropertyHelper" [ + testCase "getStaticProperties" <| fun _ -> + let p = TestObject("1", "test") + let helpers = ReflectionUtils.getStaticProperties p + Expect.hasLength helpers 2 "Should have 2 properties" + let idOption = Array.tryFind (fun h -> h.Name = "Id") helpers + let id = Expect.wantSome idOption "Should have Id property" + Expect.equal id.IsStatic true "Id should be static" + Expect.equal id.IsDynamic false "Id should not be dynamic" + Expect.equal id.IsMutable false "Id should not be mutable" + Expect.equal id.IsImmutable true "Id should be immutable" + let nameOption = Array.tryFind (fun h -> h.Name = "Name") helpers + let name = Expect.wantSome nameOption "Should have Name property" + Expect.equal name.IsStatic true "Name should be static" + Expect.equal name.IsDynamic false "Name should not be dynamic" + Expect.equal name.IsMutable true "Name should be mutable" + Expect.equal name.IsImmutable false "Name should not be immutable" + + testCase "TryGetPropertyInfo" <| fun _ -> + let p = TestObject("1", "test") + let idOption = ReflectionUtils.tryGetPropertyInfo p "Id" + let id = Expect.wantSome idOption "Should have immutable property" + Expect.equal id.Name "Id" "Should have correct property" + Expect.equal id.IsStatic true "Id should be static" + Expect.equal id.IsDynamic false "Id should not be dynamic" + Expect.equal id.IsMutable false "Id should not be mutable" + Expect.equal id.IsImmutable true "Id should be immutable" + + let nameOption = ReflectionUtils.tryGetPropertyInfo p "Name" + let name = Expect.wantSome nameOption "Should have mutable property" + Expect.equal name.Name "Name" "Should have correct property" + Expect.equal name.IsStatic true "Name should be static" + Expect.equal name.IsDynamic false "Name should not be dynamic" + Expect.equal name.IsMutable true "Name should be mutable" + Expect.equal name.IsImmutable false "Name should not be immutable" + + let nonExistingOption = ReflectionUtils.tryGetPropertyInfo p "NonExisting" + Expect.isNone nonExistingOption "Should not have property" +] + + +let tests_TryGetPropertyValue = testList "TryGetPropertyValue" [ + testCase "existing" <| fun _ -> + let p = TestObject("1", "test") + let nameOption = ReflectionUtils.tryGetPropertyValue p "Name" + let name = Expect.wantSome nameOption "Should have mutable value" + Expect.equal name "test" "Should have correct mutable value" + + let idOption = ReflectionUtils.tryGetPropertyValue p "Id" + let id = Expect.wantSome idOption "Should have immutable value" + Expect.equal id "1" "Should have correct immutable value" + + testCase "non-existing" <| fun _ -> + let p = TestObject("1", "test") + let option = ReflectionUtils.tryGetPropertyValue p "NonExisting" + Expect.equal option None "Should not have value" +] + +let tests_TrySetPropertyValue = testList "TrySetPropertyValue" [ + testCase "mutable" <| fun _ -> + let p = TestObject("1", "test") + let wasSet = ReflectionUtils.trySetPropertyValue p "Name" "newName" + Expect.isTrue wasSet "Should have set value" + let nameOption = ReflectionUtils.tryGetPropertyValue p "Name" + let name = Expect.wantSome nameOption "Should have mutable value" + Expect.equal name "newName" "Should have correct mutable value" + + testCase "immutable" <| fun _ -> + let p = TestObject("1", "test") + let wasSet = ReflectionUtils.trySetPropertyValue p "Id" "newId" + Expect.isFalse wasSet "Should not have set value" + + let idOption = ReflectionUtils.tryGetPropertyValue p "Id" + let id = Expect.wantSome idOption "Should have immutable value" + Expect.equal id "1" "Should have correct immutable value" + + testCase "non-existing" <| fun _ -> + let p = TestObject("1", "test") + let wasSet = ReflectionUtils.trySetPropertyValue p "address" "newAddress" + Expect.isFalse wasSet "Should not have set value" + + let addressOption = ReflectionUtils.tryGetPropertyValue p "address" + Expect.isNone addressOption "Should not have value" +] + +let main = testList "ReflectionUtils" [ + tests_PropertyHelper + tests_TryGetPropertyValue + tests_TrySetPropertyValue +] \ No newline at end of file diff --git a/tests/UnitTests/Tests.fs b/tests/UnitTests/Tests.fs deleted file mode 100644 index cdcb6e7..0000000 --- a/tests/UnitTests/Tests.fs +++ /dev/null @@ -1,211 +0,0 @@ -module Tests.EqualityHashcode - -open System -open Xunit -open DynamicObj - -[] -let ``Equality test 1`` () = - let a = DynamicObj () - a.SetValue("aaa", 5) - let b = DynamicObj () - b.SetValue("aaa", 5) - Assert.Equal(a, b) - Assert.Equal(a.GetHashCode(), b.GetHashCode()) - -[] -let ``Equality test 2`` () = - let a = DynamicObj () - a.SetValue("aaa", 1212) - let b = DynamicObj () - b.SetValue("aaa", 5) - Assert.NotEqual(a, b) - - -[] -let ``Equality test 3`` () = - let a = DynamicObj () - let b = DynamicObj () - - let a' = DynamicObj () - let b' = DynamicObj () - a'.SetValue("quack!", [1; 2; 3]) - b'.SetValue("quack!", [1; 2; 3]) - - a.SetValue("aaa", a') - b.SetValue("aaa", b') - Assert.Equal(a', b') - Assert.Equal(a, b) - Assert.Equal(a.GetHashCode(), b.GetHashCode()) - Assert.Equal(a'.GetHashCode(), b'.GetHashCode()) - -[] -let ``Equality test 4`` () = - let a = DynamicObj () - let b = DynamicObj () - - let a' = DynamicObj () - let b' = DynamicObj () - a'.SetValue("quack!", [1; 2; 3]) - b'.SetValue("quack!", [1; 2; 3]) - - a.SetValue("aaa", a') - b.SetValue("aaa1", b') - Assert.Equal(a', b') - Assert.NotEqual(a, b) - Assert.Equal(a'.GetHashCode(), b'.GetHashCode()) - - -[] -let ``Equality test 5`` () = - let a = DynamicObj () - a.SetValue("bvbb", 5) - let b = DynamicObj () - b.SetValue("aaa", 5) - - a.SetValue("aaa", 5) - a.Remove "bvbb" |> ignore - - Assert.Equal(a, b) - Assert.Equal(a.GetHashCode(), b.GetHashCode()) - -[] -let ``Equality test 6`` () = - //nesting - let a = DynamicObj () - let b = DynamicObj () - b.SetValue("a", 5) - a.SetValue("inner",b) - let c = DynamicObj () - let d = DynamicObj () - d.SetValue("a", 5) - c.SetValue("inner",d) - - Assert.Equal(a, c) - Assert.Equal(a.GetHashCode(), c.GetHashCode()) - -// different objects do NOT have to have different hash -// codes, so here we rely on our luckiness. - -[] -let ``Hashcode inequality 1`` () = - let a' = DynamicObj () - let b' = DynamicObj () - a'.SetValue("quack!", [1; 2; 3]) - b'.SetValue("quack!", [1; 2; 3; 4; 34]) - Assert.NotEqual(a'.GetHashCode(), b'.GetHashCode()) - - -[] -let ``Hashcode inequality 2`` () = - let a' = DynamicObj () - let b' = DynamicObj () - a'.SetValue("quack!", [1; 2; 3]) - b'.SetValue("quack!1", [1; 2; 3]) - Assert.NotEqual(a'.GetHashCode(), b'.GetHashCode()) - -[] -let ``Format string 1`` () = - - let foo = DynamicObj() - foo?bar <- [1;2;3;4] - - let expected = "?bar: [1; 2; 3; ... ]" - - Assert.Equal(expected, (foo |> DynObj.format)) - -[] -let ``Format string 2`` () = - - // nested - let foo = DynamicObj() - foo?corgi <- "corgi" - let inner = DynamicObj() - inner?bar <- "baz" - foo?foo <- inner - - let expected = $"""?corgi: corgi{Environment.NewLine}?foo:{Environment.NewLine} ?bar: baz""" - - Assert.Equal(expected, (foo |> DynObj.format)) - - - -[] -let ``combine flat DOs``() = - let target = DynamicObj() - - target.SetValue("target-unique", [42]) - target.SetValue("will-be-overridden", "WAS_NOT_OVERRIDDEN!") - - let source = DynamicObj() - - source.SetValue("source-unique", [42; 32]) - source.SetValue("will-be-overridden", "WAS_OVERRIDDEN =)") - - let combined = DynObj.combine target source - - let expected = DynamicObj() - - expected.SetValue("target-unique", [42]) - expected.SetValue("source-unique", [42; 32]) - expected.SetValue("will-be-overridden", "WAS_OVERRIDDEN =)") - - Assert.Equal(expected, combined) - -[] -let ``combine nested DOs``() = - - let target = DynamicObj() - - target.SetValue("target-unique", 1337) - target.SetValue("will-be-overridden", -42) - let something2BeCombined = DynamicObj() - something2BeCombined.SetValue("inner","I Am") - let something2BeOverriden = DynamicObj() - something2BeOverriden.SetValue("inner","NOT_OVERRIDDEN") - target.SetValue("nested-will-be-combined", something2BeCombined) - target.SetValue("nested-will-be-overridden", something2BeOverriden) - - let source = DynamicObj() - - source.SetValue("source-unique", 69) - source.SetValue("will-be-overridden", "WAS_OVERRIDDEN") - let alsoSomething2BeCombined = DynamicObj() - alsoSomething2BeCombined.SetValue("inner_combined","Complete") - source.SetValue("nested-will-be-combined", alsoSomething2BeCombined) - source.SetValue("nested-will-be-overridden", "WAS_OVERRIDDEN") - - let combined = DynObj.combine target source - - let expected = DynamicObj() - - expected.SetValue("source-unique", 69) - expected.SetValue("target-unique", 1337) - expected.SetValue("will-be-overridden", "WAS_OVERRIDDEN") - expected.SetValue("nested-will-be-overridden", "WAS_OVERRIDDEN") - expected.SetValue("nested-will-be-combined", - let inner = DynamicObj() - inner.SetValue("inner","I Am") - inner.SetValue("inner_combined","Complete") - inner - ) - - Assert.Equal(expected, combined) - -[] -let ``test print for issue 14``() = - // https://github.com/CSBiology/DynamicObj/issues/14 - let outer = DynamicObj() - let inner = DynamicObj() - inner.SetValue("Level", "Information") - inner.SetValue("MessageTemplate","{Method} Request at {Path}") - outer.SetValue("serilog", inner) - - let print = - try - outer |> DynObj.print - true - with - | e -> false - - Assert.True(print) \ No newline at end of file diff --git a/tests/playground.fsx b/tests/playground.fsx new file mode 100644 index 0000000..003db41 --- /dev/null +++ b/tests/playground.fsx @@ -0,0 +1,52 @@ +#r @"C:\Users\HLWei\source\repos\other\DynamicObj\tests\DynamicObject.Tests\bin\Release\net6.0\Fable.Pyxpecto.dll" +#r @"C:\Users\HLWei\source\repos\other\DynamicObj\tests\DynamicObject.Tests\bin\Release\net6.0\Fable.Core.dll" +#r @"C:\Users\HLWei\source\repos\other\DynamicObj\tests\DynamicObject.Tests\bin\Release\net6.0\Fable.Python.dll" +#r @"C:\Users\HLWei\source\repos\other\DynamicObj\tests\DynamicObject.Tests\bin\Release\net6.0\Microsoft.TestPlatform.Utilities.dll" +#r @"C:\Users\HLWei\source\repos\other\DynamicObj\tests\DynamicObject.Tests\bin\Release\net6.0\Microsoft.TestPlatform.CommunicationUtilities.dll" +#r @"C:\Users\HLWei\source\repos\other\DynamicObj\tests\DynamicObject.Tests\bin\Release\net6.0\Newtonsoft.Json.dll" +#r @"C:\Users\HLWei\source\repos\other\DynamicObj\tests\DynamicObject.Tests\bin\Release\net6.0\Microsoft.VisualStudio.CodeCoverage.Shim.dll" +#r @"C:\Users\HLWei\source\repos\other\DynamicObj\tests\DynamicObject.Tests\bin\Release\net6.0\DynamicObject.Tests.dll" +#r @"C:\Users\HLWei\source\repos\other\DynamicObj\tests\DynamicObject.Tests\bin\Release\net6.0\DynamicObj.dll" + +open DynamicObj + +//let a = DynamicObj () + +//a.SetValue("aaa", 5) + +//a.GetHashCode() + +//ReflectionUtils.getStaticProperties (a) + +//let o = a + +//let t = o.GetType() +//[| +// for propInfo in t.GetProperties() -> propInfo +// for i in t.GetInterfaces() do yield! i.GetProperties() +//|] +//|> Array.map PropertyHelper.fromPropertyInfo + + + + + + +type Person(name : string) = + + inherit DynamicObj() + + let mutable name = name + + member this.Name + with get() = name + //and set(value) = name <- value + +let p = Person("John") +p.SetValue("Name", "Jane") + +p.TryGetValue("Name") + +ReflectionUtils.tryGetPropertyValue p "Name" + +ReflectionUtils.tryGetPropertyInfo p "Name" \ No newline at end of file