diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 36283a2..db73ba2 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -9,7 +9,7 @@ ] }, "fable": { - "version": "4.0.0-snake-island-alpha-024", + "version": "4.0.0-snake-island-alpha-025", "commands": [ "fable" ] diff --git a/Build.fs b/Build.fs index e48e6c7..305139e 100644 --- a/Build.fs +++ b/Build.fs @@ -15,8 +15,6 @@ let cliPath = Path.getFullName "../Fable/src/Fable.Cli" Target.create "Clean" (fun _ -> Shell.cleanDir buildPath - // run dotnet "fable clean --yes" buildPath // Delete *.py files created by Fable - // run dotnet $"run -c Release -p {cliPath} -- clean --yes --lang Python " buildPath ) Target.create "Build" (fun _ -> diff --git a/examples/giraffe/Build.fs b/examples/giraffe/Build.fs new file mode 100644 index 0000000..31ccc58 --- /dev/null +++ b/examples/giraffe/Build.fs @@ -0,0 +1,64 @@ +open Fake.Core +open Fake.IO + +open Helpers + +initializeContext() + +let buildPath = Path.getFullName "build" +let srcPath = Path.getFullName "src" +let deployPath = Path.getFullName "deploy" +let testsPath = Path.getFullName "test" + +// Until Fable (beyond) is released, we need to compile with locally installed Fable branch. +let cliPath = Path.getFullName "../Fable/src/Fable.Cli" + +Target.create "Clean" (fun _ -> + Shell.cleanDir buildPath +) + +Target.create "Build" (fun _ -> + Shell.mkdir buildPath + run dotnet $"fable --exclude Fable.Core --lang Python --outDir {buildPath}" srcPath +) + +Target.create "Run" (fun _ -> + run dotnet "build" srcPath +) + +Target.create "Test" (fun _ -> + run dotnet "build" testsPath + [ "native", dotnet "run" testsPath + "python", dotnet $"fable --lang Python --outDir {buildPath}/tests" testsPath + ] + |> runParallel + run poetry $"run python -m pytest {buildPath}/tests" "" +) + +Target.create "Pack" (fun _ -> + run dotnet "pack -c Release" srcPath +) + +Target.create "Format" (fun _ -> + run dotnet "fantomas . -r" srcPath + run dotnet "fantomas . -r" testsPath +) + +open Fake.Core.TargetOperators + +let dependencies = [ + "Clean" + ==> "Build" + + "Clean" + ==> "Run" + + "Build" + ==> "Test" + + "Build" + ==> "Pack" +] + +[] +let main args = runOrDefault args \ No newline at end of file diff --git a/examples/giraffe/Build.fsproj b/examples/giraffe/Build.fsproj new file mode 100644 index 0000000..5e2e995 --- /dev/null +++ b/examples/giraffe/Build.fsproj @@ -0,0 +1,15 @@ + + + Exe + net6.0 + + + + + + + + + + + \ No newline at end of file diff --git a/examples/giraffe/Helpers.fs b/examples/giraffe/Helpers.fs index cdbf15a..5ef320e 100644 --- a/examples/giraffe/Helpers.fs +++ b/examples/giraffe/Helpers.fs @@ -1,32 +1,109 @@ -namespace Giraffe - -[] -module Helpers = - open System - open System.IO - - /// - /// Checks if an object is not null. - /// - /// The object to validate against `null`. - /// Returns true if the object is not null otherwise false. - let inline isNotNull x = not (isNull x) - - /// - /// Converts a string into a string option where null or an empty string will be converted to None and everything else to Some string. - /// - /// The string value to be converted into an option of string. - /// Returns None if the string was null or empty otherwise Some string. - let inline strOption (str : string) = - if String.IsNullOrEmpty str then None else Some str - - /// - /// Reads a file asynchronously from the file system. - /// - /// The absolute path of the file. - /// Returns the string contents of the file wrapped in a Task. - // let readFileAsStringAsync (filePath : string) = - // task { - // use reader = new StreamReader(filePath) - // return! reader.ReadToEndAsync() - // } \ No newline at end of file +module Helpers + +open Fake.Core + +let initializeContext () = + let execContext = Context.FakeExecutionContext.Create false "build.fsx" [ ] + Context.setExecutionContext (Context.RuntimeContext.Fake execContext) + +module Proc = + module Parallel = + open System + + let locker = obj() + + let colors = + [| ConsoleColor.Blue + ConsoleColor.Yellow + ConsoleColor.Magenta + ConsoleColor.Cyan + ConsoleColor.DarkBlue + ConsoleColor.DarkYellow + ConsoleColor.DarkMagenta + ConsoleColor.DarkCyan |] + + 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 createProcess exe arg dir = + CreateProcess.fromRawCommandLine exe arg + |> CreateProcess.withWorkingDirectory dir + |> CreateProcess.ensureExitCode + +let dotnet = createProcess "dotnet" + + +let pytest = createProcess "pytest" +let poetry = createProcess "poetry" + +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 run proc arg dir = + proc arg dir + |> Proc.run + |> ignore + +let runParallel processes = + processes + |> Proc.Parallel.run + |> ignore + +let runOrDefault args = + try + match args with + | [| target |] -> Target.runOrDefault target + | _ -> Target.runOrDefault "Run" + 0 + with e -> + printfn "%A" e + 1 diff --git a/examples/giraffe/Program.fs b/examples/giraffe/Program.fs deleted file mode 100644 index c33710e..0000000 --- a/examples/giraffe/Program.fs +++ /dev/null @@ -1,8 +0,0 @@ -module Program - -open Giraffe - -let webApp = - GET |> HttpHandler.choose [ route "/ping" |> HttpHandler.text "Hello World!" ] - -let app = Middleware.useGiraffe webApp diff --git a/examples/giraffe/giraffe.sln b/examples/giraffe/giraffe.sln new file mode 100644 index 0000000..4dc0768 --- /dev/null +++ b/examples/giraffe/giraffe.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Giraffe.Python", "src\Giraffe.Python.fsproj", "{6E973C60-9714-4F0D-A65B-60AB3DF3FADC}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Giraffe.Tests", "test\Giraffe.Tests.fsproj", "{D56D8E48-E02C-479D-A1F8-57352E3BD8AB}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6E973C60-9714-4F0D-A65B-60AB3DF3FADC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6E973C60-9714-4F0D-A65B-60AB3DF3FADC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6E973C60-9714-4F0D-A65B-60AB3DF3FADC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6E973C60-9714-4F0D-A65B-60AB3DF3FADC}.Release|Any CPU.Build.0 = Release|Any CPU + {D56D8E48-E02C-479D-A1F8-57352E3BD8AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D56D8E48-E02C-479D-A1F8-57352E3BD8AB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D56D8E48-E02C-479D-A1F8-57352E3BD8AB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D56D8E48-E02C-479D-A1F8-57352E3BD8AB}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/examples/giraffe/poetry.lock b/examples/giraffe/poetry.lock index 9740d8d..ff2b99b 100644 --- a/examples/giraffe/poetry.lock +++ b/examples/giraffe/poetry.lock @@ -15,6 +15,20 @@ doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] trio = ["trio (>=0.16)"] +[[package]] +name = "attrs" +version = "22.1.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] + [[package]] name = "black" version = "22.8.0" @@ -72,6 +86,14 @@ category = "main" optional = false python-versions = ">=3.5" +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "mypy-extensions" version = "0.4.3" @@ -80,6 +102,17 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + [[package]] name = "pathspec" version = "0.10.1" @@ -100,6 +133,71 @@ python-versions = ">=3.7" docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pyparsing" +version = "3.0.9" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +category = "dev" +optional = false +python-versions = ">=3.6.8" + +[package.extras] +diagrams = ["railroad-diagrams", "jinja2"] + +[[package]] +name = "pytest" +version = "7.1.3" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +tomli = ">=1.0.0" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.19.0" +description = "Pytest support for asyncio" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +pytest = ">=6.1.0" + +[package.extras] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] + [[package]] name = "sniffio" version = "1.3.0" @@ -157,43 +255,23 @@ standard = ["colorama (>=0.4)", "httptools (>=0.4.0)", "python-dotenv (>=0.13)", [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "940e503b77a307258d907129270083a1676a958d0b8244484cad69b15f10e671" +content-hash = "70270433366ed7647c74031e38ce343dc9dd3f1b48244a9ac2fc332d01d5d080" [metadata.files] anyio = [ {file = "anyio-3.6.1-py3-none-any.whl", hash = "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be"}, {file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"}, ] -black = [ - {file = "black-22.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ce957f1d6b78a8a231b18e0dd2d94a33d2ba738cd88a7fe64f53f659eea49fdd"}, - {file = "black-22.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5107ea36b2b61917956d018bd25129baf9ad1125e39324a9b18248d362156a27"}, - {file = "black-22.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8166b7bfe5dcb56d325385bd1d1e0f635f24aae14b3ae437102dedc0c186747"}, - {file = "black-22.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd82842bb272297503cbec1a2600b6bfb338dae017186f8f215c8958f8acf869"}, - {file = "black-22.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d839150f61d09e7217f52917259831fe2b689f5c8e5e32611736351b89bb2a90"}, - {file = "black-22.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a05da0430bd5ced89176db098567973be52ce175a55677436a271102d7eaa3fe"}, - {file = "black-22.8.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a098a69a02596e1f2a58a2a1c8d5a05d5a74461af552b371e82f9fa4ada8342"}, - {file = "black-22.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5594efbdc35426e35a7defa1ea1a1cb97c7dbd34c0e49af7fb593a36bd45edab"}, - {file = "black-22.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a983526af1bea1e4cf6768e649990f28ee4f4137266921c2c3cee8116ae42ec3"}, - {file = "black-22.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b2c25f8dea5e8444bdc6788a2f543e1fb01494e144480bc17f806178378005e"}, - {file = "black-22.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:78dd85caaab7c3153054756b9fe8c611efa63d9e7aecfa33e533060cb14b6d16"}, - {file = "black-22.8.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:cea1b2542d4e2c02c332e83150e41e3ca80dc0fb8de20df3c5e98e242156222c"}, - {file = "black-22.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5b879eb439094751185d1cfdca43023bc6786bd3c60372462b6f051efa6281a5"}, - {file = "black-22.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a12e4e1353819af41df998b02c6742643cfef58282915f781d0e4dd7a200411"}, - {file = "black-22.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3a73f66b6d5ba7288cd5d6dad9b4c9b43f4e8a4b789a94bf5abfb878c663eb3"}, - {file = "black-22.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:e981e20ec152dfb3e77418fb616077937378b322d7b26aa1ff87717fb18b4875"}, - {file = "black-22.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8ce13ffed7e66dda0da3e0b2eb1bdfc83f5812f66e09aca2b0978593ed636b6c"}, - {file = "black-22.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:32a4b17f644fc288c6ee2bafdf5e3b045f4eff84693ac069d87b1a347d861497"}, - {file = "black-22.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ad827325a3a634bae88ae7747db1a395d5ee02cf05d9aa7a9bd77dfb10e940c"}, - {file = "black-22.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53198e28a1fb865e9fe97f88220da2e44df6da82b18833b588b1883b16bb5d41"}, - {file = "black-22.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:bc4d4123830a2d190e9cc42a2e43570f82ace35c3aeb26a512a2102bce5af7ec"}, - {file = "black-22.8.0-py3-none-any.whl", hash = "sha256:d2c21d439b2baf7aa80d6dd4e3659259be64c6f49dfd0f32091063db0e006db4"}, - {file = "black-22.8.0.tar.gz", hash = "sha256:792f7eb540ba9a17e8656538701d3eb1afcb134e3b45b71f20b25c77a8db7e6e"}, -] +attrs = [] +black = [] click = [ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] -colorama = [] +colorama = [ + {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, + {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, +] h11 = [ {file = "h11-0.13.0-py3-none-any.whl", hash = "sha256:8ddd78563b633ca55346c8cd41ec0af27d3c79931828beffb46ce70a379e7442"}, {file = "h11-0.13.0.tar.gz", hash = "sha256:70813c1135087a248a4d38cc0e1a0181ffab2188141a93eaf567940c3957ff06"}, @@ -202,22 +280,38 @@ idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] -pathspec = [ - {file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"}, - {file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"}, +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] +pathspec = [] platformdirs = [ {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, ] -sniffio = [ - {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, - {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] +pyparsing = [ + {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, + {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, ] +pytest = [] +pytest-asyncio = [] +sniffio = [] starlette = [ {file = "starlette-0.20.4-py3-none-any.whl", hash = "sha256:c0414d5a56297d37f3db96a84034d61ce29889b9eaccf65eb98a0b39441fcaa3"}, {file = "starlette-0.20.4.tar.gz", hash = "sha256:42fcf3122f998fefce3e2c5ad7e5edbf0f02cf685d646a83a08d404726af5084"}, @@ -226,8 +320,8 @@ tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] -typing-extensions = [] -uvicorn = [ - {file = "uvicorn-0.18.3-py3-none-any.whl", hash = "sha256:0abd429ebb41e604ed8d2be6c60530de3408f250e8d2d84967d85ba9e86fe3af"}, - {file = "uvicorn-0.18.3.tar.gz", hash = "sha256:9a66e7c42a2a95222f76ec24a4b754c158261c4696e683b9dadc72b590e0311b"}, +typing-extensions = [ + {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, + {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, ] +uvicorn = [] diff --git a/examples/giraffe/pyproject.toml b/examples/giraffe/pyproject.toml index 4b1899b..1e2d778 100644 --- a/examples/giraffe/pyproject.toml +++ b/examples/giraffe/pyproject.toml @@ -18,6 +18,8 @@ starlette = "^0.20.4" #fable-python = {path = "./fable_modules/fable-python", develop = true} #fsharp-control-async-rx = {path = "./fable_modules/fsharp-control-async-rx", develop = true} black = {version = "^22.8.0", allow-prereleases = true} +pytest = "^7.1.3" +pytest-asyncio = "^0.19.0" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/examples/giraffe/Core.fs b/examples/giraffe/src/Core.fs similarity index 99% rename from examples/giraffe/Core.fs rename to examples/giraffe/src/Core.fs index 3210d27..5126a8e 100644 --- a/examples/giraffe/Core.fs +++ b/examples/giraffe/src/Core.fs @@ -1,5 +1,5 @@ // Learn more about F# at http://docs.microsoft.com/dotnet/fsharp -namespace Giraffe +namespace Giraffe.Python open System.Text open System.Threading.Tasks diff --git a/examples/giraffe/FormatExpressions.fs b/examples/giraffe/src/FormatExpressions.fs similarity index 97% rename from examples/giraffe/FormatExpressions.fs rename to examples/giraffe/src/FormatExpressions.fs index 25d63e4..eb6859d 100644 --- a/examples/giraffe/FormatExpressions.fs +++ b/examples/giraffe/src/FormatExpressions.fs @@ -166,3 +166,5 @@ let tryMatchInputExact (format: PrintfFormat<_, _, _, _, 'T>) (ignoreCase: bool) /// /// Returns `unit` if validation was successful otherwise will throw an `Exception`. /// Returns `unit` if validation was successful otherwise will throw an `Exception`. +/// Returns `unit` if validation was successful otherwise will throw an `Exception`. +/// Returns `unit` if validation was successful otherwise will throw an `Exception`. diff --git a/examples/giraffe/Giraffe.Python.fsproj b/examples/giraffe/src/Giraffe.Python.fsproj similarity index 100% rename from examples/giraffe/Giraffe.Python.fsproj rename to examples/giraffe/src/Giraffe.Python.fsproj diff --git a/examples/giraffe/src/Helpers.fs b/examples/giraffe/src/Helpers.fs new file mode 100644 index 0000000..a066700 --- /dev/null +++ b/examples/giraffe/src/Helpers.fs @@ -0,0 +1,32 @@ +namespace Giraffe.Python + +[] +module Helpers = + open System + open System.IO + + /// + /// Checks if an object is not null. + /// + /// The object to validate against `null`. + /// Returns true if the object is not null otherwise false. + let inline isNotNull x = not (isNull x) + + /// + /// Converts a string into a string option where null or an empty string will be converted to None and everything else to Some string. + /// + /// The string value to be converted into an option of string. + /// Returns None if the string was null or empty otherwise Some string. + let inline strOption (str : string) = + if String.IsNullOrEmpty str then None else Some str + + /// + /// Reads a file asynchronously from the file system. + /// + /// The absolute path of the file. + /// Returns the string contents of the file wrapped in a Task. + // let readFileAsStringAsync (filePath : string) = + // task { + // use reader = new StreamReader(filePath) + // return! reader.ReadToEndAsync() + // } diff --git a/examples/giraffe/HttpContext.fs b/examples/giraffe/src/HttpContext.fs similarity index 85% rename from examples/giraffe/HttpContext.fs rename to examples/giraffe/src/HttpContext.fs index 26ee89a..6058a5d 100644 --- a/examples/giraffe/HttpContext.fs +++ b/examples/giraffe/src/HttpContext.fs @@ -1,4 +1,4 @@ -namespace Giraffe +namespace Giraffe.Python open System.Collections.Generic open System.Threading.Tasks @@ -52,18 +52,25 @@ type HttpRequest(scope: Scope) = member x.Method: string = scope["method"] :?> string type HttpResponse(send: Request -> Task) = - let responseStart = Dictionary() - let responseBody = Dictionary() + let responseStart = + Dictionary( + dict + [ ("type", "http.response.start" :> obj) + ("status", 404) + ("headers", ResizeArray<_>()) ] + ) - do - responseStart["type"] <- "http.response.start" - responseStart["status"] <- 200 - responseStart["headers"] <- ResizeArray<_>() - - responseBody["type"] <- "http.response.body" + let responseBody = + Dictionary(dict [ ("type", "http.response.body" :> obj) ]) member val HasStarted: bool = false with get, set + member x.StatusCode + with get () = responseStart["status"] :?> int + + and set (value: int) = responseStart["status"] <- value + + member x.WriteAsync(bytes: byte[]) = task { // printfn "HttpResponse.WriteAsync()" @@ -91,9 +98,9 @@ type HttpContext(scope: Scope, receive: unit -> Task, send: Request -> let request = HttpRequest(scope) let response = HttpResponse(send) - member ctx.Items = items - member ctx.Request = request - member ctx.Response = response + member _.Items = items + member _.Request = request + member _.Response = response member ctx.WriteBytesAsync(bytes: byte[]) = // printfn "WriteBytesAsync" diff --git a/examples/giraffe/HttpHandler.fs b/examples/giraffe/src/HttpHandler.fs similarity index 97% rename from examples/giraffe/HttpHandler.fs rename to examples/giraffe/src/HttpHandler.fs index 4a5a263..eda016d 100644 --- a/examples/giraffe/HttpHandler.fs +++ b/examples/giraffe/src/HttpHandler.fs @@ -1,8 +1,10 @@ -namespace Giraffe +namespace Giraffe.Python.Pipelines open System open System.Collections.Generic +open Giraffe.Python + [] module HttpHandler = // Core handlers diff --git a/examples/giraffe/Middleware.fs b/examples/giraffe/src/Middleware.fs similarity index 96% rename from examples/giraffe/Middleware.fs rename to examples/giraffe/src/Middleware.fs index 14b1b40..a560d65 100644 --- a/examples/giraffe/Middleware.fs +++ b/examples/giraffe/src/Middleware.fs @@ -1,4 +1,4 @@ -namespace Giraffe +namespace Giraffe.Python open System open System.Threading.Tasks @@ -16,6 +16,7 @@ module Middleware = //printfn "Scope %A" scope let ctx = HttpContext(scope, receive, send) let! result = func ctx + match result with | None -> let! _ = defaultFunc ctx diff --git a/examples/giraffe/src/Program.fs b/examples/giraffe/src/Program.fs new file mode 100644 index 0000000..b262ecb --- /dev/null +++ b/examples/giraffe/src/Program.fs @@ -0,0 +1,11 @@ +module Program + +open Giraffe.Python +open Giraffe.Python.Pipelines + +let webApp = choose [ + route "/ping" + |> HttpHandler.text "pong" +] + +let app = Middleware.useGiraffe webApp diff --git a/examples/giraffe/Routing.fs b/examples/giraffe/src/Routing.fs similarity index 99% rename from examples/giraffe/Routing.fs rename to examples/giraffe/src/Routing.fs index e2a516c..9458dae 100644 --- a/examples/giraffe/Routing.fs +++ b/examples/giraffe/src/Routing.fs @@ -1,4 +1,4 @@ -namespace Giraffe +namespace Giraffe.Python open System open System.Text.RegularExpressions diff --git a/examples/giraffe/ShortGuid.fs b/examples/giraffe/src/ShortGuid.fs similarity index 100% rename from examples/giraffe/ShortGuid.fs rename to examples/giraffe/src/ShortGuid.fs diff --git a/examples/giraffe/test/Giraffe.Tests.fsproj b/examples/giraffe/test/Giraffe.Tests.fsproj new file mode 100644 index 0000000..d26a5c0 --- /dev/null +++ b/examples/giraffe/test/Giraffe.Tests.fsproj @@ -0,0 +1,30 @@ + + + net6.0 + false + false + true + preview + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/giraffe/test/Helpers.fs b/examples/giraffe/test/Helpers.fs new file mode 100644 index 0000000..970f261 --- /dev/null +++ b/examples/giraffe/test/Helpers.fs @@ -0,0 +1,70 @@ +[] +module Giraffe.Tests.Helpers + +open System +open System.Collections.Generic +open System.Threading.Tasks + +open Giraffe.Python +open System.Text + +// --------------------------------- +// Common functions +// --------------------------------- + + +let waitForDebuggerToAttach() = + printfn "Waiting for debugger to attach." + printfn "Press enter when debugger is attached in order to continue test execution..." + Console.ReadLine() |> ignore + +let removeNewLines (html : string) : string = + html.Replace(Environment.NewLine, String.Empty) + + +// --------------------------------- +// Test server/client setup +// --------------------------------- + +let next : HttpFunc = Some >> Task.FromResult + +let printBytes (bytes : byte[]) = + bytes |> Array.fold ( + fun (s : string) (b : byte) -> + match s.Length with + | 0 -> $"%i{b}" + | _ -> $"%s{s},%i{b}") "" + + +type HttpTester (?method: string, ?path: string, ?status: int) = + let _method = defaultArg method "GET" + let _path = defaultArg path "/" + let _status = defaultArg status 200 + let _scope = Dictionary (dict ["method", _method :> obj; "path", _path; "status", _status]) + let _response = Dictionary () + + let send (response: Response) = + let inline toMap kvps = + kvps + |> Seq.map (|KeyValue|) + |> Map.ofSeq + + task { + let xs = toMap response + for KeyValue(key, value) in xs do + if key <> "type" then + _response.Add(key, value) + } + + let receive () = + task { + return Dictionary () + } + + let ctx = HttpContext(_scope, receive, send) + + member this.Context = ctx + member this.Body = _response["body"] :?> byte [] + member this.Text = + let body = _response["body"] :?> byte array + Encoding.UTF8.GetString(body) diff --git a/examples/giraffe/test/Main.fs b/examples/giraffe/test/Main.fs new file mode 100644 index 0000000..3f16adb --- /dev/null +++ b/examples/giraffe/test/Main.fs @@ -0,0 +1,9 @@ +#if FABLE_COMPILER +module Program + +() +#else +module Program = + [] + let main _ = 0 +#endif diff --git a/examples/giraffe/test/TestRouting.fs b/examples/giraffe/test/TestRouting.fs new file mode 100644 index 0000000..41d6c75 --- /dev/null +++ b/examples/giraffe/test/TestRouting.fs @@ -0,0 +1,1193 @@ +module Giraffe.Tests.RoutingTests + +open System +open System.Text + +open Giraffe.Python +open Fable.Python.Tests.Util.Testing + +// --------------------------------- +// route Tests +// --------------------------------- + +[] +let ``test route: GET "/" returns "Hello World"`` () = + let test = HttpTester(path="/") + let app = + GET >=> choose [ + route "/" >=> text "Hello World" + route "/foo" >=> text "bar" + setStatusCode 404 >=> text "Not found" ] + + let expected = "Hello World" |> Encoding.UTF8.GetBytes + + let tsk = + task { + let! result = app next test.Context + + match result with + | None -> failwith $"Result was expected to be {expected}" + | Some _ -> test.Body |> equal expected + } + + tsk.GetAwaiter().GetResult() + +[] +let ``test route: GET "/foo" returns "bar"`` () = + let test = HttpTester(path="/foo") + + let app = + GET >=> choose [ + route "/" >=> text "Hello World" + route "/foo" >=> text "bar" + setStatusCode 404 >=> text "Not found" ] + + let expected = "bar" |> Encoding.UTF8.GetBytes + + let tsk = + task { + let! result = app next test.Context + + match result with + | None -> failwith $"Result was expected to be {expected}" + | Some _ -> test.Body |> equal expected + } + tsk.GetAwaiter().GetResult() + +[] +let ``test route: GET "/FOO" returns 404 "Not found"`` () = + let test = HttpTester(path="/FOO") + let app = + GET >=> choose [ + route "/" >=> text "Hello World" + route "/foo" >=> text "bar" + setStatusCode 404 >=> text "Not found" ] + + let expected = "Not found" |> Encoding.UTF8.GetBytes + + let tsk = + task { + let! result = app next test.Context + + match result with + | None -> failwith $"Result was expected to be {expected}" + | Some ctx -> + let body = test.Body + body |> equal expected + ctx.Response.StatusCode |> equal 404 + } + tsk.GetAwaiter().GetResult() + +// --------------------------------- +// routeCi Tests +// --------------------------------- + +[] +let ``test GET "/JSON" returns "BaR"`` () = + let test = HttpTester(path="/JSON") + let app = + GET >=> choose [ + route "/" >=> text "Hello World" + route "/foo" >=> text "bar" + route "/json" >=> text "FOO" + routeCi "/json" >=> text "BaR" + setStatusCode 404 >=> text "Not found" ] + + let expected = "BaR" |> Encoding.UTF8.GetBytes + + let tsk = + task { + let! result = app next test.Context + + match result with + | None -> failwith $"Result was expected to be {expected}" + | Some ctx -> test.Body |> equal expected + } + tsk.GetAwaiter().GetResult() + +// --------------------------------- +// routex Tests +// --------------------------------- + +[] +let ``test routex: GET "/" returns "Hello World"`` () = + let test = HttpTester(path="/") + let app = + GET >=> choose [ + routex "/" >=> text "Hello World" + routex "/foo" >=> text "bar" + setStatusCode 404 >=> text "Not found" ] + + let expected = "Hello World" |> Encoding.UTF8.GetBytes + + let tsk = + task { + let! result = app next test.Context + + match result with + | None -> failwith $"Result was expected to be {expected}" + | Some ctx -> test.Body |> equal expected + } + tsk.GetAwaiter().GetResult() + +[] +let ``test routex: GET "/foo" returns "bar"`` () = + let test = HttpTester(path="/foo") + let app = + GET >=> choose [ + routex "/" >=> text "Hello World" + routex "/foo" >=> text "bar" + setStatusCode 404 >=> text "Not found" ] + + let expected = "bar" |> Encoding.UTF8.GetBytes + + task { + let! result = app next test.Context + + match result with + | None -> failwith $"Result was expected to be {expected}" + | Some ctx -> test.Body |> equal expected + } |> (fun tsk -> tsk.GetAwaiter().GetResult()) + +[] +let ``test routex: GET "/FOO" returns 404 "Not found"`` () = + let test = HttpTester(path="/FOO") + let app = + GET >=> choose [ + routex "/" >=> text "Hello World" + routex "/foo" >=> text "bar" + setStatusCode 404 >=> text "Not found" ] + + let expected = "Not found" |> Encoding.UTF8.GetBytes + + task { + let! result = app next test.Context + + match result with + | None -> failwith $"Result was expected to be {expected}" + | Some ctx -> + let body = test.Body + body |> equal expected + ctx.Response.StatusCode |> equal 404 + } |> (fun tsk -> tsk.GetAwaiter().GetResult()) + +[] +let ``test routex: GET "/foo///" returns "bar"`` () = + let test = HttpTester(path="/foo///") + let app = + GET >=> choose [ + routex "/" >=> text "Hello World" + routex "/foo(/*)" >=> text "bar" + setStatusCode 404 >=> text "Not found" ] + + let expected = "bar" |> Encoding.UTF8.GetBytes + + task { + let! result = app next test.Context + + match result with + | None -> failwith $"Result was expected to be {expected}" + | Some ctx -> test.Body |> equal expected + } |> (fun tsk -> tsk.GetAwaiter().GetResult()) + +[] +let ``test routex: GET "/foo2" returns "bar"`` () = + let test = HttpTester(path="/foo2") + let app = + GET >=> choose [ + routex "/" >=> text "Hello World" + routex "/foo2(/*)" >=> text "bar" + setStatusCode 404 >=> text "Not found" ] + + let expected = "bar" |> Encoding.UTF8.GetBytes + + task { + let! result = app next test.Context + + match result with + | None ->failwith $"Result was expected to be {expected}" + | Some ctx -> test.Body |> equal expected + } |> (fun tsk -> tsk.GetAwaiter().GetResult()) + +// // --------------------------------- +// // routeCix Tests +// // --------------------------------- +// +// [] +// let ``routeCix: GET "/CaSe///" returns "right"`` () = +// let ctx = Substitute.For() +// let app = +// GET >=> choose [ +// routex "/case(/*)" >=> text "wrong" +// routeCix "/case(/*)" >=> text "right" +// setStatusCode 404 >=> text "Not found" ] +// +// ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore +// ctx.Request.Path.ReturnsForAnyArgs (PathString("/CaSe///")) |> ignore +// ctx.Response.Body <- new MemoryStream() +// let expected = "right" +// +// task { +// let! result = app next ctx +// +// match result with +// | None -> assertFailf "Result was expected to be %s" expected +// | Some ctx -> Assert.Equal(expected, getBody ctx) +// } +// +// --------------------------------- +// routef Tests +// --------------------------------- + +[] +let ``test routef: GET "/foo/blah blah/bar" returns "blah blah"`` () = + let test = HttpTester(path="/foo/blah blah/bar") + let app = + GET >=> choose [ + route "/" >=> text "Hello World" + route "/foo" >=> text "bar" + routef "/foo/%s/bar" text + routef "/foo/%s/%i" (fun (name, age) -> text (sprintf "Name: %s, Age: %d" name age)) + setStatusCode 404 >=> text "Not found" ] + + let expected = "blah blah" |> Encoding.UTF8.GetBytes + + task { + let! result = app next test.Context + + match result with + | None ->failwith $"Result was expected to be {expected}" + | Some ctx -> test.Body |> equal expected + } |> (fun tsk -> tsk.GetAwaiter().GetResult()) + +[] +let ``test routef: GET "/foo/johndoe/59" returns "Name: johndoe, Age: 59"`` () = + let test = HttpTester(path="/foo/johndoe/59") + let app = + GET >=> choose [ + route "/" >=> text "Hello World" + route "/foo" >=> text "bar" + routef "/foo/%s/bar" text + routef "/foo/%s/%i" (fun (name, age) -> text (sprintf "Name: %s, Age: %d" name age)) + setStatusCode 404 >=> text "Not found" ] + + let expected = "Name: johndoe, Age: 59" |> Encoding.UTF8.GetBytes + + task { + let! result = app next test.Context + + match result with + | None ->failwith $"Result was expected to be {expected}" + | Some ctx -> test.Body |> equal expected + } |> (fun tsk -> tsk.GetAwaiter().GetResult()) + +[] +let ``test routef: GET "/foo/b%2Fc/bar" returns "b%2Fc"`` () = + let test = HttpTester(path="/foo/b%2Fc/bar") + let app = + GET >=> choose [ + route "/" >=> text "Hello World" + route "/foo" >=> text "bar" + routef "/foo/%s/bar" text + routef "/foo/%s/%i" (fun (name, age) -> text (sprintf "Name: %s, Age: %d" name age)) + setStatusCode 404 >=> text "Not found" ] + + let expected = "b/c" |> Encoding.UTF8.GetBytes + + task { + let! result = app next test.Context + + match result with + | None ->failwith $"Result was expected to be {expected}" + | Some ctx -> test.Body |> equal expected + } |> (fun tsk -> tsk.GetAwaiter().GetResult()) + +[] +let ``test routef: GET "/foo/a%2Fb%2Bc.d%2Ce/bar" returns "a%2Fb%2Bc.d%2Ce"`` () = + let test = HttpTester(path="/foo/a%2Fb%2Bc.d%2Ce/bar") + let app = + GET >=> choose [ + route "/" >=> text "Hello World" + route "/foo" >=> text "bar" + routef "/foo/%s/bar" text + routef "/foo/%s/%i" (fun (name, age) -> text (sprintf "Name: %s, Age: %d" name age)) + setStatusCode 404 >=> text "Not found" ] + + let expected = "a/b%2Bc.d%2Ce" |> Encoding.UTF8.GetBytes + + task { + let! result = app next test.Context + + match result with + | None ->failwith $"Result was expected to be {expected}" + | Some ctx -> test.Body |> equal expected + } |> (fun tsk -> tsk.GetAwaiter().GetResult()) +// +// [] +// [] +// [] +// [] +// [] +// [] +// [] +// [] +// let ``routeStartsWith(f|Cif)`` (uri:string, expected:string) = +// +// let app = +// GET >=> choose [ +// routeStartsWithf "/API/%s/" (fun capture -> text ("routeStartsWithf:" + capture)) +// routeStartsWithCif "/api/%s/" (fun capture -> text ("routeStartsWithCif:" + capture)) +// setStatusCode 404 >=> text "Not found" +// ] +// +// let ctx = Substitute.For() +// ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore +// ctx.Request.Path.ReturnsForAnyArgs (PathString(uri)) |> ignore +// ctx.Response.Body <- new MemoryStream() +// +// task { +// let! result = app next ctx +// +// match result with +// | None -> assertFailf "Result was expected to be %s" expected +// | Some ctx -> Assert.Equal(expected, getBody ctx) +// } +// +[] +let ``test routef: GET "/foo/%O/bar/%O" returns "Guid1: ..., Guid2: ..."`` () = + let test = HttpTester(path="/foo/4ec87f064d1e41b49342ab1aead1f99d/bar/2a6c9185-95d9-4d8c-80a6-575f99c2a716") + let app = + GET >=> choose [ + route "/" >=> text "Hello World" + route "/foo" >=> text "bar" + routef "/foo/%s/bar" text + routef "/foo/%s/%i" (fun (name, age) -> text (sprintf "Name: %s, Age: %d" name age)) + routef "/foo/%O/bar/%O" (fun (guid1 : Guid, guid2 : Guid) -> text (sprintf "Guid1: %O, Guid2: %O" guid1 guid2)) + setStatusCode 404 >=> text "Not found" ] + + let expected = "Guid1: 4ec87f06-4d1e-41b4-9342-ab1aead1f99d, Guid2: 2a6c9185-95d9-4d8c-80a6-575f99c2a716" |> Encoding.UTF8.GetBytes + + task { + let! result = app next test.Context + + match result with + | None ->failwith $"Result was expected to be {expected}" + | Some ctx -> test.Body |> equal expected + } |> (fun tsk -> tsk.GetAwaiter().GetResult()) + +[] +let ``test routef: GET "/foo/%u/bar/%u" returns "Id1: ..., Id2: ..."`` () = + let test = HttpTester(path="/foo/r1iKapqh_s4/bar/5aLu720NzTs") + let app = + GET >=> choose [ + route "/" >=> text "Hello World" + route "/foo" >=> text "bar" + routef "/foo/%s/bar" text + routef "/foo/%s/%i" (fun (name, age) -> text (sprintf "Name: %s, Age: %d" name age)) + routef "/foo/%u/bar/%u" (fun (id1 : uint64, id2 : uint64) -> text (sprintf "Id1: %u, Id2: %u" id1 id2)) + setStatusCode 404 >=> text "Not found" ] + + let expected = "Id1: 12635000945053400782, Id2: 16547050693006839099" |> Encoding.UTF8.GetBytes + + task { + let! result = app next test.Context + + match result with + | None -> failwith $"Result was expected to be {expected}" + | Some ctx -> test.Body |> equal expected + } |> (fun tsk -> tsk.GetAwaiter().GetResult()) + +[] +let ``test routef: GET "/foo/bar/baz/qux" returns 404 "Not found"`` () = + let test = HttpTester(path="/foo/bar/baz/qux") + let app = + GET >=> choose [ + routef "/foo/%s/%s" (fun (s1, s2) -> text (sprintf "%s,%s" s1 s2)) + setStatusCode 404 >=> text "Not found" ] + + let expected = "Not found" |> Encoding.UTF8.GetBytes + + task { + let! result = app next test.Context + + match result with + | None -> failwith $"Result was expected to be {expected}" + | Some ctx -> + let body = test.Body + body |> equal expected + ctx.Response.StatusCode |> equal 404 + } |> (fun tsk -> tsk.GetAwaiter().GetResult()) +// +// // --------------------------------- +// // routeCif Tests +// // --------------------------------- +// +// [] +// let ``POST "/POsT/1" returns "1"`` () = +// let ctx = Substitute.For() +// mockJson ctx (Newtonsoft None) +// let app = +// choose [ +// GET >=> choose [ +// route "/" >=> text "Hello World" ] +// POST >=> choose [ +// route "/post/1" >=> text "2" +// routeCif "/post/%i" json ] +// setStatusCode 404 >=> text "Not found" ] +// +// ctx.Request.Method.ReturnsForAnyArgs "POST" |> ignore +// ctx.Request.Path.ReturnsForAnyArgs (PathString("/POsT/1")) |> ignore +// ctx.Response.Body <- new MemoryStream() +// let expected = "1" +// +// task { +// let! result = app next ctx +// +// match result with +// | None -> assertFailf "Result was expected to be %s" expected +// | Some ctx -> Assert.Equal(expected, getBody ctx) +// } +// +// [] +// let ``POST "/POsT/523" returns "523"`` () = +// let ctx = Substitute.For() +// mockJson ctx (Newtonsoft None) +// let app = +// choose [ +// GET >=> choose [ +// route "/" >=> text "Hello World" ] +// POST >=> choose [ +// route "/post/1" >=> text "1" +// routeCif "/post/%i" json ] +// setStatusCode 404 >=> text "Not found" ] +// +// ctx.Request.Method.ReturnsForAnyArgs "POST" |> ignore +// ctx.Request.Path.ReturnsForAnyArgs (PathString("/POsT/523")) |> ignore +// ctx.Response.Body <- new MemoryStream() +// let expected = "523" +// +// task { +// let! result = app next ctx +// +// match result with +// | None -> assertFailf "Result was expected to be %s" expected +// | Some ctx -> Assert.Equal(expected, getBody ctx) +// } +// +// // --------------------------------- +// // routeBind Tests +// // --------------------------------- +// +// [] +// type RouteBind = { Foo : string; Bar : int; Id : Guid } +// +// [] +// type RouteBindId = { Id : Guid } +// +// type PaymentMethod = +// | Credit +// | Debit +// +// [] +// type Purchase = { PaymentMethod : PaymentMethod } +// +// [] +// let ``routeBind: Route has matching union type``() = +// let ctx = Substitute.For() +// let app = +// GET >=> choose [ +// routeBind "/{paymentMethod}" +// (fun p -> sprintf "%s" (p.PaymentMethod.ToString()) |> text) +// setStatusCode 404 >=> text "Not found" ] +// ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore +// ctx.Request.Path.ReturnsForAnyArgs (PathString("/credit")) |> ignore +// ctx.Response.Body <- new MemoryStream() +// let expected = "Credit" +// task { +// let! result = app next ctx +// +// match result with +// | None -> assertFailf "Result was expected to be %s" expected +// | Some ctx -> Assert.Equal(expected, getBody ctx) +// } +// +// [] +// let ``routeBind: Route doesn't match union type``() = +// let ctx = Substitute.For() +// let app = +// GET >=> choose [ +// routeBind "/{paymentMethod}" +// (fun p -> sprintf "%s" (p.PaymentMethod.ToString()) |> text) +// setStatusCode 404 >=> text "Not found" ] +// ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore +// ctx.Request.Path.ReturnsForAnyArgs (PathString("/wrong")) |> ignore +// ctx.Response.Body <- new MemoryStream() +// let expected = "Not found" +// task { +// let! result = app next ctx +// +// match result with +// | None -> assertFailf "Result was expected to be %s" expected +// | Some ctx -> Assert.Equal(expected, getBody ctx) +// } +// +// [] +// let ``routeBind: Normal route``() = +// let ctx = Substitute.For() +// let app = GET >=> routeBind "/{foo}/{bar}/{id}" (fun m -> sprintf "%s %i %O" m.Foo m.Bar m.Id |> text) +// ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore +// ctx.Request.Path.ReturnsForAnyArgs (PathString("/Hello/1/f40580b1-d55b-4fe2-b6fb-ca4f90749a9d")) |> ignore +// ctx.Response.Body <- new MemoryStream() +// task { +// let! result = app next ctx +// +// match result with +// | None -> assertFail "It was expected that the result would be Hello 1" +// | Some ctx -> +// let body = getBody ctx +// Assert.Equal("Hello 1 f40580b1-d55b-4fe2-b6fb-ca4f90749a9d", body) +// } +// +// [] +// let ``routeBind: Normal route with trailing slash``() = +// let ctx = Substitute.For() +// let app = GET >=> routeBind "/{foo}/{bar}/{id}/" (fun m -> sprintf "%s %i %O" m.Foo m.Bar m.Id |> text) +// ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore +// ctx.Request.Path.ReturnsForAnyArgs (PathString("/Hello/1/f40580b1-d55b-4fe2-b6fb-ca4f90749a9d/")) |> ignore +// ctx.Response.Body <- new MemoryStream() +// task { +// let! result = app next ctx +// +// match result with +// | None -> assertFail "It was expected that the result would be Hello 1f40580b1-d55b-4fe2-b6fb-ca4f90749a9d" +// | Some ctx -> +// let body = getBody ctx +// Assert.Equal("Hello 1 f40580b1-d55b-4fe2-b6fb-ca4f90749a9d", body) +// } +// +// [] +// let ``routeBind: Route with (/*) matches mutliple trailing slashes``() = +// let ctx = Substitute.For() +// let app = GET >=> routeBind "/{foo}/{bar}/{id}(/*)" (fun m -> sprintf "%s %i %O" m.Foo m.Bar m.Id |> text) +// ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore +// ctx.Request.Path.ReturnsForAnyArgs (PathString("/Hello/1/f40580b1-d55b-4fe2-b6fb-ca4f90749a9d///")) |> ignore +// ctx.Response.Body <- new MemoryStream() +// task { +// let! result = app next ctx +// +// match result with +// | None -> assertFail "It was expected that the result would be Hello 1f40580b1-d55b-4fe2-b6fb-ca4f90749a9d" +// | Some ctx -> +// let body = getBody ctx +// Assert.Equal("Hello 1 f40580b1-d55b-4fe2-b6fb-ca4f90749a9d", body) +// } +// +// [] +// let ``routeBind: Route with (/*) matches no trailing slash``() = +// let ctx = Substitute.For() +// let app = GET >=> routeBind "/{foo}/{bar}/{id}(/*)" (fun m -> sprintf "%s %i %O" m.Foo m.Bar m.Id |> text) +// ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore +// ctx.Request.Path.ReturnsForAnyArgs (PathString("/Hello/2/f40580b1-d55b-4fe2-b6fb-ca4f90749a9d")) |> ignore +// ctx.Response.Body <- new MemoryStream() +// task { +// let! result = app next ctx +// +// match result with +// | None -> assertFail "It was expected that the result would be Hello 1f40580b1-d55b-4fe2-b6fb-ca4f90749a9d" +// | Some ctx -> +// let body = getBody ctx +// Assert.Equal("Hello 2 f40580b1-d55b-4fe2-b6fb-ca4f90749a9d", body) +// } +// +// [] +// let ``routeBind: Route with (/?) matches single trailing slash``() = +// let ctx = Substitute.For() +// let app = GET >=> routeBind "/{foo}/{bar}/{id}(/?)" (fun m -> sprintf "%s %i %O" m.Foo m.Bar m.Id |> text) +// ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore +// ctx.Request.Path.ReturnsForAnyArgs (PathString("/Hello/3/f40580b1-d55b-4fe2-b6fb-ca4f90749a9d/")) |> ignore +// ctx.Response.Body <- new MemoryStream() +// task { +// let! result = app next ctx +// +// match result with +// | None -> assertFail "It was expected that the result would be Hello 3 1f40580b1-d55b-4fe2-b6fb-ca4f90749a9d" +// | Some ctx -> +// let body = getBody ctx +// Assert.Equal("Hello 3 f40580b1-d55b-4fe2-b6fb-ca4f90749a9d", body) +// } +// +// [] +// let ``routeBind: Route with (/?) matches no trailing slash``() = +// let ctx = Substitute.For() +// let app = GET >=> routeBind "/{foo}/{bar}/{id}(/?)" (fun m -> sprintf "%s %i %O" m.Foo m.Bar m.Id |> text) +// ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore +// ctx.Request.Path.ReturnsForAnyArgs (PathString("/Hello/4/f40580b1-d55b-4fe2-b6fb-ca4f90749a9d")) |> ignore +// ctx.Response.Body <- new MemoryStream() +// task { +// let! result = app next ctx +// +// match result with +// | None -> assertFail "It was expected that the result would be Hello 4 1f40580b1-d55b-4fe2-b6fb-ca4f90749a9d" +// | Some ctx -> +// let body = getBody ctx +// Assert.Equal("Hello 4 f40580b1-d55b-4fe2-b6fb-ca4f90749a9d", body) +// } +// +// [] +// let ``routeBind: Route with non parameterised part``() = +// let ctx = Substitute.For() +// let app = GET >=> routeBind "/api/{foo}/{bar}/{id}" (fun m -> sprintf "%s %i %O" m.Foo m.Bar m.Id |> text) +// ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore +// ctx.Request.Path.ReturnsForAnyArgs (PathString("/api/Hello/1/f40580b1-d55b-4fe2-b6fb-ca4f90749a9d")) |> ignore +// ctx.Response.Body <- new MemoryStream() +// task { +// let! result = app next ctx +// +// match result with +// | None -> assertFail "It was expected that the result would be Hello 1" +// | Some ctx -> +// let body = getBody ctx +// Assert.Equal("Hello 1 f40580b1-d55b-4fe2-b6fb-ca4f90749a9d", body) +// } +// +// [] +// let ``routeBind: Route with non parameterised part and with Guid binding``() = +// let ctx = Substitute.For() +// let app = GET >=> routeBind "/api/{id}" (fun m -> sprintf "%O" m.Id |> text) +// ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore +// ctx.Request.Path.ReturnsForAnyArgs (PathString("/api/f40580b1-d55b-4fe2-b6fb-ca4f90749a9d")) |> ignore +// ctx.Response.Body <- new MemoryStream() +// task { +// let! result = app next ctx +// +// match result with +// | None -> assertFail "It was expected that the result would be f40580b1-d55b-4fe2-b6fb-ca4f90749a9d" +// | Some ctx -> +// let body = getBody ctx +// Assert.Equal("f40580b1-d55b-4fe2-b6fb-ca4f90749a9d", body) +// } +// +// [] +// let ``routeBind: Route with non parameterised part and with (/?)``() = +// let ctx = Substitute.For() +// let app = GET >=> routeBind "/api/{id}(/?)" (fun m -> sprintf "%O" m.Id |> text) +// ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore +// ctx.Request.Path.ReturnsForAnyArgs (PathString("/api/f40580b1-d55b-4fe2-b6fb-ca4f90749a9d/")) |> ignore +// ctx.Response.Body <- new MemoryStream() +// task { +// let! result = app next ctx +// +// match result with +// | None -> assertFail "It was expected that the result would be f40580b1-d55b-4fe2-b6fb-ca4f90749a9d" +// | Some ctx -> +// let body = getBody ctx +// Assert.Equal("f40580b1-d55b-4fe2-b6fb-ca4f90749a9d", body) +// } +// +// [] +// let ``routeBind: Route nested after subRoute``() = +// let ctx = Substitute.For() +// let app = GET >=> subRoute "/test" (routeBind "/api/{id}(/?)" (fun m -> sprintf "%O" m.Id |> text)) +// ctx.Items.Returns (new Dictionary() :> IDictionary) |> ignore +// ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore +// ctx.Request.Path.ReturnsForAnyArgs (PathString("/test/api/f40580b1-d55b-4fe2-b6fb-ca4f90749a9d/")) |> ignore +// ctx.Response.Body <- new MemoryStream() +// task { +// let! result = app next ctx +// +// match result with +// | None -> assertFail "It was expected that the result would be f40580b1-d55b-4fe2-b6fb-ca4f90749a9d" +// | Some ctx -> +// let body = getBody ctx +// Assert.Equal("f40580b1-d55b-4fe2-b6fb-ca4f90749a9d", body) +// } +// +// // --------------------------------- +// // subRoute Tests +// // --------------------------------- +// +// [] +// let ``subRoute: Route with empty route`` () = +// let ctx = Substitute.For() +// let app = +// GET >=> choose [ +// route "/" >=> text "Hello World" +// route "/foo" >=> text "bar" +// subRoute "/api" ( +// choose [ +// route "" >=> text "api root" +// route "/admin" >=> text "admin" +// route "/users" >=> text "users" ] ) +// route "/api/test" >=> text "test" +// setStatusCode 404 >=> text "Not found" ] +// +// ctx.Items.Returns (new Dictionary() :> IDictionary) |> ignore +// ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore +// ctx.Request.Path.ReturnsForAnyArgs (PathString("/api")) |> ignore +// ctx.Response.Body <- new MemoryStream() +// let expected = "api root" +// +// task { +// let! result = app next ctx +// +// match result with +// | None -> assertFailf "Result was expected to be %s" expected +// | Some ctx -> Assert.Equal(expected, getBody ctx) +// } +// +// [] +// let ``subRoute: Normal nested route after subRoute`` () = +// let ctx = Substitute.For() +// let app = +// GET >=> choose [ +// route "/" >=> text "Hello World" +// route "/foo" >=> text "bar" +// subRoute "/api" ( +// choose [ +// route "" >=> text "api root" +// route "/admin" >=> text "admin" +// route "/users" >=> text "users" ] ) +// route "/api/test" >=> text "test" +// setStatusCode 404 >=> text "Not found" ] +// +// ctx.Items.Returns (new Dictionary() :> IDictionary) |> ignore +// ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore +// ctx.Request.Path.ReturnsForAnyArgs (PathString("/api/users")) |> ignore +// ctx.Response.Body <- new MemoryStream() +// let expected = "users" +// +// task { +// let! result = app next ctx +// +// match result with +// | None -> assertFailf "Result was expected to be %s" expected +// | Some ctx -> Assert.Equal(expected, getBody ctx) +// } +// +// [] +// let ``subRoute: Route after subRoute has same beginning of path`` () = +// +// task { +// let ctx = Substitute.For() +// +// let app = +// GET >=> choose [ +// route "/" >=> text "Hello World" +// route "/foo" >=> text "bar" +// subRoute "/api" ( +// choose [ +// route "" >=> text "api root" +// route "/admin" >=> text "admin" +// route "/users" >=> text "users" ] ) +// route "/api/test" >=> text "test" +// setStatusCode 404 >=> text "Not found" ] +// +// ctx.Items.Returns (new Dictionary() :> IDictionary) |> ignore +// ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore +// ctx.Request.Path.ReturnsForAnyArgs (PathString("/api/test")) |> ignore +// ctx.Response.Body <- new MemoryStream() +// let expected = "test" +// +// let! result = app next ctx +// +// match result with +// | None -> assertFailf "Result was expected to be %s" expected +// | Some ctx -> Assert.Equal(expected, getBody ctx) +// } +// +// [] +// let ``subRoute: Nested sub routes`` () = +// let ctx = Substitute.For() +// let app = +// GET >=> choose [ +// route "/" >=> text "Hello World" +// route "/foo" >=> text "bar" +// subRoute "/api" ( +// choose [ +// route "" >=> text "api root" +// route "/admin" >=> text "admin" +// route "/users" >=> text "users" +// subRoute "/v2" ( +// choose [ +// route "" >=> text "api root v2" +// route "/admin" >=> text "admin v2" +// route "/users" >=> text "users v2" +// ] +// ) +// ] +// ) +// route "/api/test" >=> text "test" +// setStatusCode 404 >=> text "Not found" ] +// +// ctx.Items.Returns (new Dictionary() :> IDictionary) |> ignore +// ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore +// ctx.Request.Path.ReturnsForAnyArgs (PathString("/api/v2/users")) |> ignore +// ctx.Response.Body <- new MemoryStream() +// let expected = "users v2" +// +// task { +// let! result = app next ctx +// +// match result with +// | None -> assertFailf "Result was expected to be %s" expected +// | Some ctx -> Assert.Equal(expected, getBody ctx) +// } +// +// [] +// let ``subRoute: Multiple nested sub routes`` () = +// let ctx = Substitute.For() +// let app = +// GET >=> choose [ +// route "/" >=> text "Hello World" +// route "/foo" >=> text "bar" +// subRoute "/api" ( +// choose [ +// route "/users" >=> text "users" +// subRoute "/v2" ( +// choose [ +// route "/admin" >=> text "admin v2" +// route "/users" >=> text "users v2" +// ] +// ) +// subRoute "/v2" ( +// route "/admin2" >=> text "correct admin2" +// ) +// ] +// ) +// route "/api/test" >=> text "test" +// route "/api/v2/else" >=> text "else" +// setStatusCode 404 >=> text "Not found" ] +// +// ctx.Items.Returns (new Dictionary() :> IDictionary) |> ignore +// ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore +// ctx.Request.Path.ReturnsForAnyArgs (PathString("/api/v2/admin2")) |> ignore +// ctx.Response.Body <- new MemoryStream() +// let expected = "correct admin2" +// +// task { +// let! result = app next ctx +// +// match result with +// | None -> assertFailf "Result was expected to be %s" expected +// | Some ctx -> Assert.Equal(expected, getBody ctx) +// } +// +// [] +// let ``subRoute: Route after nested sub routes has same beginning of path`` () = +// let ctx = Substitute.For() +// let app = +// GET >=> choose [ +// route "/" >=> text "Hello World" +// route "/foo" >=> text "bar" +// subRoute "/api" ( +// choose [ +// route "" >=> text "api root" +// route "/admin" >=> text "admin" +// route "/users" >=> text "users" +// subRoute "/v2" ( +// choose [ +// route "" >=> text "api root v2" +// route "/admin" >=> text "admin v2" +// route "/users" >=> text "users v2" +// ] +// ) +// route "/yada" >=> text "yada" +// ] +// ) +// route "/api/test" >=> text "test" +// route "/api/v2/else" >=> text "else" +// setStatusCode 404 >=> text "Not found" ] +// +// ctx.Items.Returns (new Dictionary() :> IDictionary) |> ignore +// ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore +// ctx.Request.Path.ReturnsForAnyArgs (PathString("/api/v2/else")) |> ignore +// ctx.Response.Body <- new MemoryStream() +// let expected = "else" +// +// task { +// let! result = app next ctx +// +// match result with +// | None -> assertFailf "Result was expected to be %s" expected +// | Some ctx -> Assert.Equal(expected, getBody ctx) +// } +// +// [] +// let ``subRoute: routef inside subRoute`` () = +// let ctx = Substitute.For() +// let app = +// GET >=> choose [ +// route "/" >=> text "Hello World" +// route "/foo" >=> text "bar" +// subRoute "/api" ( +// choose [ +// route "" >=> text "api root" +// routef "/foo/bar/%s" text ] ) +// route "/api/test" >=> text "test" +// setStatusCode 404 >=> text "Not found" ] +// +// ctx.Items.Returns (new Dictionary() :> IDictionary) |> ignore +// ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore +// ctx.Request.Path.ReturnsForAnyArgs (PathString("/api/foo/bar/yadayada")) |> ignore +// ctx.Response.Body <- new MemoryStream() +// let expected = "yadayada" +// +// task { +// let! result = app next ctx +// +// match result with +// | None -> assertFailf "Result was expected to be %s" expected +// | Some ctx -> Assert.Equal(expected, getBody ctx) +// } +// +// // --------------------------------- +// // subRoutef Tests +// // --------------------------------- +// +// [] +// let ``routef: Validation`` () = +// Assert.Throws( fun () -> +// GET >=> choose [ +// route "/" >=> text "Hello World" +// route "/foo" >=> text "bar" +// routef "/foo/%s/%d" (fun (name, age) -> text (sprintf "Name: %s, Age: %d" name age)) +// setStatusCode 404 >=> text "Not found" ] +// |> ignore +// ) |> ignore +// +// [] +// let ``subRoutef: GET "/" returns "Not found"`` () = +// let ctx = Substitute.For() +// let app = +// GET >=> choose [ +// subRoutef "/%s/%i" (fun (lang, version) -> +// choose [ +// route "/foo" >=> text "bar" +// routef "/%s" (fun name -> text (sprintf "Hello %s! Lang: %s, Version: %i" name lang version)) +// ]) +// route "/bar" >=> text "foo" +// setStatusCode 404 >=> text "Not found" ] +// +// ctx.Items.Returns (new Dictionary() :> IDictionary) |> ignore +// ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore +// ctx.Request.Path.ReturnsForAnyArgs (PathString("/")) |> ignore +// ctx.Response.Body <- new MemoryStream() +// let expected = "Not found" +// +// task { +// let! result = app next ctx +// +// match result with +// | None -> assertFailf "Result was expected to be %s" expected +// | Some ctx -> Assert.Equal(expected, getBody ctx) +// } +// +// [] +// let ``subRoutef: GET "/bar" returns "foo"`` () = +// let ctx = Substitute.For() +// let app = +// GET >=> choose [ +// subRoutef "/%s/%i" (fun (lang, version) -> +// choose [ +// route "/foo" >=> text "bar" +// routef "/%s" (fun name -> text (sprintf "Hello %s! Lang: %s, Version: %i" name lang version)) +// ]) +// route "/bar" >=> text "foo" +// setStatusCode 404 >=> text "Not found" ] +// +// ctx.Items.Returns (new Dictionary() :> IDictionary) |> ignore +// ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore +// ctx.Request.Path.ReturnsForAnyArgs (PathString("/bar")) |> ignore +// ctx.Response.Body <- new MemoryStream() +// let expected = "foo" +// +// task { +// let! result = app next ctx +// +// match result with +// | None -> assertFailf "Result was expected to be %s" expected +// | Some ctx -> Assert.Equal(expected, getBody ctx) +// } +// +// [] +// let ``subRoutef: GET "/John/5/foo" returns "bar"`` () = +// let ctx = Substitute.For() +// let app = +// GET >=> choose [ +// subRoutef "/%s/%i" (fun (lang, version) -> +// choose [ +// route "/foo" >=> text "bar" +// routef "/%s" (fun name -> text (sprintf "Hello %s! Lang: %s, Version: %i" name lang version)) +// ]) +// route "/bar" >=> text "foo" +// setStatusCode 404 >=> text "Not found" ] +// +// ctx.Items.Returns (new Dictionary() :> IDictionary) |> ignore +// ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore +// ctx.Request.Path.ReturnsForAnyArgs (PathString("/John/5/foo")) |> ignore +// ctx.Response.Body <- new MemoryStream() +// let expected = "bar" +// +// task { +// let! result = app next ctx +// +// match result with +// | None -> assertFailf "Result was expected to be %s" expected +// | Some ctx -> Assert.Equal(expected, getBody ctx) +// } +// +// [] +// let ``subRoutef: GET "/en/10/Julia" returns "Hello Julia! Lang: en, Version: 10"`` () = +// let ctx = Substitute.For() +// let app = +// GET >=> choose [ +// subRoutef "/%s/%i" (fun (lang, version) -> +// choose [ +// route "/foo" >=> text "bar" +// routef "/%s" (fun name -> text (sprintf "Hello %s! Lang: %s, Version: %i" name lang version)) +// ]) +// route "/bar" >=> text "foo" +// setStatusCode 404 >=> text "Not found" ] +// +// ctx.Items.Returns (new Dictionary() :> IDictionary) |> ignore +// ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore +// ctx.Request.Path.ReturnsForAnyArgs (PathString("/en/10/Julia")) |> ignore +// ctx.Response.Body <- new MemoryStream() +// let expected = "Hello Julia! Lang: en, Version: 10" +// +// task { +// let! result = app next ctx +// +// match result with +// | None -> assertFailf "Result was expected to be %s" expected +// | Some ctx -> Assert.Equal(expected, getBody ctx) +// } +// +// [] +// let ``subRoutef: GET "/en/10/api/Julia" returns "Hello Julia! Lang: en, Version: 10"`` () = +// let ctx = Substitute.For() +// let app = +// GET >=> choose [ +// subRoutef "/%s/%i/api" (fun (lang, version) -> +// choose [ +// route "/foo" >=> text "bar" +// routef "/%s" (fun name -> text (sprintf "Hello %s! Lang: %s, Version: %i" name lang version)) +// ]) +// route "/bar" >=> text "foo" +// setStatusCode 404 >=> text "Not found" ] +// +// ctx.Items.Returns (new Dictionary() :> IDictionary) |> ignore +// ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore +// ctx.Request.Path.ReturnsForAnyArgs (PathString("/en/10/api/Julia")) |> ignore +// ctx.Response.Body <- new MemoryStream() +// let expected = "Hello Julia! Lang: en, Version: 10" +// +// task { +// let! result = app next ctx +// +// match result with +// | None -> assertFailf "Result was expected to be %s" expected +// | Some ctx -> Assert.Equal(expected, getBody ctx) +// } +// +// // --------------------------------- +// // subRouteCi Tests +// // --------------------------------- +// +// [] +// let ``subRouteCi: Non-filtering handler after subRouteCi is called`` () = +// let ctx = Substitute.For() +// mockJson ctx (Newtonsoft None) +// let app = +// GET >=> choose [ +// subRouteCi "/foo" (text "subroute /foo") +// setStatusCode 404 >=> text "Not found" ] +// +// ctx.Items.Returns (new Dictionary() :> IDictionary) |> ignore +// ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore +// ctx.Request.Path.ReturnsForAnyArgs (PathString("/FOO")) |> ignore +// ctx.Response.Body <- new MemoryStream() +// let expected = "subroute /foo" +// +// task { +// let! result = app next ctx +// +// match result with +// | None -> assertFailf "Result was expected to be %s" expected +// | Some ctx -> Assert.Equal(expected, getBody ctx) +// } +// +// [] +// let ``subRouteCi: Nested route after subRouteCi is called`` () = +// let ctx = Substitute.For() +// mockJson ctx (Newtonsoft None) +// let app = +// GET >=> choose [ +// subRouteCi "/foo" ( +// route "/bar" >=> text "subroute /foo/bar") +// setStatusCode 404 >=> text "Not found" ] +// +// ctx.Items.Returns (new Dictionary() :> IDictionary) |> ignore +// ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore +// ctx.Request.Path.ReturnsForAnyArgs (PathString("/FOO/bar")) |> ignore +// ctx.Response.Body <- new MemoryStream() +// let expected = "subroute /foo/bar" +// +// task { +// let! result = app next ctx +// +// match result with +// | None -> assertFailf "Result was expected to be %s" expected +// | Some ctx -> Assert.Equal(expected, getBody ctx) +// } +// +// [] +// let ``subRouteCi: Nested route after subRouteCi is still case sensitive`` () = +// let ctx = Substitute.For() +// mockJson ctx (Newtonsoft None) +// let app = +// GET >=> choose [ +// subRouteCi "/foo" ( +// choose [ +// route "/bar" >=> text "subroute /foo/bar" +// setStatusCode 404 >=> text "Not found - nested" +// ]) +// setStatusCode 404 >=> text "Not found" ] +// +// ctx.Items.Returns (new Dictionary() :> IDictionary) |> ignore +// ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore +// ctx.Request.Path.ReturnsForAnyArgs (PathString("/FOO/BAR")) |> ignore +// ctx.Response.Body <- new MemoryStream() +// let expected = "Not found - nested" +// +// task { +// let! result = app next ctx +// +// match result with +// | None -> assertFailf "Result was expected to be %s" expected +// | Some ctx -> Assert.Equal(expected, getBody ctx) +// } +// +// [] +// let ``subRouteCi: Nested routeCi after subRouteCi is called`` () = +// let ctx = Substitute.For() +// mockJson ctx (Newtonsoft None) +// let app = +// GET >=> choose [ +// subRouteCi "/foo" ( +// routeCi "/bar" >=> text "subroute /foo/bar") +// setStatusCode 404 >=> text "Not found" ] +// +// ctx.Items.Returns (new Dictionary() :> IDictionary) |> ignore +// ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore +// ctx.Request.Path.ReturnsForAnyArgs (PathString("/FOO/BAR")) |> ignore +// ctx.Response.Body <- new MemoryStream() +// let expected = "subroute /foo/bar" +// +// task { +// let! result = app next ctx +// +// match result with +// | None -> assertFailf "Result was expected to be %s" expected +// | Some ctx -> Assert.Equal(expected, getBody ctx) +// } diff --git a/examples/giraffe/test/Util.fs b/examples/giraffe/test/Util.fs new file mode 100644 index 0000000..52003e1 --- /dev/null +++ b/examples/giraffe/test/Util.fs @@ -0,0 +1,36 @@ +module Fable.Python.Tests.Util + +module Testing = +#if FABLE_COMPILER + open Fable.Core + open Fable.Core.PyInterop + + /// Fable version of Xunit Assert + type Assert = + [] + static member Equal(actual: 'T, expected: 'T, ?msg: string) : unit = nativeOnly + + [] + static member NotEqual(actual: 'T, expected: 'T, ?msg: string) : unit = nativeOnly + + let equal expected actual : unit = Assert.Equal(actual, expected) + let notEqual expected actual : unit = Assert.NotEqual(actual, expected) + + type Fact () = + inherit System.Attribute () +#else + open Xunit + type FactAttribute = Xunit.FactAttribute + + let equal<'T> (expected: 'T) (actual: 'T) : unit = Assert.Equal(expected, actual) + let notEqual<'T> (expected: 'T) (actual: 'T) : unit = Assert.NotEqual(expected, actual) + + type Assert = Xunit.Assert +#endif + let rec sumFirstSeq (zs: seq) (n: int) : float = + match n with + | 0 -> 0. + | 1 -> Seq.head zs + | _ -> + (Seq.head zs) + + sumFirstSeq (Seq.skip 1 zs) (n - 1)