From d3bf9a23b6927d6f6f582d74f097191f57f6fa9f Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Fri, 12 Jun 2020 10:33:02 +0100 Subject: [PATCH 01/10] Possible fix for #424 --- src/Giraffe/Common.fs | 3 +++ src/Giraffe/Preconditional.fs | 10 ++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Giraffe/Common.fs b/src/Giraffe/Common.fs index ac7a55cf..84414ef0 100644 --- a/src/Giraffe/Common.fs +++ b/src/Giraffe/Common.fs @@ -67,6 +67,9 @@ type DateTimeOffset with /// member this.ToIsoString() = this.ToString("o") + member this.CutOffMs() = + DateTimeOffset(this.Year, this.Month, this.Day, this.Hour, this.Minute, this.Second, 0, this.Offset) + // --------------------------- // Common helper functions // --------------------------- diff --git a/src/Giraffe/Preconditional.fs b/src/Giraffe/Preconditional.fs index 7718afc9..4290c5b1 100644 --- a/src/Giraffe/Preconditional.fs +++ b/src/Giraffe/Preconditional.fs @@ -58,7 +58,8 @@ type HttpContext with match lastModified with | None -> AllConditionsMet | Some lastModified -> - match requestHeaders.IfUnmodifiedSince.Value > DateTimeOffset.UtcNow + let lastModified = lastModified.CutOffMs() + match requestHeaders.IfUnmodifiedSince.Value > DateTimeOffset.UtcNow.CutOffMs() || requestHeaders.IfUnmodifiedSince.Value >= lastModified with | true -> AllConditionsMet | false -> ConditionFailed @@ -88,7 +89,8 @@ type HttpContext with match lastModified with | None -> AllConditionsMet | Some lastModified -> - match requestHeaders.IfModifiedSince.Value <= DateTimeOffset.UtcNow + let lastModified = lastModified.CutOffMs() + match requestHeaders.IfModifiedSince.Value <= DateTimeOffset.UtcNow.CutOffMs() && requestHeaders.IfModifiedSince.Value < lastModified with | true -> AllConditionsMet | false -> ResourceNotModified @@ -142,8 +144,8 @@ type HttpContext with | ResourceNotModified -> ResourceNotModified // Set ETag and Last-Modified in the response - if eTag.IsSome then responseHeaders.ETag <- eTag.Value - if lastModified.IsSome then responseHeaders.LastModified <- Nullable(lastModified.Value) + if eTag.IsSome then responseHeaders.ETag <- eTag.Value + if lastModified.IsSome then responseHeaders.LastModified <- Nullable(lastModified.Value.CutOffMs()) // Validate headers in correct precedence // RFC: https://tools.ietf.org/html/rfc7232#section-6 From e66f52d616623b2d98bc7bfea2d77c84180731b3 Mon Sep 17 00:00:00 2001 From: Daniel Pozmanter Date: Sun, 19 Jul 2020 15:59:05 -0400 Subject: [PATCH 02/10] Adding basic testing information to the documentation --- DOCUMENTATION.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 2fe1996b..cd567156 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -41,6 +41,7 @@ An in depth functional reference to all of Giraffe's default features. - [Serialization](#serialization) - [JSON](#json) - [XML](#xml) +- [Testing](#testing) - [Miscellaneous](#miscellaneous) - [Short GUIDs and Short IDs](#short-guids-and-short-ids) - [Common Helper Functions](#common-helper-functions) @@ -3124,6 +3125,55 @@ let customHandler (dataObj : obj) : HttpHandler = // ... do more... ``` +## Testing + +Testing a Giraffe application builds on [ASP.NET Core testing](https://docs.microsoft.com/en-us/aspnet/core/test/middleware?view=aspnetcore-3.1). + +### Necessary imports: +```fsharp +open Microsoft.AspNetCore.Builder +open Microsoft.AspNetCore.TestHost +open Microsoft.AspNetCore.Hosting +``` + +### Build a test host: +```fsharp +let getTestHost = + WebHostBuilder() + .UseTestServer() + .Configure(Action [YourApp].configureApp) + .ConfigureServices([YourApp].configureServices) + .ConfigureLogging([YourApp].configureLogging) + .UseUrls([YourUrl]) +``` + +### Build a test server: +```fsharp +let getTestServer = + let testHost = getTestHost + new TestServer(testHost) +``` + +### Build a test client: +```fsharp +let getTestClient = + let testServer = getTestServer + testServer.CreateClient() +``` + +### Example (using Xunit): +```fsharp +open System.Net // Import needed for the code below: + +[] +let ``Hello world endpoint says hello`` () = + let testClient = getTestClient + let response = testClient.GetAsync("/hello-world").Result + let content = response.Content.ReadAsStringAsync().Result + Assert.Equal(response.StatusCode, HttpStatusCode.OK) + Assert.Equal(content, "hello") +``` + ## Miscellaneous On top of default HTTP related functions such as `HttpContext` extension methods and `HttpHandler` functions Giraffe also provides a few other helper functions which are commonly required in Giraffe web applications. From 2ed6a1a4534af67f222c7f34c6a080c6395bc72d Mon Sep 17 00:00:00 2001 From: Daniel Pozmanter Date: Mon, 20 Jul 2020 11:06:12 -0400 Subject: [PATCH 03/10] Alterations from PR - Testing documentation --- DOCUMENTATION.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index cd567156..8298e886 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -3138,7 +3138,7 @@ open Microsoft.AspNetCore.Hosting ### Build a test host: ```fsharp -let getTestHost = +let getTestHost() = WebHostBuilder() .UseTestServer() .Configure(Action [YourApp].configureApp) @@ -3149,15 +3149,15 @@ let getTestHost = ### Build a test server: ```fsharp -let getTestServer = - let testHost = getTestHost +let getTestServer() = + let testHost = getTestHost() new TestServer(testHost) ``` ### Build a test client: ```fsharp -let getTestClient = - let testServer = getTestServer +let getTestClient() = + let testServer = getTestServer() testServer.CreateClient() ``` From 5158d9af01d404de19eaf8016da2ea39dead42d5 Mon Sep 17 00:00:00 2001 From: Daniel Pozmanter Date: Mon, 20 Jul 2020 11:13:33 -0400 Subject: [PATCH 04/10] let -> use --- DOCUMENTATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 8298e886..ab5600b6 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -3167,7 +3167,7 @@ open System.Net // Import needed for the code below: [] let ``Hello world endpoint says hello`` () = - let testClient = getTestClient + use testClient = getTestClient let response = testClient.GetAsync("/hello-world").Result let content = response.Content.ReadAsStringAsync().Result Assert.Equal(response.StatusCode, HttpStatusCode.OK) From 11fe1e1fa4af5ee1285d36ecb60ac00adf214fd9 Mon Sep 17 00:00:00 2001 From: Daniel Pozmanter Date: Tue, 21 Jul 2020 22:14:38 -0400 Subject: [PATCH 05/10] Fixing the use of use --- DOCUMENTATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index ab5600b6..8298e886 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -3167,7 +3167,7 @@ open System.Net // Import needed for the code below: [] let ``Hello world endpoint says hello`` () = - use testClient = getTestClient + let testClient = getTestClient let response = testClient.GetAsync("/hello-world").Result let content = response.Content.ReadAsStringAsync().Result Assert.Equal(response.StatusCode, HttpStatusCode.OK) From eb4bffbd4e1f04f36586f8aa95cf1a386c38f4c8 Mon Sep 17 00:00:00 2001 From: Daniel Pozmanter Date: Thu, 23 Jul 2020 00:41:44 -0400 Subject: [PATCH 06/10] Updating to be more flexible, fix TestServer usage, use HttpRequestMessage --- DOCUMENTATION.md | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 8298e886..8f0cac1f 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -3131,9 +3131,11 @@ Testing a Giraffe application builds on [ASP.NET Core testing](https://docs.micr ### Necessary imports: ```fsharp +open FSharp.Control.Tasks open Microsoft.AspNetCore.Builder open Microsoft.AspNetCore.TestHost open Microsoft.AspNetCore.Hosting +open System.Net.Http ``` ### Build a test host: @@ -3147,31 +3149,36 @@ let getTestHost() = .UseUrls([YourUrl]) ``` -### Build a test server: +### Create a flexible function to handle requests: ```fsharp -let getTestServer() = - let testHost = getTestHost() - new TestServer(testHost) -``` - -### Build a test client: -```fsharp -let getTestClient() = - let testServer = getTestServer() - testServer.CreateClient() +let testRequest (request : HttpRequestMessage) = + let resp = task { + use server = new TestServer(getTestHost()) + use client = server.CreateClient() + let! response = request |> client.SendAsync + return response + } + resp.Result ``` -### Example (using Xunit): +### Examples (using Xunit): ```fsharp open System.Net // Import needed for the code below: [] let ``Hello world endpoint says hello`` () = - let testClient = getTestClient - let response = testClient.GetAsync("/hello-world").Result + let response = testRequest (new HttpRequestMessage(HttpMethod.Get, "/hello-world")) let content = response.Content.ReadAsStringAsync().Result Assert.Equal(response.StatusCode, HttpStatusCode.OK) Assert.Equal(content, "hello") + +[] +let ``Example HTTP Post`` () = + let request = new HttpRequestMessage(HttpMethod.Post, "/hello-world") + request.Content <- "{\"JsonField\":\"JsonValue\"}" + let response = testRequest request + Assert.Equal(response.StatusCode, HttpStatusCode.OK) + // Check the json content ``` ## Miscellaneous From de1fbe0d7608880f6839bfe396b55a35fe165026 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Thu, 26 Nov 2020 21:22:28 +0000 Subject: [PATCH 07/10] Mini docs updates --- DOCUMENTATION.md | 11 ++++++++--- RELEASE_NOTES.md | 4 ++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index c9cda9c0..95decd6b 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -3128,9 +3128,10 @@ let customHandler (dataObj : obj) : HttpHandler = ## Testing -Testing a Giraffe application builds on [ASP.NET Core testing](https://docs.microsoft.com/en-us/aspnet/core/test/middleware?view=aspnetcore-3.1). +Testing a Giraffe application follows the concept of [ASP.NET Core testing](https://docs.microsoft.com/en-us/aspnet/core/test/middleware?view=aspnetcore-3.1). ### Necessary imports: + ```fsharp open FSharp.Control.Tasks open Microsoft.AspNetCore.Builder @@ -3140,6 +3141,7 @@ open System.Net.Http ``` ### Build a test host: + ```fsharp let getTestHost() = WebHostBuilder() @@ -3150,7 +3152,8 @@ let getTestHost() = .UseUrls([YourUrl]) ``` -### Create a flexible function to handle requests: +### Create a helper function to issue test requests: + ```fsharp let testRequest (request : HttpRequestMessage) = let resp = task { @@ -3163,8 +3166,10 @@ let testRequest (request : HttpRequestMessage) = ``` ### Examples (using Xunit): + ```fsharp -open System.Net // Import needed for the code below: +// Import needed for the code below: +open System.Net [] let ``Hello world endpoint says hello`` () = diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index c6cd2640..4ea6da59 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,10 @@ Release Notes ============= +## 5.0.0-rc-2 + +- Fixed pre-conditions validation issue (see [#424](https://github.com/giraffe-fsharp/Giraffe/issues/424)) + ## 5.0.0-rc-1 Upgraded to .NET 5. The 5.x version of Giraffe is targeting `net5.0` and dropping support for all other target frameworks. If you cannot upgrade a project to .NET 5 yet then stay on an older version of Giraffe until you can. Giraffe has always been a .NET Core centered project and in the .NET Core world (and now .NET 5 world) there is little to no reason why a project should remain on an old .NET Core version for a long time when upgrade paths are mostly as simple as changing the `` property in an `.fsproj` file. From 8315d44a3e46cb5ee926d2357d5ed14e009f6094 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Thu, 26 Nov 2020 21:28:42 +0000 Subject: [PATCH 08/10] Fixed #447 --- src/Giraffe/EndpointRouting.fs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Giraffe/EndpointRouting.fs b/src/Giraffe/EndpointRouting.fs index 3fd3f17a..ec761571 100644 --- a/src/Giraffe/EndpointRouting.fs +++ b/src/Giraffe/EndpointRouting.fs @@ -44,12 +44,11 @@ open FSharp.Control.Tasks.V2.ContextInsensitive // | routeStartsWithf | Can't do, see subRoutef | // | routeStartsWithCif | Can't do, see subRoutef | - module private RouteTemplateBuilder = let private guidPattern = - "([0-9A-Fa-f]{8}\-[0-9A-Fa-f]{4}\-[0-9A-Fa-f]{4}\-[0-9A-Fa-f]{4}\-[0-9A-Fa-f]{12}|[0-9A-Fa-f]{32}|[-_0-9A-Za-z]{22})" + "([0-9A-Fa-f]{{8}}\-[0-9A-Fa-f]{{4}}\-[0-9A-Fa-f]{{4}}\-[0-9A-Fa-f]{{4}}\-[0-9A-Fa-f]{{12}}|[0-9A-Fa-f]{{32}}|[-_0-9A-Za-z]{{22}})" let private shortIdPattern = - "([-_0-9A-Za-z]{10}[048AEIMQUYcgkosw])" + "([-_0-9A-Za-z]{{10}}[048AEIMQUYcgkosw])" let private getConstraint (i : int) (c : char) = let name = sprintf "%c%i" c i From 896da169c529b39cd1402cd4a636a698127d3c2c Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Thu, 26 Nov 2020 21:29:40 +0000 Subject: [PATCH 09/10] Release notes update --- RELEASE_NOTES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 4ea6da59..2cd83253 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -4,6 +4,7 @@ Release Notes ## 5.0.0-rc-2 - Fixed pre-conditions validation issue (see [#424](https://github.com/giraffe-fsharp/Giraffe/issues/424)) +- Fixed parsing issue with Guids and ShortIds in `Giraffe.EndpointRouting` (see [#447](https://github.com/giraffe-fsharp/Giraffe/issues/447)) ## 5.0.0-rc-1 From 466f1f837e861cab174bea612630bb46f419b989 Mon Sep 17 00:00:00 2001 From: Dustin Moris Gorski Date: Thu, 26 Nov 2020 21:41:57 +0000 Subject: [PATCH 10/10] Fixes #446 --- DOCUMENTATION.md | 4 ++++ RELEASE_NOTES.md | 1 + src/Giraffe/Routing.fs | 34 ++++++++++++++++++++++++++++------ 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 95decd6b..9f9a4fdb 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -1049,6 +1049,10 @@ In the above scenario it is not clear which one of the two http handlers a user If you want to learn more about `Regex` please check the [Regular Expression Language Reference](https://docs.microsoft.com/en-us/dotnet/standard/base-types/regular-expression-language-quick-reference). +#### routexp + +The `routexp` http handler is a combination of `routex` and `routef`. It resolves a route exactly like `routex`, but then passes the resolved Regex Groups as a `Seq` parameter into the supplied handler function similar to how `routef` invokes the next handler in the pipeline. + #### routeCix The `routeCix` http handler is the case insensitive version of `routex`: diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 2cd83253..339fc2e3 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -5,6 +5,7 @@ Release Notes - Fixed pre-conditions validation issue (see [#424](https://github.com/giraffe-fsharp/Giraffe/issues/424)) - Fixed parsing issue with Guids and ShortIds in `Giraffe.EndpointRouting` (see [#447](https://github.com/giraffe-fsharp/Giraffe/issues/447)) +- Added `routexp` http handler to default router (see [#446](https://github.com/giraffe-fsharp/Giraffe/issues/446)) ## 5.0.0-rc-1 diff --git a/src/Giraffe/Routing.fs b/src/Giraffe/Routing.fs index e8c7bdb4..3af75b45 100644 --- a/src/Giraffe/Routing.fs +++ b/src/Giraffe/Routing.fs @@ -106,6 +106,28 @@ let routex (path : string) : HttpHandler = | true -> next ctx | false -> skipPipeline +/// +/// Filters an incoming HTTP request based on the request path using Regex (case sensitive). +/// +/// If the route matches the incoming HTTP request then the Regex groups will be passed into the supplied `routeHandler`. +/// +/// This is similar to routex but also allows to use matched strings as parameters for a controller. +/// +/// Regex path. +/// A function which accepts a string sequence of the matched groups and returns a `HttpHandler` function which will subsequently deal with the request. +/// A Giraffe function which can be composed into a bigger web application. +let routexp (path : string) (routeHandler : seq -> HttpHandler): HttpHandler = + let pattern = sprintf "^%s$" path + let regex = Regex(pattern, RegexOptions.Compiled) + + fun (next : HttpFunc) (ctx : Microsoft.AspNetCore.Http.HttpContext) -> + let result = regex.Match (SubRouting.getNextPartOfPath ctx) + match result.Success with + | true -> + let args = result.Groups |> Seq.map (fun x -> x.Value) + routeHandler args next ctx + | false -> skipPipeline + /// /// Filters an incoming HTTP request based on the request path using Regex (case insensitive). /// @@ -123,7 +145,7 @@ let routeCix (path : string) : HttpHandler = /// /// Filters an incoming HTTP request based on the request path (case sensitive). /// If the route matches the incoming HTTP request then the arguments from the will be automatically resolved and passed into the supplied routeHandler. -/// +/// /// Supported format chars** /// /// %b: bool @@ -148,7 +170,7 @@ let routef (path : PrintfFormat<_,_,_,_, 'T>) (routeHandler : 'T -> HttpHandler) /// /// Filters an incoming HTTP request based on the request path. /// If the route matches the incoming HTTP request then the arguments from the will be automatically resolved and passed into the supplied routeHandler. -/// +/// /// Supported format chars** /// /// %b: bool @@ -226,7 +248,7 @@ let routeStartsWithCi (subPath : string) : HttpHandler = /// /// Filters an incoming HTTP request based on the beginning of the request path (case sensitive). /// If the route matches the incoming HTTP request then the arguments from the will be automatically resolved and passed into the supplied routeHandler. -/// +/// /// Supported format chars** /// /// %b: bool @@ -254,7 +276,7 @@ let routeStartsWithf (path : PrintfFormat<_,_,_,_, 'T>) (routeHandler : 'T -> Ht /// /// Filters an incoming HTTP request based on the beginning of the request path (case insensitive). /// If the route matches the incoming HTTP request then the arguments from the will be automatically resolved and passed into the supplied routeHandler. -/// +/// /// Supported format chars** /// /// %b: bool @@ -312,7 +334,7 @@ let subRouteCi (path : string) (handler : HttpHandler) : HttpHandler = /// /// Filters an incoming HTTP request based on a part of the request path (case sensitive). /// If the sub route matches the incoming HTTP request then the arguments from the will be automatically resolved and passed into the supplied routeHandler. -/// +/// /// Supported format chars /// /// %b: bool @@ -322,7 +344,7 @@ let subRouteCi (path : string) (handler : HttpHandler) : HttpHandler = /// %d: int64 /// %f: float/double /// %O: Guid -/// +/// /// Subsequent routing handlers inside the given handler function should omit the already validated path. /// /// A format string representing the expected request sub path.