├── tools ├── paket.references └── tools.csproj ├── .paket ├── paket.exe ├── paket.exe.config ├── paket.targets └── Paket.Restore.targets ├── sample ├── paket.references ├── App.fsproj └── Program.fs ├── src └── Giraffe.JsonTherapy │ ├── paket.references │ ├── AssemblyInfo.fs │ ├── Giraffe.JsonTherapy.fsproj │ └── JsonTherapy.fs ├── tests └── Giraffe.JsonTherapy.Tests │ ├── paket.references │ ├── Giraffe.JsonTherapy.Tests.fsproj │ ├── Main.fs │ ├── AssemblyInfo.fs │ └── Tests.fs ├── RELEASE_NOTES.md ├── appveyor.yml ├── .config └── dotnet-tools.json ├── .travis.yml ├── .editorconfig ├── paket.dependencies ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── LICENSE.md ├── .vscode └── launch.json ├── Giraffe.JsonTherapy.sln ├── .gitignore └── README.md /tools/paket.references: -------------------------------------------------------------------------------- 1 | dotnet-sourcelink 2 | -------------------------------------------------------------------------------- /.paket/paket.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zaid-Ajaj/Giraffe.JsonTherapy/HEAD/.paket/paket.exe -------------------------------------------------------------------------------- /sample/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core 2 | Giraffe 3 | Microsoft.AspNetCore.Server.Kestrel 4 | Newtonsoft.Json -------------------------------------------------------------------------------- /src/Giraffe.JsonTherapy/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core 2 | SourceLink.Create.CommandLine 3 | Giraffe 4 | Newtonsoft.Json 5 | -------------------------------------------------------------------------------- /tests/Giraffe.JsonTherapy.Tests/paket.references: -------------------------------------------------------------------------------- 1 | Expecto 2 | FSharp.Core 3 | dotnet-mono 4 | Microsoft.AspNetCore.Http 5 | Microsoft.AspNetCore.TestHost -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | #### 2.0.0 - 2021-06-03 2 | * Update to Giraffe v5 and net5 3 | 4 | #### 1.1.0 - 2019-01-28 5 | * Update nuget description 6 | 7 | #### 1.0.0 - 2019-01-28 8 | * Initial release 9 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | init: 2 | - git config --global core.autocrlf input 3 | build_script: 4 | - cmd: build.cmd 5 | test: off 6 | version: 0.0.1.{build} 7 | artifacts: 8 | - path: bin 9 | name: bin 10 | -------------------------------------------------------------------------------- /.paket/paket.exe.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tools/tools.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "paket": { 6 | "version": "6.0.0-beta4", 7 | "commands": [ 8 | "paket" 9 | ] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /sample/App.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net5.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | sudo: required 3 | dist: trusty 4 | 5 | dotnet: 2.1.300 6 | mono: 7 | - 5.4.1 8 | - latest # => "stable release" 9 | - alpha 10 | - beta 11 | - weekly # => "latest commits" 12 | os: 13 | - linux 14 | 15 | addons: 16 | apt: 17 | packages: 18 | - dotnet-sharedframework-microsoft.netcore.app-1.1.2 19 | 20 | script: 21 | - ./build.sh 22 | 23 | matrix: 24 | fast_finish: true 25 | allow_failures: 26 | - mono: latest 27 | - mono: alpha 28 | - mono: beta 29 | - mono: weekly 30 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: 2 | http://EditorConfig.org 3 | 4 | # top-most EditorConfig file 5 | root = true 6 | 7 | # Default settings: 8 | # A newline ending every file 9 | # Use 4 spaces as indentation 10 | [*] 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.{fs,fsi,fsx,config}] 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | 19 | [paket.*] 20 | trim_trailing_whitespace = true 21 | indent_size = 2 22 | 23 | [*.paket.references] 24 | trim_trailing_whitespace = true 25 | indent_size = 2 26 | -------------------------------------------------------------------------------- /tests/Giraffe.JsonTherapy.Tests/Giraffe.JsonTherapy.Tests.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net5.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /paket.dependencies: -------------------------------------------------------------------------------- 1 | source https://api.nuget.org/v3/index.json 2 | storage: none 3 | clitool dotnet-mono 0.5.4 4 | clitool dotnet-sourcelink 2.8.0 5 | nuget Argu 6 | nuget FSharp.Core 7 | nuget Expecto 8 | nuget Serilog 9 | nuget Serilog.Sinks.Console 10 | nuget Serilog.Sinks.TestCorrelator 11 | nuget SourceLink.Create.CommandLine 2.8.0 copy_local: true 12 | nuget Giraffe 13 | nuget Microsoft.AspNetCore.Server.Kestrel 14 | nuget Microsoft.AspNetCore.Hosting 15 | nuget Microsoft.AspNetCore.TestHost 16 | nuget Microsoft.AspNetCore.Http 17 | nuget Newtonsoft.Json 18 | 19 | group Build 20 | framework: >= net45 21 | source https://api.nuget.org/v3/index.json 22 | nuget FAKE 23 | github fsharp/FAKE modules/Octokit/Octokit.fsx 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Please insert a description of your problem or question. 4 | 5 | ## Error messages, screenshots 6 | 7 | Please add any error logs or screenshots if available. 8 | 9 | ## Failing test, failing github repo, or reproduction steps 10 | 11 | Please add either a failing test, a github repo of the problem or detailed reproduction steps. 12 | 13 | ## Expected Behavior 14 | 15 | Please define what you would expect the behavior to be like. 16 | 17 | ## Known workarounds 18 | 19 | Please provide a description of any known workarounds. 20 | 21 | ## Other information 22 | 23 | * Operating System: 24 | - [ ] windows [insert version here] 25 | - [ ] macOs [insert version] 26 | - [ ] linux [insert flavor/version here] 27 | * Platform 28 | - [ ] dotnet core 29 | - [ ] dotnet full 30 | - [ ] mono 31 | * Branch or release version: 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Giraffe.JsonTherapy/AssemblyInfo.fs: -------------------------------------------------------------------------------- 1 | // Auto-Generated by FAKE; do not edit 2 | namespace System 3 | open System.Reflection 4 | 5 | [] 6 | [] 7 | [] 8 | [] 9 | [] 10 | [] 11 | [] 12 | [] 13 | do () 14 | 15 | module internal AssemblyVersionInformation = 16 | let [] AssemblyTitle = "Giraffe.JsonTherapy" 17 | let [] AssemblyProduct = "Giraffe.JsonTherapy" 18 | let [] AssemblyVersion = "2.0.0" 19 | let [] AssemblyMetadata_ReleaseDate = "2021-06-03T00:00:00.0000000" 20 | let [] AssemblyFileVersion = "2.0.0" 21 | let [] AssemblyInformationalVersion = "2.0.0" 22 | let [] AssemblyMetadata_ReleaseChannel = "release" 23 | let [] AssemblyMetadata_GitHash = "4f637925b64e30adbf7250ffb793e87ae274b407" 24 | -------------------------------------------------------------------------------- /tests/Giraffe.JsonTherapy.Tests/Main.fs: -------------------------------------------------------------------------------- 1 | module ExpectoTemplate 2 | open Expecto 3 | open System.Reflection 4 | 5 | 6 | module AssemblyInfo = 7 | 8 | let metaDataValue (mda : AssemblyMetadataAttribute) = mda.Value 9 | let getMetaDataAttribute (assembly : Assembly) key = 10 | assembly.GetCustomAttributes(typedefof) 11 | |> Seq.cast 12 | |> Seq.find(fun x -> x.Key = key) 13 | 14 | let getReleaseDate assembly = 15 | "ReleaseDate" 16 | |> getMetaDataAttribute assembly 17 | |> metaDataValue 18 | 19 | let getGitHash assembly = 20 | "GitHash" 21 | |> getMetaDataAttribute assembly 22 | |> metaDataValue 23 | 24 | [] 25 | let main argv = 26 | if argv |> Seq.contains ("--version") then 27 | let assembly = Assembly.GetEntryAssembly() 28 | let name = assembly.GetName() 29 | let version = assembly.GetName().Version 30 | let releaseDate = AssemblyInfo.getReleaseDate assembly 31 | let githash = AssemblyInfo.getGitHash assembly 32 | printfn "%s - %A - %s - %s" name.Name version releaseDate githash 33 | 34 | Tests.runTestsInAssembly defaultConfig argv -------------------------------------------------------------------------------- /sample/Program.fs: -------------------------------------------------------------------------------- 1 | open System 2 | open System.Threading.Tasks 3 | open Giraffe 4 | open Giraffe.JsonTherapy 5 | open FSharp.Control.Tasks 6 | open Microsoft.AspNetCore.Http 7 | open Microsoft.AspNetCore.Hosting 8 | open Microsoft.AspNetCore.Builder 9 | open Microsoft.AspNetCore.Server.Kestrel 10 | open Microsoft.Extensions.Logging 11 | open Microsoft.Extensions.DependencyInjection 12 | 13 | type Maybe<'a> = 14 | | Nothing 15 | | Just of 'a 16 | 17 | type Rec = { First: string; Job: Maybe } 18 | 19 | let webApp : HttpHandler = 20 | choose [ GET >=> route "/index" >=> text "Index" 21 | POST >=> route "/echo" >=> text "Echo" ] 22 | 23 | type Startup() = 24 | member __.ConfigureServices (services : IServiceCollection) = 25 | // Register default Giraffe dependencies 26 | services.AddGiraffe() |> ignore 27 | 28 | member __.Configure (app : IApplicationBuilder) 29 | (env : IHostingEnvironment) 30 | (loggerFactory : ILoggerFactory) = 31 | // Add Giraffe to the ASP.NET Core pipeline 32 | app.UseGiraffe webApp 33 | 34 | [] 35 | let main _ = 36 | WebHostBuilder() 37 | .UseKestrel() 38 | .UseStartup() 39 | .Build() 40 | .Run() 41 | 0 42 | -------------------------------------------------------------------------------- /tests/Giraffe.JsonTherapy.Tests/AssemblyInfo.fs: -------------------------------------------------------------------------------- 1 | // Auto-Generated by FAKE; do not edit 2 | namespace System 3 | open System.Reflection 4 | 5 | [] 6 | [] 7 | [] 8 | [] 9 | [] 10 | [] 11 | [] 12 | [] 13 | do () 14 | 15 | module internal AssemblyVersionInformation = 16 | let [] AssemblyTitle = "Giraffe.JsonTherapy.Tests" 17 | let [] AssemblyProduct = "Giraffe.JsonTherapy" 18 | let [] AssemblyVersion = "2.0.0" 19 | let [] AssemblyMetadata_ReleaseDate = "2021-06-03T00:00:00.0000000" 20 | let [] AssemblyFileVersion = "2.0.0" 21 | let [] AssemblyInformationalVersion = "2.0.0" 22 | let [] AssemblyMetadata_ReleaseChannel = "release" 23 | let [] AssemblyMetadata_GitHash = "4f637925b64e30adbf7250ffb793e87ae274b407" 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Proposed Changes 2 | 3 | Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. 4 | 5 | ## Types of changes 6 | 7 | What types of changes does your code introduce to suave_serilog? 8 | _Put an `x` in the boxes that apply_ 9 | 10 | - [ ] Bugfix (non-breaking change which fixes an issue) 11 | - [ ] New feature (non-breaking change which adds functionality) 12 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 13 | 14 | 15 | ## Checklist 16 | 17 | _Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ 18 | 19 | - [ ] Build and tests pass locally 20 | - [ ] I have added tests that prove my fix is effective or that my feature works (if appropriate) 21 | - [ ] I have added necessary documentation (if appropriate) 22 | 23 | ## Further comments 24 | 25 | If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... 26 | -------------------------------------------------------------------------------- /src/Giraffe.JsonTherapy/Giraffe.JsonTherapy.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | true 6 | 7 | 8 | Giraffe.JsonTherapy 9 | Alternative JSON model binding mechanism for Giraffe apps without definting intermediate types 10 | f#, fsharp, giraffe, json 11 | https://github.com/zaid-ajaj/Giraffe.JsonTherapy 12 | https://github.com/zaid-ajaj/Giraffe.JsonTherapy/blob/master/LICENSE 13 | false 14 | git 15 | Zaid Ajaj 16 | https://github.com/zaid-ajaj/Giraffe.JsonTherapy 17 | 18 | 19 | true 20 | true 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "program": "${workspaceFolder}/tests/Suave.Serilog.Tests/bin/Debug/netstandard2.0/Suave.Serilog.Tests.dll", 12 | "args": [], 13 | "cwd": "${workspaceFolder}", 14 | "console": "internalConsole", 15 | "stopAtEntry": false, 16 | "internalConsoleOptions": "openOnSessionStart" 17 | }, 18 | { 19 | "name": ".NET Core Launch (web)", 20 | "type": "coreclr", 21 | "request": "launch", 22 | "preLaunchTask": "build", 23 | "program": "${workspaceFolder}/bin/Debug//.dll", 24 | "args": [], 25 | "cwd": "${workspaceFolder}", 26 | "stopAtEntry": false, 27 | "internalConsoleOptions": "openOnSessionStart", 28 | "launchBrowser": { 29 | "enabled": true, 30 | "args": "${auto-detect-url}", 31 | "windows": { 32 | "command": "cmd.exe", 33 | "args": "/C start ${auto-detect-url}" 34 | }, 35 | "osx": { 36 | "command": "open" 37 | }, 38 | "linux": { 39 | "command": "xdg-open" 40 | } 41 | }, 42 | "env": { 43 | "ASPNETCORE_ENVIRONMENT": "Development" 44 | }, 45 | "sourceFileMap": { 46 | "/Views": "${workspaceFolder}/Views" 47 | } 48 | }, 49 | { 50 | "name": ".NET Core Attach", 51 | "type": "coreclr", 52 | "request": "attach", 53 | "processId": "${command:pickProcess}" 54 | } 55 | ] 56 | } -------------------------------------------------------------------------------- /.paket/paket.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | true 7 | $(MSBuildThisFileDirectory) 8 | $(MSBuildThisFileDirectory)..\ 9 | /Library/Frameworks/Mono.framework/Commands/mono 10 | mono 11 | 12 | 13 | 14 | 15 | $(PaketRootPath)paket.exe 16 | $(PaketToolsPath)paket.exe 17 | "$(PaketExePath)" 18 | $(MonoPath) --runtime=v4.0.30319 "$(PaketExePath)" 19 | 20 | 21 | 22 | 23 | 24 | $(MSBuildProjectFullPath).paket.references 25 | 26 | 27 | 28 | 29 | $(MSBuildProjectDirectory)\$(MSBuildProjectName).paket.references 30 | 31 | 32 | 33 | 34 | $(MSBuildProjectDirectory)\paket.references 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | $(MSBuildProjectDirectory)\$(MSBuildProjectName).paket.references 47 | $(MSBuildProjectDirectory)\paket.references 48 | $(MSBuildStartupDirectory)\paket.references 49 | $(MSBuildProjectFullPath).paket.references 50 | $(PaketCommand) restore --references-files "$(PaketReferences)" 51 | 52 | RestorePackages; $(BuildDependsOn); 53 | 54 | 55 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /Giraffe.JsonTherapy.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio 15 3 | VisualStudioVersion = 15.0.26124.0 4 | MinimumVisualStudioVersion = 15.0.26124.0 5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C397A34C-84F1-49E7-AEBC-2F9F2B196216}" 6 | EndProject 7 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Giraffe.JsonTherapy", "src\Giraffe.JsonTherapy\Giraffe.JsonTherapy.fsproj", "{5D30E174-2538-47AC-8443-318C8C5DC2C9}" 8 | EndProject 9 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{ACBEE43C-7A88-4FB1-9B06-DB064D22B29F}" 10 | EndProject 11 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Giraffe.JsonTherapy.Tests", "tests\Giraffe.JsonTherapy.Tests\Giraffe.JsonTherapy.Tests.fsproj", "{1CA2E092-2320-451D-A4F0-9ED7C7C528CA}" 12 | EndProject 13 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "App", "sample\App.fsproj", "{F0C5FD47-AEFF-44A8-BB24-9F38A3CECCB9}" 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Debug|x64 = Debug|x64 19 | Debug|x86 = Debug|x86 20 | Release|Any CPU = Release|Any CPU 21 | Release|x64 = Release|x64 22 | Release|x86 = Release|x86 23 | EndGlobalSection 24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 25 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|x64.ActiveCfg = Debug|Any CPU 28 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|x64.Build.0 = Debug|Any CPU 29 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|x86.ActiveCfg = Debug|Any CPU 30 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Debug|x86.Build.0 = Debug|Any CPU 31 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|x64.ActiveCfg = Release|Any CPU 34 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|x64.Build.0 = Release|Any CPU 35 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|x86.ActiveCfg = Release|Any CPU 36 | {5D30E174-2538-47AC-8443-318C8C5DC2C9}.Release|x86.Build.0 = Release|Any CPU 37 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|x64.ActiveCfg = Debug|Any CPU 40 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|x64.Build.0 = Debug|Any CPU 41 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|x86.ActiveCfg = Debug|Any CPU 42 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Debug|x86.Build.0 = Debug|Any CPU 43 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|x64.ActiveCfg = Release|Any CPU 46 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|x64.Build.0 = Release|Any CPU 47 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|x86.ActiveCfg = Release|Any CPU 48 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA}.Release|x86.Build.0 = Release|Any CPU 49 | {F0C5FD47-AEFF-44A8-BB24-9F38A3CECCB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {F0C5FD47-AEFF-44A8-BB24-9F38A3CECCB9}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {F0C5FD47-AEFF-44A8-BB24-9F38A3CECCB9}.Debug|x64.ActiveCfg = Debug|Any CPU 52 | {F0C5FD47-AEFF-44A8-BB24-9F38A3CECCB9}.Debug|x64.Build.0 = Debug|Any CPU 53 | {F0C5FD47-AEFF-44A8-BB24-9F38A3CECCB9}.Debug|x86.ActiveCfg = Debug|Any CPU 54 | {F0C5FD47-AEFF-44A8-BB24-9F38A3CECCB9}.Debug|x86.Build.0 = Debug|Any CPU 55 | {F0C5FD47-AEFF-44A8-BB24-9F38A3CECCB9}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {F0C5FD47-AEFF-44A8-BB24-9F38A3CECCB9}.Release|Any CPU.Build.0 = Release|Any CPU 57 | {F0C5FD47-AEFF-44A8-BB24-9F38A3CECCB9}.Release|x64.ActiveCfg = Release|Any CPU 58 | {F0C5FD47-AEFF-44A8-BB24-9F38A3CECCB9}.Release|x64.Build.0 = Release|Any CPU 59 | {F0C5FD47-AEFF-44A8-BB24-9F38A3CECCB9}.Release|x86.ActiveCfg = Release|Any CPU 60 | {F0C5FD47-AEFF-44A8-BB24-9F38A3CECCB9}.Release|x86.Build.0 = Release|Any CPU 61 | EndGlobalSection 62 | GlobalSection(SolutionProperties) = preSolution 63 | HideSolutionNode = FALSE 64 | EndGlobalSection 65 | GlobalSection(NestedProjects) = preSolution 66 | {5D30E174-2538-47AC-8443-318C8C5DC2C9} = {C397A34C-84F1-49E7-AEBC-2F9F2B196216} 67 | {1CA2E092-2320-451D-A4F0-9ED7C7C528CA} = {ACBEE43C-7A88-4FB1-9B06-DB064D22B29F} 68 | EndGlobalSection 69 | GlobalSection(ExtensibilityGlobals) = postSolution 70 | SolutionGuid = {C9C98179-6748-4A52-B335-9E34076E5958} 71 | EndGlobalSection 72 | EndGlobal 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | *.VC.VC.opendb 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | # TODO: Comment the next line if you want to checkin your web deploy settings 144 | # but database connection strings (with potential passwords) will be unencrypted 145 | *.pubxml 146 | *.publishproj 147 | 148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 149 | # checkin your Azure Web App publish settings, but sensitive information contained 150 | # in these scripts will be unencrypted 151 | PublishScripts/ 152 | 153 | # NuGet Packages 154 | *.nupkg 155 | packages/ 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.pfx 193 | *.publishsettings 194 | node_modules/ 195 | orleans.codegen.cs 196 | 197 | # Since there are multiple workflows, uncomment next line to ignore bower_components 198 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 199 | #bower_components/ 200 | 201 | # RIA/Silverlight projects 202 | Generated_Code/ 203 | 204 | # Backup & report files from converting an old project file 205 | # to a newer Visual Studio version. Backup files are not needed, 206 | # because we have git ;-) 207 | _UpgradeReport_Files/ 208 | Backup*/ 209 | UpgradeLog*.XML 210 | UpgradeLog*.htm 211 | 212 | # SQL Server files 213 | *.mdf 214 | *.ldf 215 | 216 | # Business Intelligence projects 217 | *.rdl.data 218 | *.bim.layout 219 | *.bim_*.settings 220 | 221 | # Microsoft Fakes 222 | FakesAssemblies/ 223 | 224 | # GhostDoc plugin setting file 225 | *.GhostDoc.xml 226 | 227 | # Node.js Tools for Visual Studio 228 | .ntvs_analysis.dat 229 | 230 | # Visual Studio 6 build log 231 | *.plg 232 | 233 | # Visual Studio 6 workspace options file 234 | *.opt 235 | 236 | # Visual Studio LightSwitch build output 237 | **/*.HTMLClient/GeneratedArtifacts 238 | **/*.DesktopClient/GeneratedArtifacts 239 | **/*.DesktopClient/ModelManifest.xml 240 | **/*.Server/GeneratedArtifacts 241 | **/*.Server/ModelManifest.xml 242 | _Pvt_Extensions 243 | 244 | # Paket dependency manager 245 | paket-files/ 246 | 247 | # FAKE - F# Make 248 | .fake/ 249 | 250 | # JetBrains Rider 251 | .idea/ 252 | *.sln.iml 253 | 254 | TestResults.xml 255 | 256 | dist/ 257 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Giraffe.JsonTherapy [![Build Status](https://travis-ci.org/Zaid-Ajaj/Giraffe.JsonTherapy.svg?branch=master)](https://travis-ci.org/Zaid-Ajaj/Giraffe.JsonTherapy) [![Nuget](https://img.shields.io/nuget/v/Giraffe.JsonTherapy.svg?colorB=green)](https://www.nuget.org/packages/Giraffe.JsonTherapy) 2 | 3 | Simply extract JSON values from HTTP requests for [Giraffe](https://github.com/giraffe-fsharp/Giraffe) in a type-safe manner without defining intermediate types or decoders. Many times the input JSON is so simple that you just want to get the values so this library is great for rapid developement! 4 | 5 | # Install 6 | ```bash 7 | # using nuget client 8 | dotnet add package Giraffe.JsonTherapy 9 | # using Paket 10 | .paket/paket.exe add Giraffe.JsonTherapy --project path/to/Your.fsproj 11 | ``` 12 | 13 | The library code is actually only a single-file: `Jsontherapy.fs` so you can add it manually to your project and modify however you want. 14 | 15 | ## Basic Usage 16 | 17 | Namespace `Giraffe.JsonTherapy` is opened in all examples below 18 | 19 | The library has two functions: 20 | - `Json.parts` 21 | - `Json.manyParts` 22 | 23 | `Json.parts` allows you to specify the path of the JSON properties and use them directly. For example, given the following JSON for a Todo item: 24 | ```json 25 | { 26 | "id": 1, 27 | "description": "Learn F#", 28 | "complete": true 29 | } 30 | ``` 31 | where both `description` and `complete` are *optional*. You want to update a an existing Todo so you can read the *values* directly as follows: 32 | ```fs 33 | let webApp = 34 | PUT 35 | >=> route "/todo/update" 36 | >=> Json.parts("id", "description", "complete", 37 | // id: int -> required 38 | // desc: string option -> optional 39 | // complete: bool option -> optional 40 | fun id desc complete -> 41 | match desc, complete with 42 | // input JSON => { id, desc, complete } 43 | | Some desc, Some complete -> updateDescAndComplete id desc complete 44 | // input JSON => { id, complete } 45 | | None, Some complete -> updateJustComplete id complete 46 | // input JSON => { id, desc } 47 | | Some desc, None -> updateJustDescription id desc 48 | // input JSON => { id } 49 | | None, None -> setStatusCode 400 >=> text "Nothing to update" 50 | ``` 51 | the first arguments (up to 8, the function is overloaded) of `Json.parts` are the names/paths of the JSON properties that you want to read. The last argument is a function that returns a `HttpHandler`. 52 | 53 | Because `desc` and `complete` has been infered (from usage) as optionals, then they can be omitted from the JSON whereas the `id` is infered to be `int` which means it is required to be in the input JSON: 54 | 55 | ```bash 56 | PUT /todo/update 57 | 58 | { } 59 | # 400 Bad Request { "message": "cannot find value at path 'id' in the JSON" } 60 | 61 | { id: 1 } 62 | # 400 Bad Request "Nothing to update" 63 | 64 | { id: 1, description: "updated description" } 65 | # 200 OK "Updating just description" 66 | 67 | { id: 1, complete: false } 68 | # 200 OK "Updating just complete" 69 | 70 | { id: 1, complete: false, description: "updated description" } 71 | # 200 OK "Updating both description and complete" 72 | ``` 73 | 74 | ## Nested properties 75 | 76 | `Json.parts` can read nested properties by the path of the nested JSON property that you want to read. Suppose you have this JSON: 77 | ```json 78 | { 79 | "player": { 80 | "id": 1, 81 | "name": "john" 82 | }, 83 | "role": "goal keeper" 84 | } 85 | ``` 86 | You can read all the values as follows: 87 | ```fs 88 | POST 89 | >=> route "/player" 90 | >=> Json.parts("player.id", "player.name", "role", 91 | fun id name role -> 92 | text (sprintf "Player(%s, %d) is a %s" name id role)) 93 | ``` 94 | then call the end point: 95 | ```bash 96 | POST /player 97 | { "player": { "id": 1, "name": "john" }, "role": "goal keeper" } 98 | 99 | # 200 OK "Player(john, 1) is a goal keeper 100 | ``` 101 | 102 | ## Arrays of objects 103 | Using `Json.manyParts` you extract the properties from objects in an array of such object in JSON, for example: 104 | ```json 105 | [ 106 | { "product": "apple", "price": 1.50 }, 107 | { "product": "banana", "price": 3.45 } 108 | ] 109 | ``` 110 | You can read the parts as follows: 111 | ```fs 112 | POST 113 | >=> route "/total-price" 114 | >=> Json.manyParts("price", 115 | fun prices -> 116 | prices 117 | |> List.sum 118 | |> sprintf "%.2f" 119 | |> text) 120 | ``` 121 | The last argument of `Json.manyParts` is a function that takes in a list of the extracted values from the input JSON value, in the last example we had one value extracted so the value `prices` was infered to be of type `float list`. 122 | 123 | ## Multiple parts from arrays of objects 124 | You can also extract multiple parts from the array objects and map them as a tuple in the last argument: 125 | ```fs 126 | POST 127 | >=> route "/product-names" 128 | >=> Json.manyParts("product", "price", 129 | fun products -> 130 | products 131 | |> List.map (fun (name, price) -> name) 132 | |> String.concat ", " 133 | |> text) 134 | ``` 135 | then 136 | ```bash 137 | POST /product-names 138 | [ 139 | { "product": "apple", "price": 1.50 }, 140 | { "product": "banana", "price": 3.45 } 141 | ] 142 | 143 | # OK 20O "apple, banana" 144 | ``` 145 | 146 | See the tests for examples, you can also open an issue if you have questions. Missing something? PR's are very much welcome! 147 | 148 | ## Builds 149 | 150 | ![Build History](https://buildstats.info/travisci/chart/Zaid-Ajaj/Giraffe.JsonTherapy) 151 | 152 | 153 | ### Building 154 | 155 | 156 | Make sure the following **requirements** are installed in your system: 157 | 158 | * [dotnet SDK](https://www.microsoft.com/net/download/core) 2.0 or higher 159 | * [Mono](http://www.mono-project.com/) if you're on Linux or macOS. 160 | 161 | ``` 162 | > build.cmd // on windows 163 | $ ./build.sh // on unix 164 | ``` 165 | 166 | ### Watch Tests 167 | 168 | The `WatchTests` target will use [dotnet-watch](https://github.com/aspnet/Docs/blob/master/aspnetcore/tutorials/dotnet-watch.md) to watch for changes in your lib or tests and re-run your tests on all `TargetFrameworks` 169 | 170 | ``` 171 | ./build.sh WatchTests 172 | ``` 173 | -------------------------------------------------------------------------------- /tests/Giraffe.JsonTherapy.Tests/Tests.fs: -------------------------------------------------------------------------------- 1 | module Tests 2 | 3 | open Expecto 4 | open Giraffe 5 | open Giraffe.JsonTherapy 6 | open System 7 | open System.IO 8 | open System.Linq 9 | open System.Net.Http 10 | open System.Collections.Generic 11 | open Microsoft.AspNetCore.Hosting 12 | open Microsoft.AspNetCore.TestHost 13 | open Microsoft.AspNetCore.Builder 14 | open Microsoft.Extensions.DependencyInjection 15 | open System.Net 16 | 17 | let updateDescription (id: int) (desc: string) = 18 | text "Updating just the description" 19 | 20 | let updateComplete (id: int) (complete: bool) = 21 | text "updating todo complete" 22 | 23 | let updateDescAndComplete (id: int) (desc: string) (complete: bool) = 24 | text "updating description and complete" 25 | 26 | let testWebApp : HttpHandler = 27 | choose [ 28 | GET >=> route "/" >=> text "Index" 29 | PUT 30 | >=> route "/todo/update" 31 | >=> Json.parts("id", "description", "complete", 32 | // id: int -> required 33 | // desc: string option -> optional 34 | // complete: bool option -> optional 35 | fun id desc complete -> 36 | match desc, complete with 37 | // input JSON => { id, desc, complete } 38 | | Some desc, Some complete -> updateDescAndComplete id desc complete 39 | // input JSON => { id, complete } 40 | | None, Some complete -> updateComplete id complete 41 | // input JSON => { id, desc } 42 | | Some desc, None -> updateDescription id desc 43 | // input JSON => { id } 44 | | None, None -> setStatusCode 400 >=> text "Nothing to update" 45 | ) 46 | 47 | setStatusCode 404 >=> text "Not Found" 48 | ] 49 | 50 | let pass() = Expect.isTrue true "Passed" 51 | let fail() = Expect.isTrue false "Failed" 52 | 53 | let rnd = System.Random() 54 | 55 | let appBuilder (app: IApplicationBuilder) = 56 | app.UseGiraffe testWebApp 57 | 58 | let configureServices (services: IServiceCollection) = 59 | services.AddGiraffe() 60 | |> ignore 61 | 62 | let createHost() = 63 | WebHostBuilder() 64 | .UseContentRoot(Directory.GetCurrentDirectory()) 65 | .Configure(Action (appBuilder)) 66 | .ConfigureServices(Action configureServices) 67 | 68 | let withClientFor (webApp: HttpHandler) (map: HttpClient -> unit) = 69 | let host = 70 | WebHostBuilder() 71 | .UseContentRoot(Directory.GetCurrentDirectory()) 72 | .Configure(Action (fun app -> app.UseGiraffe webApp)) 73 | .ConfigureServices(Action configureServices) 74 | 75 | use server = new TestServer(host) 76 | use client = server.CreateClient() 77 | map client 78 | 79 | let runTask task = 80 | task 81 | |> Async.AwaitTask 82 | |> Async.RunSynchronously 83 | 84 | let httpGet (path : string) (client : HttpClient) = 85 | path 86 | |> client.GetAsync 87 | |> runTask 88 | 89 | let put (path: string) (content: string) (client: HttpClient) = 90 | let httpContent = new StringContent(content) 91 | client.PutAsync(path, httpContent) 92 | |> runTask 93 | 94 | let post (path: string) (content: string) (client: HttpClient) = 95 | let httpContent = new StringContent(content) 96 | client.PostAsync(path, httpContent) 97 | |> runTask 98 | 99 | let isStatus (code : HttpStatusCode) (response : HttpResponseMessage) = 100 | Expect.equal response.StatusCode code "Status code is wrong" 101 | response 102 | 103 | let ensureSuccess (response : HttpResponseMessage) = 104 | if not response.IsSuccessStatusCode 105 | then response.Content.ReadAsStringAsync() |> runTask |> failwithf "%A" 106 | else response 107 | 108 | let readText (response : HttpResponseMessage) = 109 | response.Content.ReadAsStringAsync() 110 | |> runTask 111 | 112 | let readTextEqual content (response : HttpResponseMessage) = 113 | response.Content.ReadAsStringAsync() 114 | |> runTask 115 | |> fun result -> Expect.equal result content "The expected and actual response content are not equal" 116 | 117 | 118 | [] 119 | let tests = 120 | testList "Giraffe.JsonTherapy" [ 121 | testCase "Nested properties can be extracted from JSON" <| fun _ -> 122 | let inputJson = """ { "player": { "id": 1, "name": "john" } } """ 123 | let playerId = "player.id" 124 | let playerName = "player.name" 125 | let json = Extensions.parse inputJson 126 | match Extensions.readPath (Extensions.getJPath playerId) json with 127 | | Some (JNumber 1.0) -> pass() 128 | | otherwise -> fail() 129 | 130 | match Extensions.readPath (Extensions.getJPath playerName) json with 131 | | Some (JString "john") -> pass() 132 | | otherwise -> fail() 133 | 134 | testCase "Root path / returns 'Index' as text" <| fun _ -> 135 | use server = new TestServer(createHost()) 136 | use client = server.CreateClient() 137 | 138 | client 139 | |> httpGet "/" 140 | |> isStatus HttpStatusCode.OK 141 | |> readTextEqual "Index" 142 | 143 | testCase "Unknown path returns status 404 not found " <| fun _ -> 144 | use server = new TestServer(createHost()) 145 | use client = server.CreateClient() 146 | 147 | client 148 | |> httpGet "/non-existent-path" 149 | |> isStatus HttpStatusCode.NotFound 150 | |> readTextEqual "Not Found" 151 | 152 | testCase "Basic use cases" <| fun _ -> 153 | use server = new TestServer(createHost()) 154 | use client = server.CreateClient() 155 | 156 | client 157 | |> put "/todo/update" "{ \"id\": 1 }" 158 | |> isStatus HttpStatusCode.BadRequest 159 | |> readTextEqual "Nothing to update" 160 | 161 | client 162 | |> put "/todo/update" "{ \"id\": 1, \"description\": \"description\" }" 163 | |> ensureSuccess 164 | |> readTextEqual "Updating just the description" 165 | 166 | client 167 | |> put "/todo/update" "{ \"id\": 1, \"complete\": true }" 168 | |> ensureSuccess 169 | |> readTextEqual "updating todo complete" 170 | 171 | client 172 | |> put "/todo/update" "{ \"id\": 1, \"complete\": true, \"description\": \"description\" }" 173 | |> ensureSuccess 174 | |> readTextEqual "updating description and complete" 175 | 176 | testCase "Nested properties can be extracted" <| fun _ -> 177 | let webApp = 178 | POST 179 | >=> route "/extract" 180 | >=> Json.parts("player.id", "player.name", "role", 181 | fun id name role -> text (sprintf "Player(%d, %s) is a %s" id name role)) 182 | 183 | withClientFor webApp <| fun client -> 184 | let inputJson = """ { "player": { "id": 1, "name": "john" }, "role": "goal keeper" } """ 185 | client 186 | |> post "/extract" inputJson 187 | |> readTextEqual "Player(1, john) is a goal keeper" 188 | 189 | testCase "Json.manyParts works for single paramters" <| fun _ -> 190 | let webApp = 191 | POST 192 | >=> route "/extract" 193 | >=> Json.manyParts("value", List.sum >> sprintf "%d" >> text) 194 | 195 | withClientFor webApp (post "/extract" "[{ \"value\": 10 }, { \"value\": 5 }]" >> readTextEqual "15") 196 | 197 | testCase "Json.manyParts works for single paramters with nested properties" <| fun _ -> 198 | let webApp = 199 | POST 200 | >=> route "/extract" 201 | >=> Json.manyParts("value.role", String.concat ", " >> text) 202 | 203 | withClientFor webApp (post "/extract" "[{ \"value\": { \"role\": \"admin\" } }, { \"value\": { \"role\": \"user\" } }]" >> readTextEqual "admin, user") 204 | 205 | testCase "Json.parts works with simple use cases" <| fun _ -> 206 | let webApp = 207 | POST 208 | >=> route "/extract" 209 | >=> Json.parts("value", "id", fun value id -> text (sprintf "Value(%s) = %d" value id)) 210 | 211 | withClientFor webApp (post "/extract" "{ \"value\": \"one\", \"id\": 1 }" >> readTextEqual "Value(one) = 1") 212 | 213 | testCase "Json.manyParts works with multi parameters" <| fun _ -> 214 | let webApp = 215 | POST 216 | >=> route "/extract" 217 | >=> Json.manyParts("value", "id", function 218 | | [ ("hello", 1); ("there", 2) ] -> text "pass" 219 | | otheriwse -> failwith "does not work") 220 | 221 | withClientFor webApp (post "/extract" "[{ \"value\": \"hello\", \"id\": 1 }, { \"value\": \"there\", \"id\": 2 }]" >> readTextEqual "pass") 222 | 223 | testCase "Reading floats works" <| fun _ -> 224 | let webApp = 225 | POST 226 | >=> route "/extract" 227 | >=> Json.manyParts("price", 228 | fun prices -> 229 | prices 230 | |> List.sum 231 | |> sprintf "%.2f" 232 | |> text) 233 | 234 | withClientFor webApp <| fun client -> 235 | client 236 | |> post "/extract" "[{\"price\":1.5 },{\"price\":3.5}]" 237 | |> readTextEqual "5.00" 238 | 239 | testCase "Null string in JSON becomes None" <| fun _ -> 240 | let webApp = 241 | POST 242 | >=> route "/extract" 243 | >=> Json.parts("name", 244 | function 245 | | None -> text "Name was null or missing" 246 | | Some name -> text name) 247 | 248 | withClientFor webApp (post "/extract" "{ \"name\": null }" >> readTextEqual "Name was null or missing") 249 | withClientFor webApp (post "/extract" "{ \"name\": \"non-empty\" }" >> readTextEqual "non-empty") 250 | withClientFor webApp (post "/extract" "{ }" >> readTextEqual "Name was null or missing" ) 251 | 252 | testCase "array of strings can be extracted" <| fun _ -> 253 | let webApp = Json.parts("roles", Seq.ofArray >> String.concat ", " >> text) 254 | withClientFor webApp (post "/" "{ \"roles\": [\"one\", \"two\"] }" >> readTextEqual "one, two") 255 | 256 | testCase "list of strings can be extracted" <| fun _ -> 257 | let webApp = Json.parts("roles", Seq.ofList >> String.concat ", " >> text) 258 | withClientFor webApp (post "/" "{ \"roles\": [\"one\", \"two\"] }" >> readTextEqual "one, two") 259 | 260 | testCase "list of ints can be extracted" <| fun _ -> 261 | let webApp = Json.parts("numbers", Seq.ofList >> Seq.sum >> sprintf "%d" >> text) 262 | withClientFor webApp (post "/" "{ \"numbers\": [1,2,3,4,5] }" >> readTextEqual "15") 263 | ] 264 | -------------------------------------------------------------------------------- /.paket/Paket.Restore.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 8 | 9 | $(MSBuildVersion) 10 | 15.0.0 11 | false 12 | true 13 | 14 | true 15 | $(MSBuildThisFileDirectory) 16 | $(MSBuildThisFileDirectory)..\ 17 | $(PaketRootPath)paket-files\paket.restore.cached 18 | $(PaketRootPath)paket.lock 19 | classic 20 | proj 21 | assembly 22 | native 23 | /Library/Frameworks/Mono.framework/Commands/mono 24 | mono 25 | 26 | 27 | $(PaketRootPath)paket.bootstrapper.exe 28 | $(PaketToolsPath)paket.bootstrapper.exe 29 | $([System.IO.Path]::GetDirectoryName("$(PaketBootStrapperExePath)"))\ 30 | 31 | "$(PaketBootStrapperExePath)" 32 | $(MonoPath) --runtime=v4.0.30319 "$(PaketBootStrapperExePath)" 33 | 34 | 35 | 36 | 37 | true 38 | true 39 | 40 | 41 | True 42 | 43 | 44 | False 45 | 46 | $(BaseIntermediateOutputPath.TrimEnd('\').TrimEnd('\/')) 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | $(PaketRootPath)paket 56 | $(PaketToolsPath)paket 57 | 58 | 59 | 60 | 61 | 62 | $(PaketRootPath)paket.exe 63 | $(PaketToolsPath)paket.exe 64 | 65 | 66 | 67 | 68 | 69 | <_DotnetToolsJson Condition="Exists('$(PaketRootPath)/.config/dotnet-tools.json')">$([System.IO.File]::ReadAllText("$(PaketRootPath)/.config/dotnet-tools.json")) 70 | <_ConfigContainsPaket Condition=" '$(_DotnetToolsJson)' != ''">$(_DotnetToolsJson.Contains('"paket"')) 71 | <_ConfigContainsPaket Condition=" '$(_ConfigContainsPaket)' == ''">false 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | <_PaketCommand>dotnet paket 83 | 84 | 85 | 86 | 87 | 88 | $(PaketToolsPath)paket 89 | $(PaketBootStrapperExeDir)paket 90 | 91 | 92 | paket 93 | 94 | 95 | 96 | 97 | <_PaketExeExtension>$([System.IO.Path]::GetExtension("$(PaketExePath)")) 98 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(_PaketExeExtension)' == '.dll' ">dotnet "$(PaketExePath)" 99 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(OS)' != 'Windows_NT' AND '$(_PaketExeExtension)' == '.exe' ">$(MonoPath) --runtime=v4.0.30319 "$(PaketExePath)" 100 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' ">"$(PaketExePath)" 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | true 122 | $(NoWarn);NU1603;NU1604;NU1605;NU1608 123 | false 124 | true 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | $([System.IO.File]::ReadAllText('$(PaketRestoreCacheFile)')) 134 | 135 | 136 | 137 | 138 | 139 | 141 | $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[0].Replace(`"`, ``).Replace(` `, ``)) 142 | $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[1].Replace(`"`, ``).Replace(` `, ``)) 143 | 144 | 145 | 146 | 147 | %(PaketRestoreCachedKeyValue.Value) 148 | %(PaketRestoreCachedKeyValue.Value) 149 | 150 | 151 | 152 | 153 | true 154 | false 155 | true 156 | 157 | 158 | 162 | 163 | true 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | $(PaketIntermediateOutputPath)\$(MSBuildProjectFile).paket.references.cached 183 | 184 | $(MSBuildProjectFullPath).paket.references 185 | 186 | $(MSBuildProjectDirectory)\$(MSBuildProjectName).paket.references 187 | 188 | $(MSBuildProjectDirectory)\paket.references 189 | 190 | false 191 | true 192 | true 193 | references-file-or-cache-not-found 194 | 195 | 196 | 197 | 198 | $([System.IO.File]::ReadAllText('$(PaketReferencesCachedFilePath)')) 199 | $([System.IO.File]::ReadAllText('$(PaketOriginalReferencesFilePath)')) 200 | references-file 201 | false 202 | 203 | 204 | 205 | 206 | false 207 | 208 | 209 | 210 | 211 | true 212 | target-framework '$(TargetFramework)' or '$(TargetFrameworks)' files @(PaketResolvedFilePaths) 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | false 224 | true 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',').Length) 236 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[0]) 237 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[1]) 238 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[4]) 239 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[5]) 240 | 241 | 242 | %(PaketReferencesFileLinesInfo.PackageVersion) 243 | All 244 | runtime 245 | runtime 246 | true 247 | true 248 | 249 | 250 | 251 | 252 | $(PaketIntermediateOutputPath)/$(MSBuildProjectFile).paket.clitools 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[0]) 262 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[1]) 263 | 264 | 265 | %(PaketCliToolFileLinesInfo.PackageVersion) 266 | 267 | 268 | 269 | 273 | 274 | 275 | 276 | 277 | 278 | false 279 | 280 | 281 | 282 | 283 | 284 | <_NuspecFilesNewLocation Include="$(PaketIntermediateOutputPath)\$(Configuration)\*.nuspec"/> 285 | 286 | 287 | 288 | 289 | 290 | $(MSBuildProjectDirectory)/$(MSBuildProjectFile) 291 | true 292 | false 293 | true 294 | false 295 | true 296 | false 297 | true 298 | false 299 | true 300 | $(PaketIntermediateOutputPath)\$(Configuration) 301 | $(PaketIntermediateOutputPath) 302 | 303 | 304 | 305 | <_NuspecFiles Include="$(AdjustedNuspecOutputPath)\*.$(PackageVersion.Split(`+`)[0]).nuspec"/> 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 363 | 364 | 407 | 408 | 450 | 451 | 492 | 493 | 494 | 495 | -------------------------------------------------------------------------------- /src/Giraffe.JsonTherapy/JsonTherapy.fs: -------------------------------------------------------------------------------- 1 | namespace Giraffe.JsonTherapy 2 | 3 | open System 4 | open Giraffe 5 | open Microsoft.AspNetCore.Http 6 | open Newtonsoft.Json.Linq 7 | open System.Collections.Generic 8 | open System.IO 9 | open Microsoft.Extensions.Primitives 10 | open System.Threading.Tasks 11 | open FSharp.Control.Tasks 12 | 13 | [] 14 | module Types = 15 | 16 | /// A type representing Javascript Object Notation 17 | type Json = 18 | | JNumber of float 19 | | JString of string 20 | | JBool of bool 21 | | JNull 22 | | JArray of Json list 23 | | JObject of Map 24 | 25 | [] 26 | module Extensions = 27 | open Newtonsoft.Json 28 | 29 | let internal request (requestMap : HttpRequest -> Task) : HttpHandler = 30 | fun (next : HttpFunc) (ctx : HttpContext) -> 31 | task { 32 | let! createdHandler = requestMap ctx.Request 33 | return! createdHandler next ctx 34 | } 35 | 36 | let internal isOption (typeInfo: Type) = typeInfo.FullName.StartsWith("Microsoft.FSharp.Core.FSharpOption`1") 37 | 38 | /// Parses the input string as structured JSON 39 | let parse (input: string) = 40 | let settings = JsonSerializerSettings(DateParseHandling = DateParseHandling.None) 41 | let token = JsonConvert.DeserializeObject(input, settings) 42 | let rec fromJToken (token: JToken) = 43 | match token.Type with 44 | | JTokenType.Float -> JNumber (token.Value()) 45 | | JTokenType.Integer -> JNumber (token.Value()) 46 | | JTokenType.Boolean -> JBool (token.Value()) 47 | | JTokenType.String -> JString (token.Value()) 48 | | JTokenType.Guid -> JString (token.Value().ToString()) 49 | | JTokenType.Null -> JNull 50 | | JTokenType.Array -> 51 | token.Values() 52 | |> Seq.map fromJToken 53 | |> List.ofSeq 54 | |> Json.JArray 55 | | JTokenType.Object -> 56 | token.Value>() 57 | |> Seq.map (fun pair -> pair.Key, fromJToken pair.Value) 58 | |> List.ofSeq 59 | |> Map.ofList 60 | |> Json.JObject 61 | | _ -> failwithf "JSON token type '%s' was not recognised" (token.Type.ToString()) 62 | 63 | fromJToken token 64 | 65 | let rec convertJToken = function 66 | | JNull -> JValue.CreateNull() :> JToken 67 | | JBool value -> JToken.op_Implicit(value) 68 | | JString value -> JToken.op_Implicit(value) 69 | | JNumber value -> JToken.op_Implicit(value) 70 | | JArray values -> 71 | let output = Newtonsoft.Json.Linq.JArray() 72 | for value in values do 73 | output.Add(convertJToken value) 74 | output :> JToken 75 | | JObject dict -> 76 | let output = Newtonsoft.Json.Linq.JObject() 77 | for (key, value) in Map.toSeq dict do 78 | output.Add(JProperty(key, convertJToken value)) 79 | output :> JToken 80 | 81 | /// Tries to parse the input string as structured data 82 | let tryParse input = 83 | try Ok (parse input) 84 | with | ex -> Error ex.Message 85 | 86 | let getJPath (inputPath: string) = List.ofArray (inputPath.Split('.')) 87 | 88 | let rec readPath (keys: string list) (input: Json) = 89 | match keys, input with 90 | | [ ], _ -> None 91 | | [ key ], JObject dict -> Map.tryFind key dict 92 | | firstKey :: rest, JObject dict -> 93 | match Map.tryFind firstKey dict with 94 | | Some (JObject nextDict) -> readPath rest (JObject nextDict) 95 | | _ -> None 96 | | _ -> None 97 | 98 | let parseInt (input: string) : Option = 99 | match Int32.TryParse(input) with 100 | | true, value -> Some value 101 | | _, _ -> None 102 | 103 | let parseFloat (input: string) : Option = 104 | match Double.TryParse input with 105 | | true, value -> Some value 106 | | _ -> None 107 | 108 | let parseGuid (input: string) : Option = 109 | match Guid.TryParse input with 110 | | true, value -> Some value 111 | | _ -> None 112 | 113 | let extractValue (inputPath: string) (inputJson: Json) (typeInfo: Type) : Result = 114 | match typeInfo.FullName, readPath (getJPath inputPath) inputJson with 115 | | "System.Boolean", Some (JBool value) -> Ok (box value) 116 | | "System.Boolean", Some (JNumber 1.0) -> Ok (box true) 117 | | "System.Boolean", Some (JNumber 0.0) -> Ok (box false) 118 | | "System.Boolean", Some (JString ("true"|"True")) -> Ok (box true) 119 | | "System.Boolean", Some (JString ("false"|"False")) -> Ok (box false) 120 | | "System.Int32", Some (JNumber value) -> Ok (box (int (Math.Floor value))) 121 | | "System.Int32", Some (JString value) -> 122 | match parseInt value with 123 | | Some number -> Ok (box number) 124 | | None -> Error (sprintf "Could not parse value at path '%s' as an integer" inputPath) 125 | | "System.Double", Some (JNumber value) -> Ok (box value) 126 | | "System.Double", Some (JString value) -> 127 | match parseFloat value with 128 | | Some number -> Ok (box number) 129 | | None -> Error (sprintf "Could not parse value at path '%s' as a number" inputPath) 130 | | "System.String", Some (JString value) -> Ok (box value) 131 | | "System.String", Some (JNumber value) -> Ok (box (string value)) 132 | | "System.String", Some (JNull) -> Error (sprintf "String value at path '%s' was null" inputPath) 133 | | "System.Guid", Some (JString value) -> 134 | match parseGuid value with 135 | | Some value -> Ok (box value) 136 | | None -> Error (sprintf "Could not parse value at path '%s' as valid GUID" inputPath) 137 | | "Giraffe.JsonTherapy.Types.Json", jsonValue -> Ok (box jsonValue) 138 | | name, None when not (isOption typeInfo) -> 139 | Error (sprintf "No value was found at path '%s' within the JSON" inputPath) 140 | | name, Some value when not (isOption typeInfo) -> 141 | // try parse the value automatically 142 | let originalJToken = convertJToken value 143 | Ok (originalJToken.ToObject(typeInfo)) 144 | // no value was found for an optional value <-> return None 145 | | name, None when isOption typeInfo -> Ok (box None) 146 | | name, Some value when isOption typeInfo -> 147 | // option has one generic argument 148 | let innerType = typeInfo.GetGenericArguments().[0] 149 | match innerType.FullName, value with 150 | | "System.Boolean", JNull -> Ok (box Option.None) 151 | | "System.Boolean", JBool value -> Ok (box (Some value)) 152 | | "System.Boolean", (JString ("true"|"True") | JNumber 1.0) -> Ok (box (Some true)) 153 | | "System.Boolean", (JString ("false"|"False") | JNumber 0.0) -> Ok (box (Some false)) 154 | | "System.Int32", JNull -> Ok (box Option.None) 155 | | "System.Int32", JNumber n -> Ok (box (Some (int (Math.Floor(n))))) 156 | | "System.Int32", JString value -> 157 | match parseInt value with 158 | | Some number -> Ok (box (Some number)) 159 | | None -> Error (sprintf "Could not parse value at path '%s' as an integer" inputPath) 160 | | "System.String", JString value -> Ok (box (Some value)) 161 | | "System.String", JNumber value -> Ok (box (Some (string value))) 162 | | "System.String", JNull -> Ok (box Option.None) 163 | | "System.Double", JNull -> Ok (box Option.None) 164 | | "System.Double", JNumber value -> Ok (box (Some value)) 165 | | "System.Double", JString value -> 166 | match parseFloat value with 167 | | Some value -> Ok (box (Some value)) 168 | | None -> Error (sprintf "Could not parse value at path '%s' as a number" inputPath) 169 | | "System.Guid", JNull -> Ok (box Option.None) 170 | | "System.Guid", JString value -> 171 | match parseGuid value with 172 | | Some value -> Ok (box (Some value)) 173 | | None -> Error (sprintf "Could not parse value at path '%s' as valid GUID" inputPath) 174 | | "Giraffe.QueryReader.Types.Json", jsonValue -> Ok (box (Some jsonValue)) 175 | | name, anyJson -> 176 | try 177 | let originalToken = convertJToken anyJson 178 | Ok (box (Some (originalToken.ToObject(innerType)))) 179 | with 180 | | ex -> 181 | let originalToken = convertJToken anyJson 182 | let stringified = originalToken.ToString() 183 | Error (sprintf "Could not convert %s to type %s using default deserializer" stringified name) 184 | | name, None -> Error (sprintf "Could not parse value at path '%s' as type %s" inputPath name) 185 | | name, Some anyJson -> 186 | try 187 | let originalToken = convertJToken anyJson 188 | Ok (box (Some (originalToken.ToObject(typeInfo)))) 189 | with 190 | | ex -> 191 | let originalToken = convertJToken anyJson 192 | let stringified = originalToken.ToString() 193 | Error (sprintf "Could not convert %s to type %s using default deserializer" stringified name) 194 | 195 | let badRequest (msg: string) = 196 | setStatusCode 400 197 | >=> json (dict [ "message", msg ]) 198 | 199 | type Json() = 200 | static member parts<'t>(path: string, mapper: 't -> HttpHandler) = 201 | Extensions.request <| fun req -> 202 | task { 203 | use reader = new StreamReader(req.Body) 204 | let! content = reader.ReadToEndAsync() 205 | let inputJson = Extensions.parse content 206 | let typeInfo = typeof<'t> 207 | match Extensions.extractValue path inputJson typeInfo with 208 | | Error errorMsg -> return Extensions.badRequest errorMsg 209 | | Ok value -> return mapper (unbox<'t> value) 210 | } 211 | 212 | 213 | static member manyParts<'t>(path: string, mapper: 't list -> HttpHandler) = 214 | Extensions.request <| fun req -> task { 215 | use reader = new StreamReader(req.Body) 216 | let! content = reader.ReadToEndAsync() 217 | let inputJson = Extensions.parse content 218 | let typeInfo = typeof<'t> 219 | match inputJson with 220 | | JArray values -> 221 | let result = 222 | values 223 | |> List.choose (fun value -> 224 | match Extensions.extractValue path value typeInfo with 225 | | Error erroMsg -> None 226 | | Ok part -> Some (unbox<'t> part)) 227 | |> mapper 228 | return result 229 | 230 | | otherwise -> return Extensions.badRequest "Expected input as JSON array" 231 | } 232 | 233 | static member manyParts<'t, 'u>(fstPath: string, sndPath: string, mapper: ('t * 'u) list -> HttpHandler) = 234 | Extensions.request <| fun req -> task { 235 | use reader = new StreamReader(req.Body) 236 | let! content = reader.ReadToEndAsync() 237 | let inputJson = Extensions.parse content 238 | let fstType = typeof<'t> 239 | let sndType = typeof<'u> 240 | match inputJson with 241 | | JArray values -> 242 | return 243 | values 244 | |> List.choose (fun value -> 245 | match Extensions.extractValue fstPath value fstType with 246 | | Error erroMsg -> None 247 | | Ok first -> 248 | match Extensions.extractValue sndPath value sndType with 249 | | Error errorMsg -> None 250 | | Ok second -> Some (unbox<'t> first, unbox<'u> second)) 251 | |> mapper 252 | 253 | | otherwise -> return Extensions.badRequest "Expected input as JSON array" 254 | } 255 | 256 | static member manyParts<'t, 'u, 'v>(fstPath: string, sndPath: string, thirdPath: string, mapper: ('t * 'u * 'v) list -> HttpHandler) = 257 | Extensions.request <| fun req -> task { 258 | use reader = new StreamReader(req.Body) 259 | let! content = reader.ReadToEndAsync() 260 | let inputJson = Extensions.parse content 261 | let fstType = typeof<'t> 262 | let sndType = typeof<'u> 263 | let thirdType = typeof<'v> 264 | match inputJson with 265 | | JArray values -> 266 | return 267 | values 268 | |> List.choose (fun value -> 269 | match Extensions.extractValue fstPath value fstType with 270 | | Error erroMsg -> None 271 | | Ok first -> 272 | match Extensions.extractValue sndPath value sndType with 273 | | Error errorMsg -> None 274 | | Ok second -> 275 | match Extensions.extractValue thirdPath value thirdType with 276 | | Error errMsg -> None 277 | | Ok third -> Some (unbox<'t> first, unbox<'u> second, unbox<'v> third)) 278 | |> mapper 279 | 280 | | otherwise -> return Extensions.badRequest "Expected input as JSON array" 281 | } 282 | 283 | static member manyParts<'t, 'u, 'v, 'q>(fstPath: string, sndPath: string, thirdPath: string, forthPath: string, mapper: ('t * 'u * 'v * 'q) list -> HttpHandler) = 284 | Extensions.request <| fun req -> task { 285 | use reader = new StreamReader(req.Body) 286 | let! content = reader.ReadToEndAsync() 287 | let inputJson = Extensions.parse content 288 | let fstType = typeof<'t> 289 | let sndType = typeof<'u> 290 | let thirdType = typeof<'v> 291 | let forthType = typeof<'q> 292 | match inputJson with 293 | | JArray values -> 294 | return 295 | values 296 | |> List.choose (fun value -> 297 | match Extensions.extractValue fstPath value fstType with 298 | | Error erroMsg -> None 299 | | Ok first -> 300 | match Extensions.extractValue sndPath value sndType with 301 | | Error errorMsg -> None 302 | | Ok second -> 303 | match Extensions.extractValue thirdPath value thirdType with 304 | | Error errMsg -> None 305 | | Ok third -> 306 | match Extensions.extractValue forthPath value forthType with 307 | | Error _ -> None 308 | | Ok forth -> Some (unbox<'t> first, unbox<'u> second, unbox<'v> third, unbox<'q> forth)) 309 | |> mapper 310 | 311 | | otherwise -> return Extensions.badRequest "Expected input as JSON array" 312 | } 313 | 314 | static member manyParts<'t, 'u, 'v, 'q, 'w>(fstPath: string, sndPath: string, thirdPath: string, forthPath: string, fifthPath: string, mapper: ('t * 'u * 'v * 'q * 'w) list -> HttpHandler) = 315 | Extensions.request <| fun req -> task { 316 | use reader = new StreamReader(req.Body) 317 | let! content = reader.ReadToEndAsync() 318 | let inputJson = Extensions.parse content 319 | let fstType = typeof<'t> 320 | let sndType = typeof<'u> 321 | let thirdType = typeof<'v> 322 | let forthType = typeof<'q> 323 | let fifthType = typeof<'w> 324 | match inputJson with 325 | | JArray values -> 326 | return 327 | values 328 | |> List.choose (fun value -> 329 | match Extensions.extractValue fstPath value fstType with 330 | | Error erroMsg -> None 331 | | Ok first -> 332 | match Extensions.extractValue sndPath value sndType with 333 | | Error errorMsg -> None 334 | | Ok second -> 335 | match Extensions.extractValue thirdPath value thirdType with 336 | | Error errMsg -> None 337 | | Ok third -> 338 | match Extensions.extractValue forthPath value forthType with 339 | | Error _ -> None 340 | | Ok forth -> 341 | match Extensions.extractValue fifthPath value fifthType with 342 | | Error _ -> None 343 | | Ok fifth -> Some (unbox<'t> first, unbox<'u> second, unbox<'v> third, unbox<'q> forth, unbox<'w> fifth)) 344 | 345 | |> mapper 346 | 347 | | otherwise -> return Extensions.badRequest "Expected input as JSON array" 348 | } 349 | 350 | static member manyParts<'t, 'u, 'v, 'q, 'w, 'z>(fstPath: string, sndPath: string, thirdPath: string, forthPath: string, fifthPath: string, sixthPath: string, mapper: ('t * 'u * 'v * 'q * 'w * 'z) list -> HttpHandler) = 351 | Extensions.request <| fun req -> task { 352 | use reader = new StreamReader(req.Body) 353 | let! content = reader.ReadToEndAsync() 354 | let inputJson = Extensions.parse content 355 | let fstType = typeof<'t> 356 | let sndType = typeof<'u> 357 | let thirdType = typeof<'v> 358 | let forthType = typeof<'q> 359 | let fifthType = typeof<'w> 360 | let sixthType = typeof<'z> 361 | match inputJson with 362 | | JArray values -> 363 | return 364 | values 365 | |> List.choose (fun value -> 366 | match Extensions.extractValue fstPath value fstType with 367 | | Error erroMsg -> None 368 | | Ok first -> 369 | match Extensions.extractValue sndPath value sndType with 370 | | Error errorMsg -> None 371 | | Ok second -> 372 | match Extensions.extractValue thirdPath value thirdType with 373 | | Error errMsg -> None 374 | | Ok third -> 375 | match Extensions.extractValue forthPath value forthType with 376 | | Error _ -> None 377 | | Ok forth -> 378 | match Extensions.extractValue fifthPath value fifthType with 379 | | Error _ -> None 380 | | Ok fifth -> 381 | match Extensions.extractValue sixthPath value sixthType with 382 | | Error _ -> None 383 | | Ok sixth -> Some (unbox<'t> first, unbox<'u> second, unbox<'v> third, unbox<'q> forth, unbox<'w> fifth, unbox<'z> sixth)) 384 | |> mapper 385 | 386 | | otherwise -> return Extensions.badRequest "Expected input as JSON array" 387 | } 388 | 389 | static member manyParts<'t, 'u, 'v, 'q, 'w, 'z, 'p>(fstPath: string, sndPath: string, thirdPath: string, forthPath: string, fifthPath: string, sixthPath: string, seventhPath: string, mapper: ('t * 'u * 'v * 'q * 'w * 'z * 'p) list -> HttpHandler) = 390 | Extensions.request <| fun req -> task { 391 | use reader = new StreamReader(req.Body) 392 | let! content = reader.ReadToEndAsync() 393 | let inputJson = Extensions.parse content 394 | let fstType = typeof<'t> 395 | let sndType = typeof<'u> 396 | let thirdType = typeof<'v> 397 | let forthType = typeof<'q> 398 | let fifthType = typeof<'w> 399 | let sixthType = typeof<'z> 400 | let seventhType = typeof<'p> 401 | match inputJson with 402 | | JArray values -> 403 | return 404 | values 405 | |> List.choose (fun value -> 406 | match Extensions.extractValue fstPath value fstType with 407 | | Error erroMsg -> None 408 | | Ok first -> 409 | match Extensions.extractValue sndPath value sndType with 410 | | Error errorMsg -> None 411 | | Ok second -> 412 | match Extensions.extractValue thirdPath value thirdType with 413 | | Error errMsg -> None 414 | | Ok third -> 415 | match Extensions.extractValue forthPath value forthType with 416 | | Error _ -> None 417 | | Ok forth -> 418 | match Extensions.extractValue fifthPath value fifthType with 419 | | Error _ -> None 420 | | Ok fifth -> 421 | match Extensions.extractValue sixthPath value sixthType with 422 | | Error _ -> None 423 | | Ok sixth -> 424 | match Extensions.extractValue seventhPath value seventhType with 425 | | Error _ -> None 426 | | Ok seventh -> Some (unbox<'t> first, unbox<'u> second, unbox<'v> third, unbox<'q> forth, unbox<'w> fifth, unbox<'z> sixth, unbox<'p> seventh)) 427 | |> mapper 428 | | otherwise -> 429 | return Extensions.badRequest "Expected input as JSON array" 430 | } 431 | 432 | static member manyParts<'t, 'u, 'v, 'q, 'w, 'z, 'p, 'r>(fstPath: string, sndPath: string, thirdPath: string, forthPath: string, fifthPath: string, sixthPath: string, seventhPath: string, eighthPath: string, mapper: ('t * 'u * 'v * 'q * 'w * 'z * 'p * 'r) list -> HttpHandler) = 433 | Extensions.request <| fun req -> task { 434 | use reader = new StreamReader(req.Body) 435 | let! content = reader.ReadToEndAsync() 436 | let inputJson = Extensions.parse content 437 | let fstType = typeof<'t> 438 | let sndType = typeof<'u> 439 | let thirdType = typeof<'v> 440 | let forthType = typeof<'q> 441 | let fifthType = typeof<'w> 442 | let sixthType = typeof<'z> 443 | let seventhType = typeof<'p> 444 | let eighthType = typeof<'r> 445 | match inputJson with 446 | | JArray values -> 447 | return 448 | values 449 | |> List.choose (fun value -> 450 | match Extensions.extractValue fstPath value fstType with 451 | | Error erroMsg -> None 452 | | Ok first -> 453 | match Extensions.extractValue sndPath value sndType with 454 | | Error errorMsg -> None 455 | | Ok second -> 456 | match Extensions.extractValue thirdPath value thirdType with 457 | | Error errMsg -> None 458 | | Ok third -> 459 | match Extensions.extractValue forthPath value forthType with 460 | | Error _ -> None 461 | | Ok forth -> 462 | match Extensions.extractValue fifthPath value fifthType with 463 | | Error _ -> None 464 | | Ok fifth -> 465 | match Extensions.extractValue sixthPath value sixthType with 466 | | Error _ -> None 467 | | Ok sixth -> 468 | match Extensions.extractValue seventhPath value seventhType with 469 | | Error _ -> None 470 | | Ok seventh -> 471 | match Extensions.extractValue eighthPath value eighthType with 472 | | Error _ -> None 473 | | Ok eighth -> Some (unbox<'t> first, unbox<'u> second, unbox<'v> third, unbox<'q> forth, unbox<'w> fifth, unbox<'z> sixth, unbox<'p> seventh, unbox<'r> eighth)) 474 | |> mapper 475 | | otherwise -> 476 | return Extensions.badRequest "Expected input as JSON array" 477 | } 478 | static member parts<'t, 'u>(fstPath: string, sndPath: string, mapper: 't -> 'u -> HttpHandler) = 479 | Extensions.request <| fun req -> task { 480 | use reader = new StreamReader(req.Body) 481 | let! content = reader.ReadToEndAsync() 482 | let inputJson = Extensions.parse content 483 | let first = typeof<'t> 484 | let second = typeof<'u> 485 | match Extensions.extractValue fstPath inputJson first with 486 | | Error errorMsg -> return Extensions.badRequest errorMsg 487 | | Ok first -> 488 | match Extensions.extractValue sndPath inputJson second with 489 | | Error errorMsg -> return Extensions.badRequest errorMsg 490 | | Ok second -> return mapper (unbox<'t> first) (unbox<'u> second) 491 | } 492 | 493 | static member parts<'t, 'u, 'v>(fstPath: string, sndPath: string, thirdPath: string, mapper: 't -> 'u -> 'v -> HttpHandler) = 494 | Extensions.request <| fun req -> task { 495 | use reader = new StreamReader(req.Body) 496 | let! content = reader.ReadToEndAsync() 497 | let inputJson = Extensions.parse content 498 | let firstType = typeof<'t> 499 | let secondType = typeof<'u> 500 | let thirdType = typeof<'v> 501 | match Extensions.extractValue fstPath inputJson firstType with 502 | | Error errorMsg -> return Extensions.badRequest errorMsg 503 | | Ok first -> 504 | match Extensions.extractValue sndPath inputJson secondType with 505 | | Error errorMsg -> return Extensions.badRequest errorMsg 506 | | Ok second -> 507 | match Extensions.extractValue thirdPath inputJson thirdType with 508 | | Error errorMsg -> return Extensions.badRequest errorMsg 509 | | Ok third -> return mapper (unbox<'t> first) (unbox<'u> second) (unbox<'v> third) 510 | } 511 | static member parts<'t, 'u, 'v, 'w>(fstPath: string, sndPath: string, thirdPath: string, forthPath: string, mapper: 't -> 'u -> 'v -> 'w -> HttpHandler) = 512 | Extensions.request <| fun req -> task { 513 | use reader = new StreamReader(req.Body) 514 | let! content = reader.ReadToEndAsync() 515 | let inputJson = Extensions.parse content 516 | let firstType = typeof<'t> 517 | let secondType = typeof<'u> 518 | let thirdType = typeof<'v> 519 | let forthType = typeof<'w> 520 | match Extensions.extractValue fstPath inputJson firstType with 521 | | Error errorMsg -> return Extensions.badRequest errorMsg 522 | | Ok first -> 523 | match Extensions.extractValue sndPath inputJson secondType with 524 | | Error errorMsg -> return Extensions.badRequest errorMsg 525 | | Ok second -> 526 | match Extensions.extractValue thirdPath inputJson thirdType with 527 | | Error errorMsg -> return Extensions.badRequest errorMsg 528 | | Ok third -> 529 | match Extensions.extractValue forthPath inputJson forthType with 530 | | Error errorMsg -> return Extensions.badRequest errorMsg 531 | | Ok forth -> return mapper (unbox<'t> first) (unbox<'u> second) (unbox<'v> third) (unbox<'w> forth) 532 | } 533 | 534 | static member parts<'t, 'u, 'v, 'w, 'q>(fstPath: string, sndPath: string, thirdPath: string, forthPath: string, fifthPath: string, mapper: 't -> 'u -> 'v -> 'w -> 'q -> HttpHandler) = 535 | Extensions.request <| fun req -> task { 536 | use reader = new StreamReader(req.Body) 537 | let! content = reader.ReadToEndAsync() 538 | let inputJson = Extensions.parse content 539 | let firstType = typeof<'t> 540 | let secondType = typeof<'u> 541 | let thirdType = typeof<'v> 542 | let forthType = typeof<'w> 543 | let fifthType = typeof<'q> 544 | match Extensions.extractValue fstPath inputJson firstType with 545 | | Error errorMsg -> return Extensions.badRequest errorMsg 546 | | Ok first -> 547 | match Extensions.extractValue sndPath inputJson secondType with 548 | | Error errorMsg -> return Extensions.badRequest errorMsg 549 | | Ok second -> 550 | match Extensions.extractValue thirdPath inputJson thirdType with 551 | | Error errorMsg -> return Extensions.badRequest errorMsg 552 | | Ok third -> 553 | match Extensions.extractValue forthPath inputJson forthType with 554 | | Error errorMsg -> return Extensions.badRequest errorMsg 555 | | Ok forth -> 556 | match Extensions.extractValue fifthPath inputJson fifthType with 557 | | Error errorMsg -> return Extensions.badRequest errorMsg 558 | | Ok fifth -> return mapper (unbox<'t> first) (unbox<'u> second) (unbox<'v> third) (unbox<'w> forth) (unbox<'q> fifth) 559 | } 560 | 561 | static member parts<'t, 'u, 'v, 'w, 'q, 'y>(fstPath: string, sndPath: string, thirdPath: string, forthPath: string, fifthPath: string, sixthPath: string, mapper: 't -> 'u -> 'v -> 'w -> 'q -> 'y -> HttpHandler) = 562 | Extensions.request <| fun req -> task { 563 | use reader = new StreamReader(req.Body) 564 | let! content = reader.ReadToEndAsync() 565 | let inputJson = Extensions.parse content 566 | let firstType = typeof<'t> 567 | let secondType = typeof<'u> 568 | let thirdType = typeof<'v> 569 | let forthType = typeof<'w> 570 | let fifthType = typeof<'q> 571 | let sixthType = typeof<'y> 572 | match Extensions.extractValue fstPath inputJson firstType with 573 | | Error errorMsg -> return Extensions.badRequest errorMsg 574 | | Ok first -> 575 | match Extensions.extractValue sndPath inputJson secondType with 576 | | Error errorMsg -> return Extensions.badRequest errorMsg 577 | | Ok second -> 578 | match Extensions.extractValue thirdPath inputJson thirdType with 579 | | Error errorMsg -> return Extensions.badRequest errorMsg 580 | | Ok third -> 581 | match Extensions.extractValue forthPath inputJson forthType with 582 | | Error errorMsg -> return Extensions.badRequest errorMsg 583 | | Ok forth -> 584 | match Extensions.extractValue fifthPath inputJson fifthType with 585 | | Error errorMsg -> return Extensions.badRequest errorMsg 586 | | Ok fifth -> 587 | match Extensions.extractValue sixthPath inputJson sixthType with 588 | | Error errorMsg -> return Extensions.badRequest errorMsg 589 | | Ok sixth -> return mapper (unbox<'t> first) (unbox<'u> second) (unbox<'v> third) (unbox<'w> forth) (unbox<'q> fifth) (unbox<'y> sixth) 590 | } 591 | static member parts<'t, 'u, 'v, 'w, 'q, 'y, 'r>(fstPath: string, sndPath: string, thirdPath: string, forthPath: string, fifthPath: string, sixthPath: string, seventhPath: string, mapper: 't -> 'u -> 'v -> 'w -> 'q -> 'y -> 'r -> HttpHandler) = 592 | Extensions.request <| fun req -> task { 593 | use reader = new StreamReader(req.Body) 594 | let! content = reader.ReadToEndAsync() 595 | let inputJson = Extensions.parse content 596 | let firstType = typeof<'t> 597 | let secondType = typeof<'u> 598 | let thirdType = typeof<'v> 599 | let forthType = typeof<'w> 600 | let fifthType = typeof<'q> 601 | let sixthType = typeof<'y> 602 | let seventhType = typeof<'r> 603 | match Extensions.extractValue fstPath inputJson firstType with 604 | | Error errorMsg -> return Extensions.badRequest errorMsg 605 | | Ok first -> 606 | match Extensions.extractValue sndPath inputJson secondType with 607 | | Error errorMsg -> return Extensions.badRequest errorMsg 608 | | Ok second -> 609 | match Extensions.extractValue thirdPath inputJson thirdType with 610 | | Error errorMsg -> return Extensions.badRequest errorMsg 611 | | Ok third -> 612 | match Extensions.extractValue forthPath inputJson forthType with 613 | | Error errorMsg -> return Extensions.badRequest errorMsg 614 | | Ok forth -> 615 | match Extensions.extractValue fifthPath inputJson fifthType with 616 | | Error errorMsg -> return Extensions.badRequest errorMsg 617 | | Ok fifth -> 618 | match Extensions.extractValue sixthPath inputJson sixthType with 619 | | Error errorMsg -> return Extensions.badRequest errorMsg 620 | | Ok sixth -> 621 | match Extensions.extractValue seventhPath inputJson seventhType with 622 | | Error errorMsg -> return Extensions.badRequest errorMsg 623 | | Ok seventh -> return mapper (unbox<'t> first) (unbox<'u> second) (unbox<'v> third) (unbox<'w> forth) (unbox<'q> fifth) (unbox<'y> sixth) (unbox<'r> seventh) 624 | } 625 | static member parts<'t, 'u, 'v, 'w, 'q, 'y, 'r, 'z>(fstPath: string, sndPath: string, thirdPath: string, forthPath: string, fifthPath: string, sixthPath: string, seventhPath: string, eighthPath: string, mapper: 't -> 'u -> 'v -> 'w -> 'q -> 'y -> 'r -> 'z -> HttpHandler) = 626 | Extensions.request <| fun req -> task { 627 | use reader = new StreamReader(req.Body) 628 | let! content = reader.ReadToEndAsync() 629 | let inputJson = Extensions.parse content 630 | let firstType = typeof<'t> 631 | let secondType = typeof<'u> 632 | let thirdType = typeof<'v> 633 | let forthType = typeof<'w> 634 | let fifthType = typeof<'q> 635 | let sixthType = typeof<'y> 636 | let seventhType = typeof<'r> 637 | let eighthType = typeof<'z> 638 | match Extensions.extractValue fstPath inputJson firstType with 639 | | Error errorMsg -> return Extensions.badRequest errorMsg 640 | | Ok first -> 641 | match Extensions.extractValue sndPath inputJson secondType with 642 | | Error errorMsg -> return Extensions.badRequest errorMsg 643 | | Ok second -> 644 | match Extensions.extractValue thirdPath inputJson thirdType with 645 | | Error errorMsg -> return Extensions.badRequest errorMsg 646 | | Ok third -> 647 | match Extensions.extractValue forthPath inputJson forthType with 648 | | Error errorMsg -> return Extensions.badRequest errorMsg 649 | | Ok forth -> 650 | match Extensions.extractValue fifthPath inputJson fifthType with 651 | | Error errorMsg -> return Extensions.badRequest errorMsg 652 | | Ok fifth -> 653 | match Extensions.extractValue sixthPath inputJson sixthType with 654 | | Error errorMsg -> return Extensions.badRequest errorMsg 655 | | Ok sixth -> 656 | match Extensions.extractValue seventhPath inputJson seventhType with 657 | | Error errorMsg -> return Extensions.badRequest errorMsg 658 | | Ok seventh -> 659 | match Extensions.extractValue eighthPath inputJson eighthType with 660 | | Error errorMsg -> return Extensions.badRequest errorMsg 661 | | Ok eighth -> return mapper (unbox<'t> first) (unbox<'u> second) (unbox<'v> third) (unbox<'w> forth) (unbox<'q> fifth) (unbox<'y> sixth) (unbox<'r> seventh) (unbox<'z> eighth) 662 | } 663 | --------------------------------------------------------------------------------