diff --git a/.github/workflows/build-and-publish-docs.yml b/.github/workflows/build-and-publish-docs.yml index be3f14b5..780a5e88 100644 --- a/.github/workflows/build-and-publish-docs.yml +++ b/.github/workflows/build-and-publish-docs.yml @@ -2,8 +2,8 @@ name: Build and Publish Docs on: workflow_dispatch: - push: - branches: [ master ] + # push: + # branches: [ master ] jobs: build: @@ -16,10 +16,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Setup .NET 7 + - name: Setup .NET 8 uses: actions/setup-dotnet@v4 with: - dotnet-version: 7.x + dotnet-version: 8.x - name: Build solution run: dotnet build -c Release diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 9bd69cfa..62709a3b 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -18,15 +18,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Setup .NET 6 + - name: Setup .NET 8 uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.x - - - name: Setup .NET 7 - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 7.x + dotnet-version: 8.x - name: Build and run tests run: dotnet fsi build.fsx test diff --git a/.github/workflows/publish-nuget.yml b/.github/workflows/publish-nuget.yml index 3381d509..8fd8ac84 100644 --- a/.github/workflows/publish-nuget.yml +++ b/.github/workflows/publish-nuget.yml @@ -13,15 +13,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Setup .NET 6 + - name: Setup .NET 8 uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.x - - - name: Setup .NET 7 - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 7.x + dotnet-version: 8.x - name: nuget publish env: diff --git a/.gitignore b/.gitignore index 9cfe6da5..1a09e227 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ .ionide symbolCache.db .paket/load +docs/index.md # User-specific files *.suo diff --git a/Directory.Build.props b/Directory.Build.props index 10d9b23f..a91db79c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,13 +1,13 @@ - 12.2.0-preview02 + 13.2.0 Ronald Schlenker - Copyright 2023 Ronald Schlenker + Copyright 2024 Ronald Schlenker f# c# fSharp http rest HttpClient fetch curl true - logo_small.png + logo.png Apache-2.0 README.md https://github.com/fsprojects/FsHttp @@ -16,6 +16,14 @@ https://www.nuget.org/packages/FsHttp#release-body-tab + v13.1.0 + - All `Response._TAsync` functions (task based) in F# require a CancellationToken now. + + v13.1.0 + - There's no more StartingContext, which means: + we give up a little bit of safety here, for the sake of pre-configuring HTTP requests + without specifying the URL. This is a trade-off we are willing to take. + v12.2.0 - added HttpMethods for better composability diff --git a/FsHttp.sln b/FsHttp.sln index 15809390..e702d3d8 100644 --- a/FsHttp.sln +++ b/FsHttp.sln @@ -3,8 +3,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.32014.148 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{13B4B071-F3F1-4E38-93E4-CFA72611A84F}" -EndProject Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FsHttp", "src\FsHttp\FsHttp.fsproj", "{D1AD2123-678B-4DC9-9DBA-3D6FF2E30F61}" EndProject Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FsHttp.FSharpData", "src\FsHttp.FSharpData\FsHttp.FSharpData.fsproj", "{07320ED5-9E4A-452E-80C3-007C3303735F}" @@ -51,14 +49,6 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {D1AD2123-678B-4DC9-9DBA-3D6FF2E30F61} = {13B4B071-F3F1-4E38-93E4-CFA72611A84F} - {07320ED5-9E4A-452E-80C3-007C3303735F} = {13B4B071-F3F1-4E38-93E4-CFA72611A84F} - {CB577E4F-F4F0-4ADA-BD90-036B5ECF17AD} = {13B4B071-F3F1-4E38-93E4-CFA72611A84F} - {1163EF7E-1845-4D2A-8C3F-8691618500DF} = {13B4B071-F3F1-4E38-93E4-CFA72611A84F} - {E7E326FD-1D66-4DBD-AB60-D006D3B77244} = {13B4B071-F3F1-4E38-93E4-CFA72611A84F} - {63A6AF47-CB3F-4DBA-A16E-B66D6EFA3573} = {13B4B071-F3F1-4E38-93E4-CFA72611A84F} - EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {490D0829-D4E3-4267-8132-14A5B0C9D347} EndGlobalSection diff --git a/README.md b/README.md index cdb69cb1..e1ddba93 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- logo + logo

FsHttp is a .Net HTTP client library for C# and F#. It aims for describing and executing HTTP requests in convenient ways that can be used in production and interactive environments. @@ -41,9 +41,10 @@ An example in C#: ```csharp #r "nuget: FsHttp" -using FsHttp.CSharp; +using FsHttp; -await "https://reqres.in/api/users".Post() +await "https://reqres.in/api/users" + .Post() .CacheControl("no-cache") .Body() .JsonSerialize(new @@ -67,7 +68,7 @@ Release Notes / Migrating to new versions --- * See https://www.nuget.org/packages/FsHttp#release-body-tab -* For different upgrade paths, please read the [Migrations section](https://fsprojects.github.io/FsHttp/Migrations.html) in the docu. +* For different upgrade paths, please read the [Migrations section](https://schlenkr.github.io/FsHttp/Migrations.html) in the docu. GitHub @@ -101,5 +102,5 @@ For common tasks, there are powershell files located in the repo root: Credits ------- -* Parts of the code is taken from the [HTTP utilities of FSharp.Data](https://fsprojects.github.io/FSharp.Data/library/Http.html). +* Parts of the code is taken from the [HTTP utilities of FSharp.Data](https://schlenkr.github.io/FSharp.Data/library/Http.html). * Credits to all critics, supporters, contributors, promoters, users, and friends. diff --git a/artwork/Grayscale Transparent.png b/artwork/Grayscale Transparent.png new file mode 100644 index 00000000..bbbf03e4 Binary files /dev/null and b/artwork/Grayscale Transparent.png differ diff --git a/artwork/Original Logo Symbol.png b/artwork/Original Logo Symbol.png new file mode 100644 index 00000000..c5b00ac7 Binary files /dev/null and b/artwork/Original Logo Symbol.png differ diff --git a/artwork/Original Logo.png b/artwork/Original Logo.png new file mode 100644 index 00000000..1a109d50 Binary files /dev/null and b/artwork/Original Logo.png differ diff --git a/artwork/Transparent Logo.png b/artwork/Transparent Logo.png new file mode 100644 index 00000000..0c7474ad Binary files /dev/null and b/artwork/Transparent Logo.png differ diff --git a/docs/Builders_and_Evaluation.fsx.bak b/docs/Builders_and_Evaluation.fsx.bak index 02c1c337..01389f2e 100644 --- a/docs/Builders_and_Evaluation.fsx.bak +++ b/docs/Builders_and_Evaluation.fsx.bak @@ -23,7 +23,6 @@ simply build a request, pass it around and send it later or to warp it in async. Chaining builders together: First, use a httpLazy to create a 'HeaderContext' -*Hint:* ```httpLazy { ... }``` is just a shortcut for ```httpRequest StartingContext { ... }``` *) let postOnly = httpLazy { diff --git a/docs/Overview.fsx b/docs/Overview.fsx index 774d599a..b188880a 100644 --- a/docs/Overview.fsx +++ b/docs/Overview.fsx @@ -20,7 +20,6 @@ open FsHttp // Reference the 'FsHttp' package from NuGet in your script or project #r "nuget: FsHttp" -// Opening 'FsHttp' is sufficient (no need for FsHttp.DSL or others anymore). open FsHttp (** diff --git a/docs/Response_Handling.fsx b/docs/Response_Handling.fsx index 4095fd8d..52519001 100644 --- a/docs/Response_Handling.fsx +++ b/docs/Response_Handling.fsx @@ -19,7 +19,7 @@ open FsHttp There are several ways transforming the content of the returned response to something like text or JSON: -See also: [Response](reference/fshttp-response.html) +See also: [Response](reference/FsHttp-response.html) *) http { POST "https://reqres.in/api/users" diff --git a/docs/content/fsdocs-theme.css b/docs/content/fsdocs-theme.css index 127f7320..fded7e13 100644 --- a/docs/content/fsdocs-theme.css +++ b/docs/content/fsdocs-theme.css @@ -1,36 +1,36 @@ :root { - --fshttp-50: #f0fdf2; - --fshttp-100: #dbfde3; - --fshttp-200: #baf8c8; - --fshttp-300: #64ee85; - --fshttp-400: #47e16c; - --fshttp-500: #1ec948; - --fshttp-600: #13a637; - --fshttp-700: #12832e; - --fshttp-800: #14672a; - --fshttp-900: #135425; - --fshttp-950: #042f11; + --FsHttp-50: #f0fdf2; + --FsHttp-100: #dbfde3; + --FsHttp-200: #baf8c8; + --FsHttp-300: #64ee85; + --FsHttp-400: #47e16c; + --FsHttp-500: #1ec948; + --FsHttp-600: #13a637; + --FsHttp-700: #12832e; + --FsHttp-800: #14672a; + --FsHttp-900: #135425; + --FsHttp-950: #042f11; --logo-background-color: #224454; --pearl: #FFFDFA; - --primary: var(--fshttp-300); + --primary: var(--FsHttp-300); --header-background: var(--logo-background-color); --header-link-color: var(--pearl); --fsdocs-theme-toggle-light-color: var(--pearl); --menu-color: var(--pearl); --blockquote-bacground-color: var(--primary); --blockquote-color: var(--pearl); - --menu-item-hover-background: var(--fshttp-200); + --menu-item-hover-background: var(--FsHttp-200); --menu-item-hover-color: var(--logo-background-color); --on-this-page-color: var(--pearl); - --heading-color: var(--fshttp-950); - --page-menu-background-hover-border-color: var(--fshttp-300); - --nav-item-active-border-color: var(--fshttp-300); - --dialog-icon-color: var(--fshttp-300); - --dialog-link-color: var(--fshttp-300); + --heading-color: var(--FsHttp-950); + --page-menu-background-hover-border-color: var(--FsHttp-300); + --nav-item-active-border-color: var(--FsHttp-300); + --dialog-icon-color: var(--FsHttp-300); + --dialog-link-color: var(--FsHttp-300); } [data-theme=dark] { - --link-color: var(--fshttp-200); + --link-color: var(--FsHttp-200); --heading-color: var(--pearl); } \ No newline at end of file diff --git a/docs/img/logo.png b/docs/img/logo.png index b7082ed1..0c7474ad 100644 Binary files a/docs/img/logo.png and b/docs/img/logo.png differ diff --git a/docs/img/logo_big.png b/docs/img/logo_big.png deleted file mode 100644 index ff4b05db..00000000 Binary files a/docs/img/logo_big.png and /dev/null differ diff --git a/docs/img/logo_small.png b/docs/img/logo_small.png deleted file mode 100644 index b7082ed1..00000000 Binary files a/docs/img/logo_small.png and /dev/null differ diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 625cf550..00000000 --- a/docs/index.md +++ /dev/null @@ -1,126 +0,0 @@ ---- -IMPORTANT: This file is generated by `./docu.ps1`. Please don't edit it manually! - -title: FsHttp Overview -index: 1 ---- -

- logo -

- -FsHttp is a .Net HTTP client library for C# and F#. It aims for describing and executing HTTP requests in convenient ways that can be used in production and interactive environments. - -The design principle behind FsHttp is: - -> Specify common HTTP requests in a most convenient and readable way, while still being able to access the underlying .Net Http representations for covering unusual cases. - -**FsHttp** is developed and maintained by [@ronaldschlenker](https://github.com/ronaldschlenker) and [@dawedawe](https://github.com/dawedawe). Feel free to leave us a message. - -[![NuGet Badge](http://img.shields.io/nuget/v/FsHttp.svg?style=flat)](https://www.nuget.org/packages/FsHttp) ![build status](https://github.com/fsprojects/FsHttp/actions/workflows/push-master_pull-request.yml/badge.svg?event=push) - - -Sponsoring ----------- - -Want to help keep FsHttp alive? Then help keep F# and its ecosystem alive by supporting one of the following developers: - -* [@edgarfgp](https://github.com/sponsors/edgarfgp). Why? E.g. for "Amplifying F#", for bringing people together, for his support and his work in the F# community. -* [@TheAngryByrd](https://github.com/sponsors/TheAngryByrd). Why? E.g. for maintaining Ionidem and many, many more. -* [@AngelMunoz](https://github.com/sponsors/AngelMunoz). Why? E.g. for his work in Fable (The F# to JS compiler), his passion and support. -* [@dawedawe](https://github.com/sponsors/dawedawe) and [@nojaf](https://github.com/sponsors/nojaf). Why? E.g. for Fantomas and their support in the F# community. -* [@PawelStadnicki](https://github.com/sponsors/PawelStadnicki). Why? E.g. for his attempt to push F# forward in data science. - -For sure, there are many more. If you think someone should appear on that list, leave us a message! - - -A Simple Example ----------------- - -An example in F#: - -```fsharp -#r "nuget: FsHttp" - -open FsHttp - -http { - POST "https://reqres.in/api/users" - CacheControl "no-cache" - body - jsonSerialize - {| - name = "morpheus" - job = "leader" - |} -} -|> Request.send -``` - -An example in C#: - -```csharp -#r "nuget: FsHttp" - -using FsHttp.CSharp; - -await "https://reqres.in/api/users".Post() - .CacheControl("no-cache") - .Body() - .JsonSerialize(new - { - name = "morpheus", - job = "leader" - } - ) - .SendAsync(); -``` - - -Documentation -------------- - -* 📖 Please see [FsHttp Documentation](https://fsprojects.github.io/FsHttp) site for a detailed documentation. -* 🧪 In addition, have a look at the [Integration Tests](https://github.com/fsprojects/FsHttp/tree/master/src/Tests) that show various library details. - - -Release Notes / Migrating to new versions ---- - -* See https://www.nuget.org/packages/FsHttp#release-body-tab -* For different upgrade paths, please read the [Migrations section](https://fsprojects.github.io/FsHttp/Migrations.html) in the docu. - - -GitHub -------------- - -Please see [FsHttp on GitHub](https://github.com/fsprojects/FsHttp). - - -Building --------- - -**.Net SDK:** - -You need to have the latest .Net SDK installed, which is specified in `./global.json`. - -**Build Tasks** - -There is a F# build script (`./build.fsx`) that can be used to perform several build tasks from command line. - -For common tasks, there are powershell files located in the repo root: - -* `./test.ps1`: Runs all tests (sources in `./src/Tests`). - * You can pass args to this task. E.g. for executing only some tests: - `./test.ps1 --filter Name~'Response Decompression'` -* `./docu.ps1`: Rebuilds the FsHttp documentation site (sources in `./src/docs`). -* `./docu-watch.ps1`: Run it if you are working on the documentation sources, and want to see the result in a browser. -* `./publish.ps1`: Publishes all packages (FsHttp and it's integration packages for Newtonsoft and FSharp.Data) to NuGet. - * Always have a look at `./src/Directory.Build.props` and keep the file up-to-date. - - -Credits -------- - -* Parts of the code is taken from the [HTTP utilities of FSharp.Data](https://fsprojects.github.io/FSharp.Data/library/Http.html). -* Credits to all critics, supporters, contributors, promoters, users, and friends. - diff --git a/fiddle/DUShadowingInCEs.fsx b/fiddle/DUShadowingInCEs.fsx index 3b47346d..e2876ffd 100644 --- a/fiddle/DUShadowingInCEs.fsx +++ b/fiddle/DUShadowingInCEs.fsx @@ -1,4 +1,4 @@ -#r "../src/FsHttp/bin/debug/net7.0/fshttp.dll" +#r "../src/FsHttp/bin/debug/net7.0/FsHttp.dll" open System open FsHttp diff --git a/fiddle/co-maintainer.fsx b/fiddle/co-maintainer.fsx index 2337dca9..769d8681 100644 --- a/fiddle/co-maintainer.fsx +++ b/fiddle/co-maintainer.fsx @@ -1,5 +1,5 @@ -#r "../src/FsHttp/bin/debug/net7.0/fshttp.dll" +#r "../src/FsHttp/bin/debug/net7.0/FsHttp.dll" #r "nuget: FsHttp" open System diff --git a/fiddle/debugBuildFiddle.fsx b/fiddle/debugBuildFiddle.fsx new file mode 100644 index 00000000..86cf739d --- /dev/null +++ b/fiddle/debugBuildFiddle.fsx @@ -0,0 +1,14 @@ + +#r "../src/FsHttp/bin/Debug/net8.0/FsHttp.dll" + +open FsHttp + +let httpWithAuth = + http { + AuthorizationBearer "sdfsdffsdf" + } + +httpWithAuth { + GET "https://jsonplaceholder.typicode.com/todos/1" +} +|> Request.send diff --git a/fiddle/issue-101.fsx b/fiddle/issue-101.fsx index 27ddc97c..ac9fa78d 100644 --- a/fiddle/issue-101.fsx +++ b/fiddle/issue-101.fsx @@ -3,7 +3,7 @@ open FsHttp let getString url rjson = async { let! response = - // See also: https://fsprojects.github.io/FsHttp/Migrations.html + // See also: https://schlenkr.github.io/FsHttp/Migrations.html // 'httpAsync' is replaced by 'http { ... } |> Request.sendAsync' http { POST url diff --git a/fiddle/issue-103.fsx b/fiddle/issue-103.fsx index 97ae12a6..d16cdd7c 100644 --- a/fiddle/issue-103.fsx +++ b/fiddle/issue-103.fsx @@ -1,5 +1,5 @@ -#r "../FsHttp/bin/debug/net8.0/fshttp.dll" +#r "../FsHttp/bin/debug/net8.0/FsHttp.dll" FsHttp.FsiInit.doInit() diff --git a/fiddle/issue-113.fsx b/fiddle/issue-113.fsx index 90d7d02c..8d68bc41 100644 --- a/fiddle/issue-113.fsx +++ b/fiddle/issue-113.fsx @@ -1,5 +1,5 @@ -#r "../src/FsHttp/bin/debug/net7.0/fshttp.dll" +#r "../src/FsHttp/bin/debug/net7.0/FsHttp.dll" open System open System.Net.Http diff --git a/fiddle/issue-121.fsx b/fiddle/issue-121.fsx index b855abd6..18dbf9d1 100644 --- a/fiddle/issue-121.fsx +++ b/fiddle/issue-121.fsx @@ -1,5 +1,5 @@ -#r "../src/FsHttp/bin/debug/net7.0/fshttp.dll" +#r "../src/FsHttp/bin/debug/net7.0/FsHttp.dll" open System open System.Net.Http diff --git a/fiddle/issue-126.fsx b/fiddle/issue-126.fsx index 28fca391..43f0b843 100644 --- a/fiddle/issue-126.fsx +++ b/fiddle/issue-126.fsx @@ -1,5 +1,5 @@ -#r "../src/FsHttp/bin/debug/net7.0/fshttp.dll" +#r "../src/FsHttp/bin/debug/net7.0/FsHttp.dll" open System.IO open System.Net.Http diff --git a/fiddle/issue-129.fsx b/fiddle/issue-129.fsx index 0ee5d727..2500eb70 100644 --- a/fiddle/issue-129.fsx +++ b/fiddle/issue-129.fsx @@ -1,5 +1,5 @@ -#r "../src/FsHttp/bin/debug/net7.0/fshttp.dll" +#r "../src/FsHttp/bin/debug/net7.0/FsHttp.dll" open System.IO open System.Net.Http diff --git a/fiddle/prettyFsiIntegration.fsx b/fiddle/prettyFsiIntegration.fsx index 6cf5183b..b26ee9f4 100644 --- a/fiddle/prettyFsiIntegration.fsx +++ b/fiddle/prettyFsiIntegration.fsx @@ -8,7 +8,7 @@ open FsHttp.Operators % http { GET "https://api.github.com/users/ronaldschlenker" - UserAgent "fshttp" + UserAgent "FsHttp" } diff --git a/src/FsHttp.FSharpData/FsHttp.FSharpData.fsproj b/src/FsHttp.FSharpData/FsHttp.FSharpData.fsproj index 6ff80d17..476fae8e 100644 --- a/src/FsHttp.FSharpData/FsHttp.FSharpData.fsproj +++ b/src/FsHttp.FSharpData/FsHttp.FSharpData.fsproj @@ -1,6 +1,6 @@  - netstandard2.0;netstandard2.1;net6.0;net7.0;net8.0 + net6.0;net7.0;net8.0 false Debug;Release true @@ -20,7 +20,7 @@ - + diff --git a/src/FsHttp.FSharpData/Response.fs b/src/FsHttp.FSharpData/Response.fs index 8b72c46c..c2acda5b 100644 --- a/src/FsHttp.FSharpData/Response.fs +++ b/src/FsHttp.FSharpData/Response.fs @@ -17,8 +17,12 @@ let toJsonAsync response = } ) -let toJsonTAsync response = toJsonAsync response |> Async.StartAsTask -let toJson response = toJsonAsync response |> Async.RunSynchronously +let toJsonTAsync cancellationToken response = + Async.StartAsTask( + toJsonAsync response, + cancellationToken = cancellationToken) +let toJson response = + toJsonAsync response |> Async.RunSynchronously let toJsonArrayAsync response = async { @@ -26,5 +30,9 @@ let toJsonArrayAsync response = return res.AsArray() } -let toJsonArrayTAsync response = toJsonArrayAsync response |> Async.StartAsTask -let toJsonArray response = toJsonArrayAsync response |> Async.RunSynchronously +let toJsonArrayTAsync cancellationToken response = + Async.StartAsTask( + toJsonArrayAsync response, + cancellationToken = cancellationToken) +let toJsonArray response = + toJsonArrayAsync response |> Async.RunSynchronously diff --git a/src/FsHttp.NewtonsoftJson/FsHttp.NewtonsoftJson.fsproj b/src/FsHttp.NewtonsoftJson/FsHttp.NewtonsoftJson.fsproj index 8991af6d..2a99c878 100644 --- a/src/FsHttp.NewtonsoftJson/FsHttp.NewtonsoftJson.fsproj +++ b/src/FsHttp.NewtonsoftJson/FsHttp.NewtonsoftJson.fsproj @@ -1,6 +1,6 @@  - netstandard2.0;netstandard2.1;net6.0;net7.0;net8.0 + net6.0;net7.0;net8.0 false Debug;Release true @@ -19,7 +19,7 @@ - + diff --git a/src/FsHttp.NewtonsoftJson/Response.fs b/src/FsHttp.NewtonsoftJson/Response.fs index 4bcea321..53a3e517 100644 --- a/src/FsHttp.NewtonsoftJson/Response.fs +++ b/src/FsHttp.NewtonsoftJson/Response.fs @@ -25,32 +25,53 @@ let private loadJsonAsync loader response = ) let toJsonWithAsync settings response = response |> loadJsonAsync (fun jr ct -> JObject.LoadAsync(jr, settings, ct)) -let toJsonWithTAsync settings response = toJsonWithAsync settings response |> Async.StartAsTask +let toJsonWithTAsync settings cancellationToken response = + Async.StartAsTask( + toJsonWithAsync settings response, + cancellationToken = cancellationToken) let toJsonWith settings response = toJsonWithAsync settings response |> Async.RunSynchronously -let toJsonAsync response = toJsonWithAsync defaultJsonLoadSettings response -let toJsonTAsync response = toJsonWithTAsync defaultJsonLoadSettings response -let toJson response = toJsonWith defaultJsonLoadSettings response +let toJsonAsync response = + toJsonWithAsync defaultJsonLoadSettings response +let toJsonTAsync cancellationToken response = + toJsonWithTAsync defaultJsonLoadSettings cancellationToken response +let toJson response = + toJsonWith defaultJsonLoadSettings response let toJsonSeqWithAsync settings response = response |> loadJsonAsync (fun jr ct -> JArray.LoadAsync(jr, settings, ct)) |> Async.map (fun jarr -> jarr :> JToken seq) -let toJsonSeqWithTAsync settings response = toJsonSeqWithAsync settings response |> Async.StartAsTask -let toJsonSeqWith settings response = toJsonSeqWithAsync settings response |> Async.RunSynchronously - -let toJsonSeqAsync response = toJsonSeqWithAsync defaultJsonLoadSettings response -let toJsonSeqTAsync response = toJsonSeqWithTAsync defaultJsonLoadSettings response -let toJsonSeq response = toJsonSeqWith defaultJsonLoadSettings response - -let toJsonArrayWithAsync settings response = toJsonSeqWithAsync settings response |> Async.map Seq.toArray -let toJsonArrayWithTAsync settings response = toJsonArrayWithAsync settings response |> Async.StartAsTask -let toJsonArrayWith settings response = toJsonArrayWithAsync settings response |> Async.RunSynchronously - -let toJsonArrayAsync response = toJsonArrayWithAsync defaultJsonLoadSettings response -let toJsonArrayTAsync response = toJsonArrayWithTAsync defaultJsonLoadSettings response -let toJsonArray response = toJsonArrayWith defaultJsonLoadSettings response +let toJsonSeqWithTAsync settings cancellationToken response = + Async.StartAsTask( + toJsonSeqWithAsync settings response, + cancellationToken = cancellationToken) +let toJsonSeqWith settings response = + toJsonSeqWithAsync settings response |> Async.RunSynchronously + +let toJsonSeqAsync response = + toJsonSeqWithAsync defaultJsonLoadSettings response +let toJsonSeqTAsync cancellationToken response = + toJsonSeqWithTAsync defaultJsonLoadSettings cancellationToken response +let toJsonSeq response = + toJsonSeqWith defaultJsonLoadSettings response + +let toJsonArrayWithAsync settings response = + toJsonSeqWithAsync settings response |> Async.map Seq.toArray +let toJsonArrayWithTAsync settings cancellationToken response = + Async.StartAsTask( + toJsonArrayWithAsync settings response, + cancellationToken = cancellationToken) +let toJsonArrayWith settings response = + toJsonArrayWithAsync settings response |> Async.RunSynchronously + +let toJsonArrayAsync response = + toJsonArrayWithAsync defaultJsonLoadSettings response +let toJsonArrayTAsync cancellationToken response = + toJsonArrayWithTAsync defaultJsonLoadSettings cancellationToken response +let toJsonArray response = + toJsonArrayWith defaultJsonLoadSettings response let deserializeJsonWithAsync<'a> (settings: JsonSerializerSettings) response = async { @@ -58,11 +79,17 @@ let deserializeJsonWithAsync<'a> (settings: JsonSerializerSettings) response = return JsonConvert.DeserializeObject<'a>(json, settings) } -let deserializeWithJsonTAsync<'a> settings response = - deserializeJsonWithAsync<'a> settings response |> Async.StartAsTask +let deserializeWithJsonTAsync<'a> settings cancellationToken response = + Async.StartAsTask( + deserializeJsonWithAsync<'a> settings response, + cancellationToken = cancellationToken) -let deserializeWithJson<'a> settings response = deserializeJsonWithAsync<'a> settings response |> Async.RunSynchronously +let deserializeWithJson<'a> settings response = + deserializeJsonWithAsync<'a> settings response |> Async.RunSynchronously -let deserializeJsonAsync<'a> response = deserializeJsonWithAsync<'a> defaultJsonSerializerSettings response -let deserializeJsonTAsync<'a> response = deserializeWithJsonTAsync<'a> defaultJsonSerializerSettings response -let deserializeJson<'a> response = deserializeWithJson<'a> defaultJsonSerializerSettings response +let deserializeJsonAsync<'a> response = + deserializeJsonWithAsync<'a> defaultJsonSerializerSettings response +let deserializeJsonTAsync<'a> cancellationToken response = + deserializeWithJsonTAsync<'a> defaultJsonSerializerSettings cancellationToken response +let deserializeJson<'a> response = + deserializeWithJson<'a> defaultJsonSerializerSettings response diff --git a/src/FsHttp/Defaults.fs b/src/FsHttp/Defaults.fs index a4119c1b..4f633c56 100644 --- a/src/FsHttp/Defaults.fs +++ b/src/FsHttp/Defaults.fs @@ -12,17 +12,8 @@ let defaultJsonSerializerOptions = JsonSerializerOptions JsonSerializerDefaults. let defaultHttpClientFactory (config: Config) = let getDefaultClientHandler ignoreSslIssues = -#if NETSTANDARD2_0 || NETSTANDARD2_1 - let handler = new HttpClientHandler() - - if ignoreSslIssues then - handler.ServerCertificateCustomValidationCallback <- (fun msg cert chain errors -> true) - - handler -#else let handler = new SocketsHttpHandler(UseCookies = false, PooledConnectionLifetime = TimeSpan.FromMinutes 5.0) - if ignoreSslIssues then handler.SslOptions <- let options = Security.SslClientAuthenticationOptions() @@ -32,9 +23,7 @@ let defaultHttpClientFactory (config: Config) = do options.RemoteCertificateValidationCallback <- callback options - handler -#endif let ignoreSslIssues = match config.certErrorStrategy with @@ -72,14 +61,7 @@ let defaultHeadersAndBodyPrintMode = { maxLength = Some 7000 } -let defaultDecompressionMethods = [ -#if NETSTANDARD2_0 || NETSTANDARD2_1 - DecompressionMethods.Deflate - DecompressionMethods.GZip -#else - DecompressionMethods.All -#endif -] +let defaultDecompressionMethods = [ DecompressionMethods.All ] let defaultConfig = { timeout = None diff --git a/src/FsHttp/Domain.fs b/src/FsHttp/Domain.fs index e1f33d72..86ca3593 100644 --- a/src/FsHttp/Domain.fs +++ b/src/FsHttp/Domain.fs @@ -34,12 +34,7 @@ type Proxy = { credentials: System.Net.ICredentials option } -type HttpClientHandlerTransformer = -#if NETSTANDARD2_0 || NETSTANDARD2_1 - (System.Net.Http.HttpClientHandler -> System.Net.Http.HttpClientHandler) -#else - (System.Net.Http.SocketsHttpHandler -> System.Net.Http.SocketsHttpHandler) -#endif +type HttpClientHandlerTransformer = (System.Net.Http.SocketsHttpHandler -> System.Net.Http.SocketsHttpHandler) and Config = { timeout: System.TimeSpan option @@ -62,12 +57,12 @@ type ConfigTransformer = Config -> Config type PrintHintTransformer = PrintHint -> PrintHint type FsHttpUrl = { - address: string + address: string option additionalQueryParams: List } type Header = { - method: System.Net.Http.HttpMethod + method: System.Net.Http.HttpMethod option headers: Map // We use a .Net type here, which we never do in other places. // Since Cookie is record style, I see no problem here. diff --git a/src/FsHttp/DomainExtensions.fs b/src/FsHttp/DomainExtensions.fs index 19c5b373..967ca2eb 100644 --- a/src/FsHttp/DomainExtensions.fs +++ b/src/FsHttp/DomainExtensions.fs @@ -6,22 +6,23 @@ open System.Net.Http.Headers open FsHttp type FsHttpUrl with - member this.ToUriString() = - let uri = UriBuilder(this.address) - + member this.ToUriStringWithDefault(defaultValue) = let queryParamsString = this.additionalQueryParams |> Seq.map (fun (k, v) -> $"""{k}={Uri.EscapeDataString $"{v}"}""") |> String.concat "&" - uri.Query <- - match uri.Query, queryParamsString with - | "", "" -> "" - | s, "" -> s - | "", q -> $"?{q}" - | s, q -> $"{s}&{q}" - - uri.ToString() + match this.address with + | None -> defaultValue + | Some address -> + let uri = UriBuilder(address) + uri.Query <- + match uri.Query, queryParamsString with + | "", "" -> "" + | s, "" -> s + | "", q -> $"?{q}" + | s, q -> $"{s}&{q}" + uri.ToString() type ContentType with member this.ToMediaHeaderValue() = diff --git a/src/FsHttp/Dsl.CE.fs b/src/FsHttp/Dsl.CE.fs index 5af45230..75689226 100644 --- a/src/FsHttp/Dsl.CE.fs +++ b/src/FsHttp/Dsl.CE.fs @@ -12,23 +12,11 @@ open FsHttp type IRequestContext<'self> with member this.Yield(_) = this -type StartingContext = { - config: Config option -} with - member this.ActualConfig = - this.config |> Option.defaultValue GlobalConfig.defaults.Config - - interface IRequestContext with - member this.Self = this - - interface IConfigure with - member this.Configure(transformConfig) = { this with config = Some (transformConfig this.ActualConfig) } - - interface IConfigure with - member this.Configure(transformPrintHint) = configPrinter this transformPrintHint - -let http = { config = None } - +[] +type HttpBuilder = + // This is important to always have a new instance of the builder + // that uses the config present at the time of the call. + static member http = createHeaderContext None None GlobalConfig.defaults.Config // --------- // Methods @@ -37,45 +25,45 @@ let http = { config = None } type IRequestContext<'self> with [] - member this.Method(_: IRequestContext, method, url) = + member this.Method(_: IRequestContext, method, url) = Http.method method url // RFC 2626 specifies 8 methods [] - member this.Get(context: IRequestContext, url) = - getWithConfig context.Self.ActualConfig url + member this.Get(context: IRequestContext, url) = + getWithConfig context.Self.config url [] - member this.Put(context: IRequestContext, url) = - putWithConfig context.Self.ActualConfig url + member this.Put(context: IRequestContext, url) = + putWithConfig context.Self.config url [] - member this.Post(context: IRequestContext, url) = - postWithConfig context.Self.ActualConfig url + member this.Post(context: IRequestContext, url) = + postWithConfig context.Self.config url [] - member this.Delete(context: IRequestContext, url) = - deleteWithConfig context.Self.ActualConfig url + member this.Delete(context: IRequestContext, url) = + deleteWithConfig context.Self.config url [] - member this.Options(context: IRequestContext, url) = - optionsWithConfig context.Self.ActualConfig url + member this.Options(context: IRequestContext, url) = + optionsWithConfig context.Self.config url [] - member this.Head(context: IRequestContext, url) = - headWithConfig context.Self.ActualConfig url + member this.Head(context: IRequestContext, url) = + headWithConfig context.Self.config url [] - member this.Trace(context: IRequestContext, url) = - traceWithConfig context.Self.ActualConfig url + member this.Trace(context: IRequestContext, url) = + traceWithConfig context.Self.config url [] - member this.Connect(context: IRequestContext, url) = - connectWithConfig context.Self.ActualConfig url + member this.Connect(context: IRequestContext, url) = + connectWithConfig context.Self.config url [] - member this.Patch(context: IRequestContext, url) = - patchWithConfig context.Self.ActualConfig url + member this.Patch(context: IRequestContext, url) = + patchWithConfig context.Self.config url // --------- diff --git a/src/FsHttp/Dsl.fs b/src/FsHttp/Dsl.fs index 89fa85af..ddbc27ab 100644 --- a/src/FsHttp/Dsl.fs +++ b/src/FsHttp/Dsl.fs @@ -10,70 +10,79 @@ open System.Globalization open FsHttp open FsHttp.Helper -[] +[] module HttpMethods = - let GET = "GET" - let PUT = "PUT" - let POST = "POST" - let DELETE = "DELETE" - let OPTIONS = "OPTIONS" - let HEAD = "HEAD" - let TRACE = "TRACE" - let CONNECT = "CONNECT" - let PATCH = "PATCH" + let get = "GET" + let put = "PUT" + let post = "POST" + let delete = "DELETE" + let options = "OPTIONS" + let head = "HEAD" + let trace = "TRACE" + let connect = "CONNECT" + let patch = "PATCH" /// Request constructors for RFC 2626 HTTP methods [] module Http = - let methodWithConfig config (method: string) (url: string) = - + let createHeaderContext address method config = // FSI init HACK FsiInit.init () - if String.IsNullOrWhiteSpace url then - failwith "The given URL is empty." - - let formattedUrl = - url.Split([| '\n' |], StringSplitOptions.RemoveEmptyEntries) - |> Seq.map (fun x -> x.Trim().Replace("\r", "")) - |> Seq.filter (fun x -> not (x.StartsWith("//", StringComparison.Ordinal))) - |> Seq.reduce (+) - { url = { - address = formattedUrl + address = address additionalQueryParams = [] } header = { - method = HttpMethod(method) + method = method headers = Map.empty cookies = [] } config = config } + let methodWithConfig config (method: string) (url: string) = + if String.IsNullOrWhiteSpace(method) then + failwith "Method must not be empty" + if String.IsNullOrWhiteSpace(url) then + failwith "URL must not be empty" + + // TODO: See comment for type level safety + // We give up a little bit of safety here, for the sake of pre-configuring HTTP requests + // without specifying the URL. This is a trade-off we are willing to take. + let formattedUrl = + if String.IsNullOrWhiteSpace(url) then + "" + else + url.Split([| '\n' |], StringSplitOptions.RemoveEmptyEntries) + |> Seq.map (fun x -> x.Trim().Replace("\r", "")) + |> Seq.filter (fun x -> not (x.StartsWith("//", StringComparison.Ordinal))) + |> Seq.reduce (+) + createHeaderContext (Some formattedUrl) (Some (HttpMethod(method))) config + let method (method: string) (url: string) = methodWithConfig GlobalConfig.defaults.Config method url - let internal getWithConfig config (url: string) = methodWithConfig config HttpMethods.GET url - let internal putWithConfig config (url: string) = methodWithConfig config HttpMethods.PUT url - let internal postWithConfig config (url: string) = methodWithConfig config HttpMethods.POST url - let internal deleteWithConfig config (url: string) = methodWithConfig config HttpMethods.DELETE url - let internal optionsWithConfig config (url: string) = methodWithConfig config HttpMethods.OPTIONS url - let internal headWithConfig config (url: string) = methodWithConfig config HttpMethods.HEAD url - let internal traceWithConfig config (url: string) = methodWithConfig config HttpMethods.TRACE url - let internal connectWithConfig config (url: string) = methodWithConfig config HttpMethods.CONNECT url - let internal patchWithConfig config (url: string) = methodWithConfig config HttpMethods.PATCH url - - let get (url: string) = methodWithConfig GlobalConfig.defaults.Config HttpMethods.GET url - let put (url: string) = methodWithConfig GlobalConfig.defaults.Config HttpMethods.PUT url - let post (url: string) = methodWithConfig GlobalConfig.defaults.Config HttpMethods.POST url - let delete (url: string) = methodWithConfig GlobalConfig.defaults.Config HttpMethods.DELETE url - let options (url: string) = methodWithConfig GlobalConfig.defaults.Config HttpMethods.OPTIONS url - let head (url: string) = methodWithConfig GlobalConfig.defaults.Config HttpMethods.HEAD url - let trace (url: string) = methodWithConfig GlobalConfig.defaults.Config HttpMethods.TRACE url - let connect (url: string) = methodWithConfig GlobalConfig.defaults.Config HttpMethods.CONNECT url - let patch (url: string) = methodWithConfig GlobalConfig.defaults.Config HttpMethods.PATCH url + let internal getWithConfig config (url: string) = methodWithConfig config HttpMethods.get url + let internal putWithConfig config (url: string) = methodWithConfig config HttpMethods.put url + let internal postWithConfig config (url: string) = methodWithConfig config HttpMethods.post url + let internal deleteWithConfig config (url: string) = methodWithConfig config HttpMethods.delete url + let internal optionsWithConfig config (url: string) = methodWithConfig config HttpMethods.options url + let internal headWithConfig config (url: string) = methodWithConfig config HttpMethods.head url + let internal traceWithConfig config (url: string) = methodWithConfig config HttpMethods.trace url + let internal connectWithConfig config (url: string) = methodWithConfig config HttpMethods.connect url + let internal patchWithConfig config (url: string) = methodWithConfig config HttpMethods.patch url + + let get (url: string) = methodWithConfig GlobalConfig.defaults.Config HttpMethods.get url + let put (url: string) = methodWithConfig GlobalConfig.defaults.Config HttpMethods.put url + let post (url: string) = methodWithConfig GlobalConfig.defaults.Config HttpMethods.post url + let delete (url: string) = methodWithConfig GlobalConfig.defaults.Config HttpMethods.delete url + let options (url: string) = methodWithConfig GlobalConfig.defaults.Config HttpMethods.options url + let head (url: string) = methodWithConfig GlobalConfig.defaults.Config HttpMethods.head url + let trace (url: string) = methodWithConfig GlobalConfig.defaults.Config HttpMethods.trace url + let connect (url: string) = methodWithConfig GlobalConfig.defaults.Config HttpMethods.connect url + let patch (url: string) = methodWithConfig GlobalConfig.defaults.Config HttpMethods.patch url // TODO: RFC 4918 (WebDAV) adds 7 methods diff --git a/src/FsHttp/CSharp.fs b/src/FsHttp/Extensions.fs similarity index 69% rename from src/FsHttp/CSharp.fs rename to src/FsHttp/Extensions.fs index 2b445ea3..1fc9eace 100644 --- a/src/FsHttp/CSharp.fs +++ b/src/FsHttp/Extensions.fs @@ -1,7 +1,8 @@ -namespace FsHttp.CSharp +namespace FsHttp open System open System.Runtime.CompilerServices +open System.Threading open FsHttp @@ -10,7 +11,8 @@ open FsHttp // --------- [] -type Http = +type HttpExtensions = + [] static member Method(url, method) = Http.method method url @@ -74,7 +76,7 @@ type Http = // --------- [] -type Header = +type HeaderExtensions = /// Append query params [] @@ -83,11 +85,13 @@ type Header = /// Custom header [] - static member Header(context: IRequestContext, key, value) = Header.header key value context.Self + static member Header(context: IRequestContext, key, value) = + Header.header key value context.Self /// Content-Types that are acceptable for the response [] - static member Accept(context: IRequestContext, contentType) = Header.accept contentType context.Self + static member Accept(context: IRequestContext, contentType) = + Header.accept contentType context.Self /// Character sets that are acceptable [] @@ -154,29 +158,35 @@ type Header = /// The date and time that the message was sent [] - static member Date(context: IRequestContext, date) = Header.date date context.Self + static member Date(context: IRequestContext, date) = + Header.date date context.Self /// Indicates that particular server behaviors are required by the client [] - static member Expect(context: IRequestContext, behaviors) = Header.expect behaviors context.Self + static member Expect(context: IRequestContext, behaviors) = + Header.expect behaviors context.Self /// Gives the date/time after which the response is considered stale [] - static member Expires(context: IRequestContext, dateTime) = Header.expires dateTime context.Self + static member Expires(context: IRequestContext, dateTime) = + Header.expires dateTime context.Self /// The email address of the user making the request [] - static member From(context: IRequestContext, email) = Header.from email context.Self + static member From(context: IRequestContext, email) = + Header.from email context.Self /// The domain name of the server (for virtual hosting), and the TCP port number on which the server is listening. /// The port number may be omitted if the port is the standard port for the service requested. [] - static member Host(context: IRequestContext, host) = Header.host host context.Self + static member Host(context: IRequestContext, host) = + Header.host host context.Self /// Only perform the action if the client supplied entity matches the same entity on the server. /// This is mainly for methods like PUT to only update a resource if it has not been modified since the user last updated it. If-Match: "737060cd8c284d8af7ad3082f209582d" Permanent [] - static member IfMatch(context: IRequestContext, entity) = Header.ifMatch entity context.Self + static member IfMatch(context: IRequestContext, entity) = + Header.ifMatch entity context.Self /// Allows a 304 Not Modified to be returned if content is unchanged [] @@ -185,11 +195,13 @@ type Header = /// Allows a 304 Not Modified to be returned if content is unchanged [] - static member IfNoneMatch(context: IRequestContext, etag) = Header.ifNoneMatch etag context.Self + static member IfNoneMatch(context: IRequestContext, etag) = + Header.ifNoneMatch etag context.Self /// If the entity is unchanged, send me the part(s) that I am missing; otherwise, send me the entire new entity [] - static member IfRange(context: IRequestContext, range) = Header.ifRange range context.Self + static member IfRange(context: IRequestContext, range) = + Header.ifRange range context.Self /// Only send the response if the entity has not been modified since a specific time [] @@ -208,19 +220,23 @@ type Header = /// Limit the number of times the message can be forwarded through proxies or gateways [] - static member MaxForwards(context: IRequestContext, count) = Header.maxForwards count context.Self + static member MaxForwards(context: IRequestContext, count) = + Header.maxForwards count context.Self /// Initiates a request for cross-origin resource sharing (asks server for an 'Access-Control-Allow-Origin' response header) [] - static member Origin(context: IRequestContext, origin) = Header.origin origin context.Self + static member Origin(context: IRequestContext, origin) = + Header.origin origin context.Self /// Implementation-specific headers that may have various effects anywhere along the request-response chain. [] - static member Pragma(context: IRequestContext, pragma) = Header.pragma pragma context.Self + static member Pragma(context: IRequestContext, pragma) = + Header.pragma pragma context.Self /// Optional instructions to the server to control request processing. See RFC https://tools.ietf.org/html/rfc7240 for more details [] - static member Prefer(context: IRequestContext, prefer) = Header.prefer prefer context.Self + static member Prefer(context: IRequestContext, prefer) = + Header.prefer prefer context.Self /// Authorization credentials for connecting to a proxy. [] @@ -229,22 +245,26 @@ type Header = /// Request only part of an entity. Bytes are numbered from 0 [] - static member Range(context: IRequestContext, start, finish) = Header.range start finish context.Self + static member Range(context: IRequestContext, start, finish) = + Header.range start finish context.Self /// This is the address of the previous web page from which a link to the currently requested page was followed. /// (The word "referrer" is misspelled in the RFC as well as in most implementations.) [] - static member Referer(context: IRequestContext, referer) = Header.referer referer context.Self + static member Referer(context: IRequestContext, referer) = + Header.referer referer context.Self /// The transfer encodings the user agent is willing to accept: the same values as for the response header /// Transfer-Encoding can be used, plus the "trailers" value (related to the "chunked" transfer method) to /// notify the server it expects to receive additional headers (the trailers) after the last, zero-sized, chunk. [] - static member TE(context: IRequestContext, te) = Header.te te context.Self + static member TE(context: IRequestContext, te) = + Header.te te context.Self /// The Trailer general field value indicates that the given set of header fields is present in the trailer of a message encoded with chunked transfer-coding [] - static member Trailer(context: IRequestContext, trailer) = Header.trailer trailer context.Self + static member Trailer(context: IRequestContext, trailer) = + Header.trailer trailer context.Self /// The TransferEncoding header indicates the form of encoding used to safely transfer the entity to the user. /// The valid directives are one of: chunked, compress, deflate, gzip, orentity. @@ -259,7 +279,8 @@ type Header = /// Specifies additional communications protocols that the client supports. [] - static member Upgrade(context: IRequestContext, upgrade) = Header.upgrade upgrade context.Self + static member Upgrade(context: IRequestContext, upgrade) = + Header.upgrade upgrade context.Self /// The user agent string of the user agent [] @@ -268,11 +289,13 @@ type Header = /// Informs the server of proxies through which the request was sent [] - static member Via(context: IRequestContext, server) = Header.via server context.Self + static member Via(context: IRequestContext, server) = + Header.via server context.Self /// A general warning about possible problems with the entity body [] - static member Warning(context: IRequestContext, message) = Header.warning message context.Self + static member Warning(context: IRequestContext, message) = + Header.warning message context.Self /// Override HTTP method. [] @@ -285,40 +308,48 @@ type Header = // --------- [] -type Body = +type BodyExtensions = /// An explicit transformation from a previous context to allow for describing the request body. [] - static member Body(context: IRequestContext<#IToBodyContext>) = context.Self.Transform() + static member Body(context: IRequestContext<#IToBodyContext>) = + context.Self.Transform() [] static member Content(context: IRequestContext, contentType, data) = Body.content contentType data context.Self [] - static member Binary(context: IRequestContext, data) = Body.binary data context.Self + static member Binary(context: IRequestContext, data) = + Body.binary data context.Self [] - static member Stream(context: IRequestContext, stream) = Body.stream stream context.Self + static member Stream(context: IRequestContext, stream) = + Body.stream stream context.Self [] - static member Text(context: IRequestContext, text) = Body.text text context.Self + static member Text(context: IRequestContext, text) = + Body.text text context.Self [] - static member Json(context: IRequestContext, json) = Body.json json context.Self + static member Json(context: IRequestContext, json) = + Body.json json context.Self [] static member JsonSerializeWith(context: IRequestContext, options, json) = Body.jsonSerializeWith options json context.Self [] - static member JsonSerialize(context: IRequestContext, json) = Body.jsonSerialize json context.Self + static member JsonSerialize(context: IRequestContext, json) = + Body.jsonSerialize json context.Self [] - static member FormUrlEncoded(context: IRequestContext, data) = Body.formUrlEncoded data context.Self + static member FormUrlEncoded(context: IRequestContext, data) = + Body.formUrlEncoded data context.Self [] - static member File(context: IRequestContext, path) = Body.file path context.Self + static member File(context: IRequestContext, path) = + Body.file path context.Self /// The type of encoding used on the data [] @@ -336,7 +367,7 @@ type Body = // ----------------- [] -type MultipartElement = +type MultipartElementExtensions = /// The MIME type of the body of the request (used with POST and PUT requests) [] @@ -349,11 +380,12 @@ type MultipartElement = // --------- [] -type Multipart = +type MultipartExtensions = /// An explicit transformation from a previous context to allow for describing the request multiparts. [] - static member Multipart(context: IRequestContext<#IToMultipartContext>) = context.Self.Transform() + static member Multipart(context: IRequestContext<#IToMultipartContext>) = + context.Self.Transform() [] static member TextPart(context: IRequestContext, value, name, ?fileName) = @@ -376,33 +408,36 @@ type Multipart = // Config // --------- -type ConfigTransformer = Func +type ConfigTransformerFunc = Func [] -type Config = +type ConfigExtensions = [] - static member Configure(context: HeaderContext, configTransformer: ConfigTransformer) = + static member Configure(context: HeaderContext, configTransformer: ConfigTransformerFunc) = Config.update configTransformer.Invoke context [] - static member Configure(context: BodyContext, configTransformer: ConfigTransformer) = + static member Configure(context: BodyContext, configTransformer: ConfigTransformerFunc) = Config.update configTransformer.Invoke context [] - static member Configure(context: MultipartContext, configTransformer: ConfigTransformer) = + static member Configure(context: MultipartContext, configTransformer: ConfigTransformerFunc) = Config.update configTransformer.Invoke context // ---------------- [] - static member IgnoreCertIssues(config: Domain.Config) = Config.With.ignoreCertIssues config + static member IgnoreCertIssues(config: Domain.Config) = + Config.With.ignoreCertIssues config [] - static member Timeout(config: Domain.Config, value) = Config.With.timeout value config + static member Timeout(config: Domain.Config, value) = + Config.With.timeout value config [] - static member TimeoutInSeconds(config: Domain.Config, value) = Config.With.timeoutInSeconds value config + static member TimeoutInSeconds(config: Domain.Config, value) = + Config.With.timeoutInSeconds value config [] static member SetHttpClientFactory(config: Domain.Config, httpClientFactory) = @@ -421,7 +456,8 @@ type Config = Config.With.transformHttpClientHandler transformer config [] - static member Proxy(config: Domain.Config, url) = Config.With.proxy url config + static member Proxy(config: Domain.Config, url) = + Config.With.proxy url config [] static member ProxyWithCredentials(config: Domain.Config, url, credentials) = @@ -432,7 +468,8 @@ type Config = Config.With.decompressionMethods decompressionMethods config [] - static member NoDecompression(config: Domain.Config) = Config.With.noDecompression config + static member NoDecompression(config: Domain.Config) = + Config.With.noDecompression config [] static member CancellationToken(config: Domain.Config, cancellationToken) = @@ -444,13 +481,19 @@ type Config = // --------- [] -type Request = +type RequestExtensions = + + [] + static member ToHttpRequestMessage(request: IToRequest) = + Request.toHttpRequestMessage request [] - static member ToHttpRequestMessage(request: IToRequest) = Request.toHttpRequestMessage request + static member SendAsync(request: IToRequest) = + request |> Request.sendTAsync [] - static member SendAsync(request: IToRequest) = Request.sendTAsync request + static member SendAsync(request: IToRequest, cancellationToken: CancellationToken) = + request |> Request.toAsync (Some cancellationToken) |> Async.StartAsTask // --------- @@ -458,55 +501,127 @@ type Request = // --------- [] -type Response = +type ResponseExtensions = + + [] + static member ToStreamAsync(response: Domain.Response) = + response |> Response.toStreamTAsync CancellationToken.None + + [] + static member ToStreamAsync(response: Domain.Response, cancellationToken: CancellationToken) = + response |> Response.toStreamTAsync cancellationToken + + [] + static member ToBytesAsync(response: Domain.Response) = + response |> Response.toBytesTAsync CancellationToken.None [] - static member ToStreamAsync(response: Domain.Response) = Response.toStreamTAsync response + static member ToBytesAsync(response: Domain.Response, cancellationToken: CancellationToken) = + response |> Response.toBytesTAsync cancellationToken [] - static member ToBytesAsync(response: Domain.Response) = Response.toBytesTAsync response + static member ToStringAsync(response: Domain.Response, maxLength) = + response |> Response.toStringTAsync maxLength CancellationToken.None [] - static member ToStringAsync(response: Domain.Response, maxLength) = Response.toStringTAsync maxLength response + static member ToStringAsync(response: Domain.Response, maxLength, cancellationToken: CancellationToken) = + response |> Response.toStringTAsync maxLength cancellationToken [] - static member ToTextAsync(response: Domain.Response) = Response.toTextTAsync response + static member ToTextAsync(response: Domain.Response) = + response |> Response.toTextTAsync CancellationToken.None [] - static member ToXmlAsync(response: Domain.Response) = Response.toXmlTAsync response + static member ToTextAsync(response: Domain.Response, cancellationToken: CancellationToken) = + response |> Response.toTextTAsync cancellationToken + + [] + static member ToXmlAsync(response: Domain.Response) = + response |> Response.toXmlTAsync CancellationToken.None + + [] + static member ToXmlAsync(response: Domain.Response, cancellationToken: CancellationToken) = + response |> Response.toXmlTAsync cancellationToken [] static member ToJsonDocumentWithAsync(response: Domain.Response, options) = - Response.toJsonDocumentWithTAsync options response + response |> Response.toJsonDocumentWithTAsync options CancellationToken.None + + [] + static member ToJsonDocumentWithAsync(response: Domain.Response, options, cancellationToken: CancellationToken) = + response |> Response.toJsonDocumentWithTAsync options cancellationToken + + [] + static member ToJsonDocumenAsync(response: Domain.Response) = + response |> Response.toJsonDocumentTAsync CancellationToken.None [] - static member ToJsonDocumentAsync(response: Domain.Response) = Response.toJsonDocumentTAsync response + static member ToJsonDocumenAsync(response: Domain.Response, cancellationToken: CancellationToken) = + response |> Response.toJsonDocumentTAsync cancellationToken [] - static member ToJsonWithAsync(response: Domain.Response, options) = Response.toJsonWithTAsync options response + static member ToJsonWithAsync(response: Domain.Response, options) = + response |> Response.toJsonWithTAsync options CancellationToken.None [] - static member ToJsonAsync(response: Domain.Response) = Response.toJsonTAsync response + static member ToJsonWithAsync(response: Domain.Response, options, cancellationToken: CancellationToken) = + response |> Response.toJsonWithTAsync options cancellationToken + + [] + static member ToJsonAsync(response: Domain.Response) = + response |> Response.toJsonTAsync CancellationToken.None + + [] + static member ToJsonAsync(response: Domain.Response, cancellationToken: CancellationToken) = + response |> Response.toJsonTAsync cancellationToken [] static member ToJsonEnumerableWithAsync(response: Domain.Response, options) = - Response.toJsonSeqWithTAsync options response + response |> Response.toJsonSeqWithTAsync options CancellationToken.None + + [] + static member ToJsonEnumerableWithAsync(response: Domain.Response, options, cancellationToken: CancellationToken) = + response |> Response.toJsonSeqWithTAsync options cancellationToken + + [] + static member ToJsonEnumerableAsync(response: Domain.Response) = + response |> Response.toJsonSeqTAsync CancellationToken.None [] - static member ToJsonEnumerableAsync(response: Domain.Response) = Response.toJsonSeqTAsync response + static member ToJsonEnumerableAsync(response: Domain.Response, cancellationToken: CancellationToken) = + response |> Response.toJsonSeqTAsync cancellationToken [] static member DeserializeJsonWithAsync<'T>(response: Domain.Response, options) = - Response.deserializeJsonWithTAsync options response + response |> Response.deserializeJsonWithTAsync options CancellationToken.None + + [] + static member DeserializeJsonWithAsync<'T>(response: Domain.Response, options, cancellationToken: CancellationToken) = + response |> Response.deserializeJsonWithTAsync options cancellationToken + + [] + static member DeserializeJsonAsync(response: Domain.Response) = + response |> Response.deserializeJsonTAsync CancellationToken.None + + [] + static member DeserializeJsonAsync(response: Domain.Response, cancellationToken: CancellationToken) = + response |> Response.deserializeJsonTAsync cancellationToken + + [] + static member ToFormattedTexAsync(response: Domain.Response) = + response |> Response.toFormattedTextTAsync CancellationToken.None [] - static member DeserializeJsonAsync(response: Domain.Response) = Response.deserializeJsonTAsync response + static member ToFormattedTexAsync(response: Domain.Response, cancellationToken: CancellationToken) = + response |> Response.toFormattedTextTAsync cancellationToken [] - static member ToFormattedTextAsync(response: Domain.Response) = Response.toFormattedTextTAsync response + static member SaveFileAsync(response: Domain.Response, fileName) = + response |> Response.saveFileTAsync fileName CancellationToken.None [] - static member SaveFileAsync(response: Domain.Response, fileName) = Response.saveFileTAsync fileName response + static member SaveFileAsync(response: Domain.Response, fileName, cancellationToken: CancellationToken) = + response |> Response.saveFileTAsync fileName cancellationToken [] static member AssertStatusCodes(response: Domain.Response, statusCodes) = @@ -517,4 +632,5 @@ type Response = Response.assertStatusCode statusCode response [] - static member AssertOk(response: Domain.Response) = Response.assertOk response + static member AssertOk(response: Domain.Response) = + Response.assertOk response diff --git a/src/FsHttp/FsHttp.fsproj b/src/FsHttp/FsHttp.fsproj index aff54574..3423ec48 100644 --- a/src/FsHttp/FsHttp.fsproj +++ b/src/FsHttp/FsHttp.fsproj @@ -1,6 +1,6 @@ - netstandard2.0;netstandard2.1;net6.0;net7.0;net8.0 + net6.0;net7.0;net8.0 false Debug;Release true @@ -31,13 +31,10 @@ - - - - + - + \ No newline at end of file diff --git a/src/FsHttp/Helper.fs b/src/FsHttp/Helper.fs index 6f0c6cff..5aeabf79 100644 --- a/src/FsHttp/Helper.fs +++ b/src/FsHttp/Helper.fs @@ -100,11 +100,7 @@ type Utf8StringBufferingStream(baseStream: Stream, readBufferLimit: int option) and set (_) = notSeekable () member _.GetUtf8String() = -#if NETSTANDARD2_0 || NETSTANDARD2_1 - let buffer = readBuffer |> Seq.toArray -#else let buffer = CollectionsMarshal.AsSpan(readBuffer) -#endif let s = Encoding.UTF8.GetString(buffer).AsSpan() if s.Length = 0 then diff --git a/src/FsHttp/Print.fs b/src/FsHttp/Print.fs index eab5ccbb..b7f4d28c 100644 --- a/src/FsHttp/Print.fs +++ b/src/FsHttp/Print.fs @@ -32,7 +32,7 @@ let private doPrintRequestOnly (httpVersion: string) (request: Request) (request let requestPrintHint = request.config.printHint.requestPrintMode do sb.appendSection "REQUEST" - do sb.appendLine $"{request.header.method} {request.url.ToUriString()} HTTP/{httpVersion}" + do sb.appendLine $"{Request.addressToString request} HTTP/{httpVersion}" let printRequestHeaders () = let contentHeaders, multipartHeaders = diff --git a/src/FsHttp/Request.fs b/src/FsHttp/Request.fs index 47d563e8..c9c3facc 100644 --- a/src/FsHttp/Request.fs +++ b/src/FsHttp/Request.fs @@ -7,10 +7,24 @@ open System.Net.Http.Headers open FsHttp open FsHttp.Helper +// TODO: Remove this +let getAddressDefaults (request: Request) = + let uri = request.url.ToUriStringWithDefault("") + let method = request.header.method |> Option.defaultValue HttpMethod.Get + uri, method + +let addressToString (request: Request) = + let uri, method = getAddressDefaults request + $"{method} {uri}" + /// Transforms a Request into a System.Net.Http.HttpRequestMessage. let toRequestAndMessage (request: IToRequest) : Request * HttpRequestMessage = let request = request.Transform() - let requestMessage = new HttpRequestMessage(request.header.method, request.url.ToUriString()) + + // TODO: Try to encode URL / HTTP method presence or absence on type level. + let uri, method = getAddressDefaults request + + let requestMessage = new HttpRequestMessage(method, uri) let buildDotnetContent (part: ContentData) @@ -114,16 +128,20 @@ let toRequest request = request |> toRequestAndMessage |> fst let toHttpRequestMessage request = request |> toRequestAndMessage |> snd /// Builds an asynchronous request, without sending it. -let toAsync (context: IToRequest) = +let toAsync cancellationTokenOverride (context: IToRequest) = async { let request, requestMessage = toRequestAndMessage context - do Fsi.logfn $"Sending request {request.header.method} {request.url.ToUriString()} ..." + do Fsi.logfn $"Sending request {addressToString request} ..." use finalRequestMessage = request.config.httpMessageTransformers |> List.fold (fun c n -> n c) requestMessage - let ctok = request.config.cancellationToken + // cancellationTokenOverride: Because of C# interop (see Extensions) + let ctok = + match cancellationTokenOverride with + | Some ctok -> ctok + | None -> request.config.cancellationToken let client = request.config.httpClientFactory request.config match request.header.cookies with @@ -143,9 +161,7 @@ let toAsync (context: IToRequest) = // Task is started immediately, but must not be awaited when running in background. response.Content.LoadIntoBufferAsync() |> ignore - do - Fsi.logfn - $"{response.StatusCode |> int} ({response.StatusCode}) ({request.header.method} {request.url.ToUriString()})" + do Fsi.logfn $"{response.StatusCode |> int} ({response.StatusCode}) ({addressToString request})" let dispose () = do finalClient.Dispose() @@ -167,10 +183,10 @@ let toAsync (context: IToRequest) = } /// Sends a request asynchronously. -let sendTAsync (context: IToRequest) = context |> toAsync |> Async.StartAsTask +let sendTAsync (request: IToRequest) = request |> toAsync None |> Async.StartAsTask /// Sends a request asynchronously. -let sendAsync (context: IToRequest) = sendTAsync context |> Async.AwaitTask +let sendAsync (request: IToRequest) = request |> sendTAsync |> Async.AwaitTask /// Sends a request synchronously. -let send context = context |> toAsync |> Async.RunSynchronously +let send request = request |> toAsync None |> Async.RunSynchronously diff --git a/src/FsHttp/Response.fs b/src/FsHttp/Response.fs index 5dcddb86..71839582 100644 --- a/src/FsHttp/Response.fs +++ b/src/FsHttp/Response.fs @@ -5,6 +5,7 @@ open System.IO open System.Net open System.Text open System.Text.Json +open System.Threading open System.Xml.Linq open FsHttp @@ -24,9 +25,14 @@ let loadContent (response: Response) = response.content.LoadIntoBufferAsync() |> ignore response -let toStreamTAsync response = response.content.ReadAsStreamAsync() -let toStreamAsync response = toStreamTAsync response |> Async.AwaitTask -let toStream response = (toStreamTAsync response).Result +// TODO: The CTok passing and Async<->Task conversion is really chaotic and needs treatment. + +let toStreamTAsync (cancellationToken: CancellationToken) (response: Response) = + response.content.ReadAsStreamAsync(cancellationToken) +let toStreamAsync response = + toStreamTAsync CancellationToken.None response |> Async.AwaitTask +let toStream response = + (toStreamTAsync CancellationToken.None response).Result let parseAsync parserName parse response = async { @@ -51,8 +57,12 @@ let toBytesAsync response = return! stream |> Stream.toBytesAsync } -let toBytesTAsync response = toBytesAsync response |> Async.StartAsTask -let toBytes response = toBytesAsync response |> Async.RunSynchronously +let toBytesTAsync cancellationToken response = + Async.StartAsTask( + toBytesAsync response, + cancellationToken = cancellationToken) +let toBytes response = + toBytesAsync response |> Async.RunSynchronously let private toStringWithLengthAsync maxLength response = async { @@ -65,25 +75,33 @@ let private toStringWithLengthAsync maxLength response = | Some maxLength -> return! stream |> Stream.readUtf8StringAsync maxLength } -let toStringAsync maxLength response = toStringWithLengthAsync maxLength response -let toStringTAsync maxLength response = toStringAsync maxLength response |> Async.StartAsTask -let toString maxLength response = toStringAsync maxLength response |> Async.RunSynchronously - -let toTextAsync response = toStringWithLengthAsync None response -let toTextTAsync response = toTextAsync response |> Async.StartAsTask -let toText response = toTextAsync response |> Async.RunSynchronously +let toStringAsync maxLength response = + toStringWithLengthAsync maxLength response +let toStringTAsync maxLength cancellationToken response = + Async.StartAsTask( + toStringAsync maxLength response, + cancellationToken = cancellationToken) +let toString maxLength response = + toStringAsync maxLength response |> Async.RunSynchronously + +let toTextAsync response = + toStringWithLengthAsync None response +let toTextTAsync cancellationToken response = + Async.StartAsTask( + toTextAsync response, + cancellationToken = cancellationToken) +let toText response = + toTextAsync response |> Async.RunSynchronously -#if NETSTANDARD2_0 -let toXmlAsync response = - response - |> parseAsync "XML" (fun stream ct -> async { return XDocument.Load(stream, LoadOptions.SetLineInfo) }) -#else let toXmlAsync response = response |> parseAsync "XML" (fun stream ct -> XDocument.LoadAsync(stream, LoadOptions.SetLineInfo, ct) |> Async.AwaitTask) -#endif -let toXmlTAsync response = toXmlAsync response |> Async.StartAsTask -let toXml response = toXmlAsync response |> Async.RunSynchronously +let toXmlTAsync cancellationToken response = + Async.StartAsTask( + toXmlAsync response, + cancellationToken = cancellationToken) +let toXml response = + toXmlAsync response |> Async.RunSynchronously // TODO: toHtml @@ -103,42 +121,71 @@ let toJsonDocumentWithAsync options response = |> Async.AwaitTask ) -let toJsonDocumentWithTAsync options response = toJsonDocumentWithAsync options response |> Async.StartAsTask -let toJsonDocumentWith options response = toJsonDocumentWithAsync options response |> Async.RunSynchronously +let toJsonDocumentWithTAsync options cancellationToken response = + Async.StartAsTask( + toJsonDocumentWithAsync options response, + cancellationToken = cancellationToken) +let toJsonDocumentWith options response = + toJsonDocumentWithAsync options response |> Async.RunSynchronously -let toJsonDocumentAsync response = toJsonDocumentWithAsync defaultJsonDocumentOptions response -let toJsonDocumentTAsync response = toJsonDocumentWithTAsync defaultJsonDocumentOptions response -let toJsonDocument response = toJsonDocumentWith defaultJsonDocumentOptions response +let toJsonDocumentAsync response = + toJsonDocumentWithAsync defaultJsonDocumentOptions response +let toJsonDocumentTAsync cancellationToken response = + toJsonDocumentWithTAsync defaultJsonDocumentOptions cancellationToken response +let toJsonDocument response = + toJsonDocumentWith defaultJsonDocumentOptions response let toJsonWithAsync options response = toJsonDocumentWithAsync options response |> Async.map (fun doc -> doc.RootElement) -let toJsonWithTAsync options response = toJsonWithAsync options response |> Async.StartAsTask -let toJsonWith options response = toJsonWithAsync options response |> Async.RunSynchronously +let toJsonWithTAsync options cancellationToken response = + Async.StartAsTask( + toJsonWithAsync options response, + cancellationToken = cancellationToken) +let toJsonWith options response = + toJsonWithAsync options response |> Async.RunSynchronously -let toJsonAsync response = toJsonWithAsync defaultJsonDocumentOptions response -let toJsonTAsync response = toJsonWithTAsync defaultJsonDocumentOptions response -let toJson response = toJsonWith defaultJsonDocumentOptions response +let toJsonAsync response = + toJsonWithAsync defaultJsonDocumentOptions response +let toJsonTAsync cancellationToken response = + toJsonWithTAsync defaultJsonDocumentOptions cancellationToken response +let toJson response = + toJsonWith defaultJsonDocumentOptions response let toJsonSeqWithAsync options response = toJsonWithAsync options response |> Async.map (fun json -> json.EnumerateArray()) -let toJsonSeqWithTAsync options response = toJsonSeqWithAsync options response |> Async.StartAsTask -let toJsonSeqWith options response = toJsonSeqWithAsync options response |> Async.RunSynchronously - -let toJsonSeqAsync response = toJsonSeqWithAsync defaultJsonDocumentOptions response -let toJsonSeqTAsync response = toJsonSeqWithTAsync defaultJsonDocumentOptions response -let toJsonSeq response = toJsonSeqWith defaultJsonDocumentOptions response - -let toJsonArrayWithAsync options response = toJsonSeqWithAsync options response |> Async.map Seq.toArray -let toJsonArrayWithTAsync options response = toJsonArrayWithAsync options response |> Async.StartAsTask -let toJsonArrayWith options response = toJsonArrayWithAsync options response |> Async.RunSynchronously - -let toJsonArrayAsync response = toJsonArrayWithAsync defaultJsonDocumentOptions response -let toJsonArrayTAsync response = toJsonArrayWithTAsync defaultJsonDocumentOptions response -let toJsonArray response = toJsonArrayWith defaultJsonDocumentOptions response +let toJsonSeqWithTAsync options cancellationToken response = + Async.StartAsTask( + toJsonSeqWithAsync options response, + cancellationToken = cancellationToken) +let toJsonSeqWith options response = + toJsonSeqWithAsync options response |> Async.RunSynchronously + +let toJsonSeqAsync response = + toJsonSeqWithAsync defaultJsonDocumentOptions response +let toJsonSeqTAsync cancellationToken response = + toJsonSeqWithTAsync defaultJsonDocumentOptions cancellationToken response +let toJsonSeq response = + toJsonSeqWith defaultJsonDocumentOptions response + +let toJsonArrayWithAsync options response = + toJsonSeqWithAsync options response |> Async.map Seq.toArray +let toJsonArrayWithTAsync options cancellationToken response = + Async.StartAsTask( + toJsonArrayWithAsync options response, + cancellationToken = cancellationToken) +let toJsonArrayWith options response = + toJsonArrayWithAsync options response |> Async.RunSynchronously + +let toJsonArrayAsync response = + toJsonArrayWithAsync defaultJsonDocumentOptions response +let toJsonArrayTAsync cancellationToken response = + toJsonArrayWithTAsync defaultJsonDocumentOptions cancellationToken response +let toJsonArray response = + toJsonArrayWith defaultJsonDocumentOptions response let deserializeJsonWithAsync<'a> options response = async { @@ -153,12 +200,20 @@ let deserializeJsonWithAsync<'a> options response = } -let deserializeJsonWithTAsync<'a> options response = deserializeJsonWithAsync<'a> options response |> Async.StartAsTask -let deserializeJsonWith<'a> options response = deserializeJsonWithAsync<'a> options response |> Async.RunSynchronously +let deserializeJsonWithTAsync<'a> options cancellationToken response = + Async.StartAsTask( + deserializeJsonWithAsync<'a> options response, + cancellationToken = cancellationToken) + +let deserializeJsonWith<'a> options response = + deserializeJsonWithAsync<'a> options response |> Async.RunSynchronously -let deserializeJsonAsync<'a> response = deserializeJsonWithAsync<'a> defaultJsonSerializerOptions response -let deserializeJsonTAsync<'a> response = deserializeJsonWithTAsync<'a> defaultJsonSerializerOptions response -let deserializeJson<'a> response = deserializeJsonWith<'a> defaultJsonSerializerOptions response +let deserializeJsonAsync<'a> response = + deserializeJsonWithAsync<'a> defaultJsonSerializerOptions response +let deserializeJsonTAsync<'a> cancellationToken response = + deserializeJsonWithTAsync<'a> defaultJsonSerializerOptions cancellationToken response +let deserializeJson<'a> response = + deserializeJsonWith<'a> defaultJsonSerializerOptions response // ----------- @@ -194,8 +249,12 @@ let toFormattedTextAsync response = return! toTextAsync response } -let toFormattedTextTAsync response = toFormattedTextAsync response |> Async.StartAsTask -let toFormattedText response = toFormattedTextAsync response |> Async.RunSynchronously +let toFormattedTextTAsync cancellationToken response = + Async.StartAsTask( + toFormattedTextAsync response, + cancellationToken = cancellationToken) +let toFormattedText response = + toFormattedTextAsync response |> Async.RunSynchronously // ----------- @@ -214,8 +273,12 @@ let saveFileAsync (fileName: string) response = do! stream |> Stream.saveFileAsync fileName } -let saveFileTAsync (fileName: string) response = saveFileAsync fileName response |> Async.StartAsTask -let saveFile (fileName: string) response = saveFileAsync fileName response |> Async.RunSynchronously +let saveFileTAsync (fileName: string) cancellationToken response = + Async.StartAsTask( + saveFileAsync fileName response, + cancellationToken = cancellationToken) +let saveFile (fileName: string) response = + saveFileAsync fileName response |> Async.RunSynchronously // ----------- diff --git a/src/Test.CSharp/BasicTests.cs b/src/Test.CSharp/BasicTests.cs index 77bcfe4b..1ebaf1cb 100644 --- a/src/Test.CSharp/BasicTests.cs +++ b/src/Test.CSharp/BasicTests.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using FsHttp.CSharp; +using FsHttp; using FsHttp.Tests; using NUnit.Framework; @@ -25,7 +25,7 @@ public async Task PostsText() .SendAsync()) .ToTextAsync(); - Assert.AreEqual(Content, response); + Assert.That(Content, Is.EqualTo(response)); } public record Person(string Name, string Job); @@ -44,7 +44,7 @@ public async Task PostsJsonObject() .SendAsync()) .DeserializeJsonAsync(); - Assert.AreEqual(jsonObj, response); + Assert.That(jsonObj, Is.EqualTo(response)); } [Test] diff --git a/src/Test.CSharp/Test.CSharp.csproj b/src/Test.CSharp/Test.CSharp.csproj index 4e06c05d..0d4e4f13 100644 --- a/src/Test.CSharp/Test.CSharp.csproj +++ b/src/Test.CSharp/Test.CSharp.csproj @@ -7,6 +7,10 @@ false
+ + + + diff --git a/src/Tests/BuildersAndSignatures.fs b/src/Tests/BuildersAndSignatures.fs index 2a3d88fa..bf358a8b 100644 --- a/src/Tests/BuildersAndSignatures.fs +++ b/src/Tests/BuildersAndSignatures.fs @@ -10,7 +10,7 @@ let signatures () = let _: IToRequest = http { GET "" } let _: Request = http { GET "" } |> Request.toRequest let _: HttpRequestMessage = http { GET "" } |> Request.toHttpRequestMessage - let _: Async = http { GET "" } |> Request.toAsync + let _: Async = http { GET "" } |> Request.toAsync None let _: Async = http { GET "" } |> Request.sendAsync let _: Response = http { GET "" } |> Request.send () diff --git a/src/Tests/Tests.fsproj b/src/Tests/Tests.fsproj index 631db9d4..7bc4955c 100644 --- a/src/Tests/Tests.fsproj +++ b/src/Tests/Tests.fsproj @@ -39,7 +39,7 @@ - +