Skip to content

Commit 8a1b035

Browse files
authored
Giraffe Sample (#67)
* A sample Giraffe port (wip) * add HTTP verbs and choose * Add middleware handling * Add routing functions * Fix default handler * Add readme
1 parent c64c6cd commit 8a1b035

17 files changed

+1179
-7
lines changed

.config/dotnet-tools.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,16 @@
88
"paket"
99
]
1010
},
11-
"fantomas-tool": {
12-
"version": "4.7.9",
11+
"fable": {
12+
"version": "4.0.0-snake-island-alpha-024",
1313
"commands": [
14-
"fantomas"
14+
"fable"
1515
]
1616
},
17-
"fable": {
18-
"version": "4.0.0-snake-island-alpha-019",
17+
"fantomas": {
18+
"version": "5.0.0-beta-009",
1919
"commands": [
20-
"fable"
20+
"fantomas"
2121
]
2222
}
2323
}

examples/giraffe/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.py

examples/giraffe/Core.fs

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// Learn more about F# at http://docs.microsoft.com/dotnet/fsharp
2+
namespace Giraffe
3+
4+
open System.Text
5+
open System.Threading.Tasks
6+
7+
type HttpFuncResult = Task<HttpContext option>
8+
9+
type HttpFunc = HttpContext -> HttpFuncResult
10+
11+
type HttpHandler = HttpFunc -> HttpFunc
12+
13+
14+
[<AutoOpen>]
15+
module Core =
16+
let earlyReturn: HttpFunc = Some >> Task.FromResult
17+
let skipPipeline () : HttpFuncResult = Task.FromResult None
18+
19+
let compose (handler1: HttpHandler) (handler2: HttpHandler) : HttpHandler =
20+
fun (final: HttpFunc) ->
21+
let func = final |> handler2 |> handler1
22+
23+
fun (ctx: HttpContext) ->
24+
match ctx.Response.HasStarted with
25+
| true -> final ctx
26+
| false -> func ctx
27+
28+
let (>=>) = compose
29+
30+
/// <summary>
31+
/// The warbler function is a <see cref="HttpHandler"/> wrapper function which prevents a <see cref="HttpHandler"/> to be pre-evaluated at startup.
32+
/// </summary>
33+
/// <param name="f">A function which takes a HttpFunc * HttpContext tuple and returns a <see cref="HttpHandler"/> function.</param>
34+
/// <param name="next"></param>
35+
/// <param name="source"></param>
36+
/// <example>
37+
/// <code>
38+
/// warbler(fun _ -> someHttpHandler)
39+
/// </code>
40+
/// </example>
41+
/// <returns>Returns a <see cref="HttpHandler"/> function.</returns>
42+
let inline warbler f (source: HttpHandler) (next: HttpFunc) =
43+
fun (ctx: HttpContext) -> f (next, ctx) id next ctx
44+
|> source
45+
46+
/// <summary>
47+
/// Iterates through a list of `HttpFunc` functions and returns the result of the first `HttpFunc` of which the outcome is `Some HttpContext`.
48+
/// </summary>
49+
/// <param name="funcs"></param>
50+
/// <param name="ctx"></param>
51+
/// <returns>A <see cref="HttpFuncResult"/>.</returns>
52+
let rec private chooseHttpFunc (funcs: HttpFunc list) : HttpFunc =
53+
fun (ctx: HttpContext) ->
54+
task {
55+
match funcs with
56+
| [] -> return None
57+
| func :: tail ->
58+
let! result = func ctx
59+
60+
match result with
61+
| Some c -> return Some c
62+
| None -> return! chooseHttpFunc tail ctx
63+
}
64+
65+
/// <summary>
66+
/// Iterates through a list of <see cref="HttpHandler"/> functions and returns the result of the first <see cref="HttpHandler"/> of which the outcome is Some HttpContext.
67+
/// Please mind that all <see cref="HttpHandler"/> functions will get pre-evaluated at runtime by applying the next (HttpFunc) parameter to each handler.
68+
/// </summary>
69+
/// <param name="handlers"></param>
70+
/// <param name="next"></param>
71+
/// <returns>A <see cref="HttpFunc"/>.</returns>
72+
let choose (handlers: HttpHandler list) : HttpHandler =
73+
fun (next: HttpFunc) ->
74+
let funcs = handlers |> List.map (fun h -> h next)
75+
fun (ctx: HttpContext) -> chooseHttpFunc funcs ctx
76+
77+
78+
let text (str: string) : HttpHandler =
79+
let bytes = Encoding.UTF8.GetBytes str
80+
81+
fun (_: HttpFunc) (ctx: HttpContext) ->
82+
ctx.SetContentType "text/plain; charset=utf-8"
83+
ctx.WriteBytesAsync bytes
84+
85+
86+
/// <summary>
87+
/// Filters an incoming HTTP request based on the HTTP verb.
88+
/// </summary>
89+
/// <param name="validate">A validation function which checks for a single HTTP verb.</param>
90+
/// <param name="next"></param>
91+
/// <param name="ctx"></param>
92+
/// <returns>A Giraffe <see cref="HttpHandler"/> function which can be composed into a bigger web application.</returns>
93+
let private httpVerb (validate: string -> bool) : HttpHandler =
94+
fun (next: HttpFunc) (ctx: HttpContext) ->
95+
if validate ctx.Request.Method then
96+
next ctx
97+
else
98+
skipPipeline ()
99+
100+
let GET: HttpHandler = httpVerb HttpMethods.IsGet
101+
let POST: HttpHandler = httpVerb HttpMethods.IsPost
102+
let PUT: HttpHandler = httpVerb HttpMethods.IsPut
103+
let PATCH: HttpHandler = httpVerb HttpMethods.IsPatch
104+
let DELETE: HttpHandler = httpVerb HttpMethods.IsDelete
105+
let HEAD: HttpHandler = httpVerb HttpMethods.IsHead
106+
let OPTIONS: HttpHandler = httpVerb HttpMethods.IsOptions
107+
let TRACE: HttpHandler = httpVerb HttpMethods.IsTrace
108+
let CONNECT: HttpHandler = httpVerb HttpMethods.IsConnect
109+
110+
let GET_HEAD: HttpHandler = choose [ GET; HEAD ]
111+
112+
/// <summary>
113+
/// Sets the HTTP status code of the response.
114+
/// </summary>
115+
/// <param name="statusCode">The status code to be set in the response. For convenience you can use the static <see cref="Microsoft.AspNetCore.Http.StatusCodes"/> class for passing in named status codes instead of using pure int values.</param>
116+
/// <param name="next"></param>
117+
/// <param name="ctx"></param>
118+
/// <returns>A Giraffe <see cref="HttpHandler"/> function which can be composed into a bigger web application.</returns>
119+
let setStatusCode (statusCode: int) : HttpHandler =
120+
fun (next: HttpFunc) (ctx: HttpContext) ->
121+
ctx.SetStatusCode statusCode
122+
next ctx
123+
124+
125+
/// <summary>
126+
/// Adds or sets a HTTP header in the response.
127+
/// </summary>
128+
/// <param name="key">The HTTP header name. For convenience you can use the static <see cref="Microsoft.Net.Http.Headers.HeaderNames"/> class for passing in strongly typed header names instead of using pure string values.</param>
129+
/// <param name="value">The value to be set. Non string values will be converted to a string using the object's ToString() method.</param>
130+
/// <param name="next"></param>
131+
/// <param name="ctx"></param>
132+
/// <returns>A Giraffe <see cref="HttpHandler"/> function which can be composed into a bigger web application.</returns>
133+
let setHttpHeader (key: string) (value: obj) : HttpHandler =
134+
fun (next: HttpFunc) (ctx: HttpContext) ->
135+
ctx.SetHttpHeader(key, value)
136+
next ctx
137+
138+
/// <summary>
139+
/// Serializes an object to JSON and writes the output to the body of the HTTP response.
140+
/// It also sets the HTTP Content-Type header to application/json and sets the Content-Length header accordingly.
141+
/// The JSON serializer can be configured in the ASP.NET Core startup code by registering a custom class of type <see cref="Json.ISerializer"/>.
142+
/// </summary>
143+
/// <param name="dataObj">The object to be send back to the client.</param>
144+
/// <param name="ctx"></param>
145+
/// <typeparam name="'T"></typeparam>
146+
/// <returns>A Giraffe <see cref="HttpHandler" /> function which can be composed into a bigger web application.</returns>
147+
let json<'T> (dataObj: 'T) : HttpHandler =
148+
fun (_: HttpFunc) (ctx: HttpContext) -> ctx.WriteJsonAsync dataObj
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
module Giraffe.Python.FormatExpressions
2+
3+
open System
4+
open System.Text.RegularExpressions
5+
open Microsoft.FSharp.Reflection
6+
open FSharp.Core
7+
8+
// ---------------------------
9+
// String matching functions
10+
// ---------------------------
11+
12+
let private formatStringMap =
13+
let decodeSlashes (str: string) =
14+
// Kestrel has made the weird decision to
15+
// partially decode a route argument, which
16+
// means that a given route argument would get
17+
// entirely URL decoded except for '%2F' (/).
18+
// Hence decoding %2F must happen separately as
19+
// part of the string parsing function.
20+
//
21+
// For more information please check:
22+
// https://github.com/aspnet/Mvc/issues/4599
23+
str.Replace("%2F", "/").Replace("%2f", "/")
24+
25+
let parseGuid (str: string) =
26+
match str.Length with
27+
| 22 -> ShortGuid.toGuid str
28+
| _ -> Guid str
29+
30+
let guidPattern =
31+
"([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})"
32+
33+
let shortIdPattern = "([-_0-9A-Za-z]{10}[048AEIMQUYcgkosw])"
34+
35+
dict
36+
[
37+
// Char Regex Parser
38+
// -------------------------------------------------------------
39+
'b', ("(?i:(true|false)){1}", (fun (s: string) -> bool.Parse s) >> box) // bool
40+
'c', ("([^/]{1})", char >> box) // char
41+
's', ("([^/]+)", decodeSlashes >> box) // string
42+
'i', ("(-?\d+)", int32 >> box) // int
43+
'd', ("(-?\d+)", int64 >> box) // int64
44+
'f', ("(-?\d+\.{1}\d+)", float >> box) // float
45+
'O', (guidPattern, parseGuid >> box) // Guid
46+
'u', (shortIdPattern, ShortId.toUInt64 >> box) ] // uint64
47+
48+
type MatchMode =
49+
| Exact // Will try to match entire string from start to end.
50+
| StartsWith // Will try to match a substring. Subject string should start with test case.
51+
| EndsWith // Will try to match a substring. Subject string should end with test case.
52+
| Contains // Will try to match a substring. Subject string should contain test case.
53+
54+
type MatchOptions =
55+
{ IgnoreCase: bool
56+
MatchMode: MatchMode }
57+
58+
static member Exact =
59+
{ IgnoreCase = false
60+
MatchMode = Exact }
61+
62+
static member IgnoreCaseExact = { IgnoreCase = true; MatchMode = Exact }
63+
64+
let private convertToRegexPatternAndFormatChars (mode: MatchMode) (formatString: string) =
65+
let rec convert (chars: char list) =
66+
match chars with
67+
| '%' :: '%' :: tail ->
68+
let pattern, formatChars = convert tail
69+
"%" + pattern, formatChars
70+
| '%' :: c :: tail ->
71+
let pattern, formatChars = convert tail
72+
let regex, _ = formatStringMap.[c]
73+
regex + pattern, c :: formatChars
74+
| c :: tail ->
75+
let pattern, formatChars = convert tail
76+
c.ToString() + pattern, formatChars
77+
| [] -> "", []
78+
79+
let inline formatRegex mode pattern =
80+
match mode with
81+
| Exact -> "^" + pattern + "$"
82+
| StartsWith -> "^" + pattern
83+
| EndsWith -> pattern + "$"
84+
| Contains -> pattern
85+
86+
formatString
87+
|> List.ofSeq
88+
|> convert
89+
|> (fun (pattern, formatChars) -> formatRegex mode pattern, formatChars)
90+
91+
/// <summary>
92+
/// Tries to parse an input string based on a given format string and return a tuple of all parsed arguments.
93+
/// </summary>
94+
/// <param name="format">The format string which shall be used for parsing.</param>
95+
/// <param name="options">The options record with specifications on how the matching should behave.</param>
96+
/// <param name="input">The input string from which the parsed arguments shall be extracted.</param>
97+
/// <returns>Matched value as an option of 'T</returns>
98+
let tryMatchInput (format: PrintfFormat<_, _, _, _, 'T>) (options: MatchOptions) (input: string) =
99+
try
100+
let pattern, formatChars =
101+
format.Value
102+
|> Regex.Escape
103+
|> convertToRegexPatternAndFormatChars options.MatchMode
104+
105+
let options =
106+
match options.IgnoreCase with
107+
| true -> RegexOptions.IgnoreCase
108+
| false -> RegexOptions.None
109+
110+
let result = Regex.Match(input, pattern, options)
111+
112+
if result.Groups.Count <= 1 then
113+
None
114+
else
115+
let groups = result.Groups |> Seq.cast<Group> |> Seq.skip 1
116+
117+
let values =
118+
(groups, formatChars)
119+
||> Seq.map2 (fun g c ->
120+
let _, parser = formatStringMap.[c]
121+
let value = parser g.Value
122+
value)
123+
|> Seq.toArray
124+
125+
let result =
126+
match values.Length with
127+
| 1 -> values.[0]
128+
| _ ->
129+
let types = values |> Array.map (fun v -> v.GetType())
130+
let tupleType = FSharpType.MakeTupleType types
131+
FSharpValue.MakeTuple(values, tupleType)
132+
133+
result :?> 'T |> Some
134+
with _ ->
135+
None
136+
137+
/// <summary>
138+
/// Tries to parse an input string based on a given format string and return a tuple of all parsed arguments.
139+
/// </summary>
140+
/// <param name="format">The format string which shall be used for parsing.</param>
141+
/// <param name="ignoreCase">The flag to make matching case insensitive.</param>
142+
/// <param name="input">The input string from which the parsed arguments shall be extracted.</param>
143+
/// <returns>Matched value as an option of 'T</returns>
144+
let tryMatchInputExact (format: PrintfFormat<_, _, _, _, 'T>) (ignoreCase: bool) (input: string) =
145+
let options =
146+
match ignoreCase with
147+
| true -> MatchOptions.IgnoreCaseExact
148+
| false -> MatchOptions.Exact
149+
150+
tryMatchInput format options input
151+
152+
153+
// ---------------------------
154+
// Validation helper functions
155+
// ---------------------------
156+
157+
/// **Description**
158+
///
159+
/// Validates if a given format string can be matched with a given tuple.
160+
///
161+
/// **Parameters**
162+
///
163+
/// `format`: The format string which shall be used for parsing.
164+
///
165+
/// **Output**
166+
///
167+
/// Returns `unit` if validation was successful otherwise will throw an `Exception`.
168+
/// Returns `unit` if validation was successful otherwise will throw an `Exception`.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Project Sdk="Microsoft.NET.Sdk">
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net6.0</TargetFramework>
6+
<WarnOn>3390;$(WarnOn)</WarnOn>
7+
</PropertyGroup>
8+
<ItemGroup>
9+
<Compile Include="Helpers.fs" />
10+
<Compile Include="ShortGuid.fs" />
11+
<Compile Include="FormatExpressions.fs" />
12+
<Compile Include="HttpContext.fs" />
13+
<Compile Include="Core.fs" />
14+
<Compile Include="Routing.fs" />
15+
<Compile Include="HttpHandler.fs" />
16+
<Compile Include="Middleware.fs" />
17+
<Compile Include="Program.fs" />
18+
</ItemGroup>
19+
<ItemGroup>
20+
<PackageReference Include="Fable.Python" Version="0.17.0" />
21+
<PackageReference Include="Fable.Core" Version="4.0.0-snake-island-alpha-007" />
22+
</ItemGroup>
23+
</Project>

0 commit comments

Comments
 (0)