Browse Source

Zebra - F# / Aspnetcore web framework (#3936)

* init stripped down with forced abort

* Readme name Zebra

* fixed up custom operations

* update readme

* fixed error in benchmark config

* remove  csharp cache
Gerard 7 years ago
parent
commit
69255016aa

+ 0 - 0
frameworks/CSharp/aspnet-mono-ngx/src/obj/Debug/Benchmarks.csproj.CoreCompileInputs.cache


BIN
frameworks/CSharp/aspnet-mono-ngx/src/obj/Debug/Benchmarks.csprojAssemblyReference.cache


+ 0 - 0
frameworks/CSharp/aspnet-mono-ngx/src/obj/Debug/TemporaryGeneratedFile_036C0B5B-1481-4323-8D20-8F5ADCB23D92.cs


+ 0 - 0
frameworks/CSharp/aspnet-mono-ngx/src/obj/Debug/TemporaryGeneratedFile_5937a670-0e60-4077-877b-f7221da3dda1.cs


+ 0 - 0
frameworks/CSharp/aspnet-mono-ngx/src/obj/Debug/TemporaryGeneratedFile_E7A71F73-0F8D-4B9B-B56E-8E70B10BC5D3.cs


+ 38 - 0
frameworks/FSharp/Zebra/.gitignore

@@ -0,0 +1,38 @@
+[Oo]bj/
+[Bb]in/
+TestResults/
+.nuget/
+*.sln.ide/
+_ReSharper.*/
+.idea/
+packages/
+artifacts/
+PublishProfiles/
+.vs/
+*.user
+*.suo
+*.cache
+*.docstates
+_ReSharper.*
+nuget.exe
+*net45.csproj
+*net451.csproj
+*k10.csproj
+*.psess
+*.vsp
+*.pidb
+*.userprefs
+*DS_Store
+*.ncrunchsolution
+*.*sdf
+*.ipch
+*.swp
+*~
+.build/
+.testPublish/
+launchSettings.json
+BenchmarkDotNet.Artifacts/
+BDN.Generated/
+binaries/
+global.json
+*.sln

+ 31 - 0
frameworks/FSharp/Zebra/README.md

@@ -0,0 +1,31 @@
+# Zebra Tests on Linux
+This includes tests for plaintext and json serialization.
+
+Zebra is a new F# functional Asp.net Framework Wrapper that utalises a shared state-machine for Task binding the same as C# async/await but done using F# computation expressions.
+
+## Infrastructure Software Versions
+
+**Language**
+
+* F# 4.1
+
+**Platforms**
+
+* .NET Core (Windows and Linux)
+
+**Web Servers**
+
+* [Kestrel](https://github.com/aspnet/KestrelHttpServer)
+
+**Web Stack**
+
+* [Zebra](https://medium.com/@gerardtoconnor/racing-the-zebra-benchmark-performance-architecture-for-f-web-server-58dd922f5cfe)
+* ASP.NET Core
+
+## Paths & Source for Tests
+
+* [Plaintext](src/App/Program.fs): "/plaintext"
+* [JSON serialization](src/App/Program.fs): "/json"
+
+
+

+ 26 - 0
frameworks/FSharp/Zebra/benchmark_config.json

@@ -0,0 +1,26 @@
+{
+  "framework": "zebra",
+  "tests": [
+    {
+      "default": {
+        "plaintext_url": "/plaintext",
+        "json_url": "/json",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "fullstack",
+        "database": "None",
+        "framework": "zebra",
+        "language": "F#",
+        "orm": "Raw",
+        "platform": ".NET",
+        "flavor": "CoreCLR",
+        "webserver": "Kestrel",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "Zebra",
+        "notes": "",
+        "versus": "aspcore"
+      }
+    }
+  ]
+}

+ 36 - 0
frameworks/FSharp/Zebra/src/App/App.fsproj

@@ -0,0 +1,36 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+  <PropertyGroup>
+    <TargetFramework>netcoreapp2.1</TargetFramework>
+    <DebugType>portable</DebugType>
+    <AssemblyName>App</AssemblyName>
+    <OutputType>Exe</OutputType>
+    <EnableDefaultContentItems>false</EnableDefaultContentItems>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.Extensions.Primitives" Version="2.1.1" />
+    <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.1.1" />
+    <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.1.0" />
+    <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.1.0" />
+    <PackageReference Include="Utf8Json" Version="1.3.7" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Compile Include="Buffer.fs" />
+    <Compile Include="Parsers.fs" />
+    <Compile Include="Encoding.fs" />
+    <Compile Include="State.fs" />
+    <Compile Include="ExecNodes.fs" />
+    <Compile Include="RouteNode.fs" />
+    <Compile Include="Router.fs" />
+    <Compile Include="Composition.fs" />
+    <Compile Include="Middleware.fs" />
+    <Compile Include="Program.fs" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Update="FSharp.Core" Version="4.5.0" />
+  </ItemGroup>
+
+</Project>

+ 32 - 0
frameworks/FSharp/Zebra/src/App/Buffer.fs

@@ -0,0 +1,32 @@
+module StreamBuffer
+open System
+open System .IO
+
+let private DefaultCapacity = 1386
+let private MaxBuilderSize = DefaultCapacity * 3
+
+type MemoryStreamCache = 
+    
+    [<ThreadStatic>]
+    [<DefaultValue>]
+    static val mutable private instance: MemoryStream
+
+    static member Get() = MemoryStreamCache.Get(DefaultCapacity)
+    static member Get(capacity:int) = 
+        
+        if capacity <= MaxBuilderSize then
+            let ms = MemoryStreamCache.instance;
+            let capacity = max capacity DefaultCapacity
+            
+            if ms <> null && capacity <= ms.Capacity then
+                MemoryStreamCache.instance <- null;
+                ms.SetLength 0L
+                ms
+            else
+                new MemoryStream(capacity)
+        else
+            new MemoryStream(capacity)
+
+    static member Release(ms:MemoryStream) = 
+        if ms.Capacity <= MaxBuilderSize then
+            MemoryStreamCache.instance <- ms

+ 123 - 0
frameworks/FSharp/Zebra/src/App/Composition.fs

@@ -0,0 +1,123 @@
+[<AutoOpen>]
+module Composition
+open State
+open ExecNodes
+open RouteNode
+open Parsers
+open Router
+
+//////////////////////////////////////
+/// Composisiotn
+let inline composeActAct (handler1:Zapp<'T>) (handler2:Zapp<'T>) : PipeLine<'T> =
+    fun (next:INode<'T>,fail:INode<'T>) -> // new nodeTree Fn
+        let child =  ChoiceNode<'T>(next,fail,handler2)      // pushing initial next/fail down pipeline
+        let parent = ChoiceNode<'T>(child,fail,handler1)
+        parent :> INode<'T>
+
+let inline composeFnAct (parentFn:PipeLine<'T>) (handler:Zapp<'T>) : PipeLine<'T> =
+    fun (next:INode<'T>,fail:INode<'T>) -> // new nodeTree Fn
+        let child = ChoiceNode<'T>(next,fail,handler)      // pushing initial next/fail down pipeline
+        let parent = parentFn (child,fail)
+        parent
+
+let inline composeActFn (parentHandler:Zapp<'T>) (childFn:PipeLine<'T>) : PipeLine<'T> =
+    fun (next:INode<'T>,fail:INode<'T>) -> // new nodeTree Fn
+        let child = childFn(next,fail)      // pushing initial next/fail down pipeline
+        let parent = ChoiceNode<'T>(child,fail,parentHandler)
+        parent :> INode<'T>
+
+let inline composeFnFn (parentFn:PipeLine<'T>) (childFn:PipeLine<'T>) : PipeLine<'T> =
+    fun (next:INode<'T>,fail:INode<'T>) -> // new nodeTree Fn
+        let child = childFn(next,fail)      // pushing initial next/fail down pipeline
+        let parent = parentFn(child,fail)
+        parent
+
+let inline composeFnChoose (parentFn:PipeLine<'T>) (childFn:ChooseWrap<'T>) : PipeLine<'T> =
+    fun (next:INode<'T>,fail:INode<'T>) -> // new nodeTree Fn
+        let child = childFn.Apply(next,fail)
+        let parent = parentFn(child,fail)
+        parent
+
+type RouteBuilder<'T> = INode<'T> * INode<'T> -> RNode<'T> -> RNode<'T>
+
+let inline composeRouter (routeFn:RouteBuilder<'T>) (childFn:Zapp<'T>) : RouteBuilder<'T> =
+    fun (next:INode<'T>,fail:INode<'T>) ->
+        let child = ChoiceNode<'T>(next,fail,childFn)      // pushing initial next/fail down pipeline
+        let parent = routeFn(child,fail)
+        parent
+
+let inline composeRoutePipe (routeFn:RouteBuilder<'T>) (childFn:PipeLine<'T>) : RouteBuilder<'T> =
+    fun (next:INode<'T>,fail:INode<'T>) ->
+        let child = childFn(next,fail)      // pushing initial next/fail down pipeline
+        let parent = routeFn(child,fail)
+        parent
+
+type PipeRoute<'T> = INode<'T> * INode<'T> -> IRouteNode<'T>     
+let inline composeRoute (pipeRoute: PipeRoute<'T>) (childFn:Zapp<'T>) =
+    fun (next:INode<'T>,fail:INode<'T>) ->
+        let child = ChoiceNode<'T>(next,fail,childFn)
+        pipeRoute(child,fail)
+
+let inline composeBase (a:RouteBase<'T>) (b:Zapp<'T>) =
+    fun (next:INode<'T>,fail:INode<'T>) ->
+        let child = ChoiceNode<'T>(next,fail,b)
+        route a.Method a.Pattern (child,fail)
+
+let inline composeRouteChoose (a:RouteBase<'T>) (b:ChooseWrap<'T>) =
+    fun (next:INode<'T>,fail:INode<'T>) ->
+        let child = b.Apply(next,fail)
+        route a.Method a.Pattern (child,fail)
+
+
+let inline composeBasePipe (a:RouteBase<'T>) (b:PipeLine<'T>) =
+    fun (next:INode<'T>,fail:INode<'T>) ->
+        let child = b(next,fail)
+        route a.Method a.Pattern (child,fail)    
+
+let inline composeBase1 (a:RouteBase<'T,'a>) (b:'a -> Zapp<'T>) =
+    fun (next:INode<'T>,fail:INode<'T>) ->
+        let p1 = Parse $ Unchecked.defaultof<'a>
+        let rn = RouteNode1(next,fail,b,p1) :> IRouteNode<'T>
+        routef a.Method a.Pattern rn 1
+
+let inline composeBase2 (a:RouteBase<'T,'a,'b>) (b:'a -> 'b -> Zapp<'T>) =
+    fun (next:INode<'T>,fail:INode<'T>) ->
+        let p1 = Parse $ Unchecked.defaultof<'a>
+        let p2 = Parse $ Unchecked.defaultof<'b>
+        let rn = RouteNode2(next,fail,b,p1,p2) :> IRouteNode<'T>
+        routef a.Method a.Pattern rn 2
+
+let inline composeBase3 (a:RouteBase<'T,'a,'b,'c>) (b:'a -> 'b -> 'c -> Zapp<'T>) =
+    fun (next:INode<'T>,fail:INode<'T>) ->
+        let p1 = Parse $ Unchecked.defaultof<'a>
+        let p2 = Parse $ Unchecked.defaultof<'b>
+        let p3 = Parse $ Unchecked.defaultof<'c>
+        let rn = RouteNode3(next,fail,b,p1,p2,p3) :> IRouteNode<'T>
+        routef a.Method a.Pattern rn 3      
+
+type ComposeExtension = ComposeExtension with
+
+    // initial binding
+    static member inline (?<-) (ComposeExtension, a:Zapp<'T> , b:Zapp<'T>) = composeActAct a b
+    static member inline (?<-) (ComposeExtension, a:PipeLine<'T> , b:Zapp<'T>) = composeFnAct a b
+    static member inline (?<-) (ComposeExtension, a:PipeLine<'T> , b:PipeLine<'T>) = composeFnFn a b
+    static member inline (?<-) (ComposeExtension, a:PipeLine<'T> , b:ChooseWrap<'T>) = composeFnChoose a b
+    static member inline (?<-) (ComposeExtension, a:RouteBuilder<'T> , b:Zapp<'T>) = composeRouter a b
+    //static member inline (?<-) (ComposeExtension, a:RouteBuilder<'T> , b:PipeLine<'T>) = composeRoutePipe a b
+    static member inline (?<-) (ComposeExtension, a:PipeRoute<'T> , b:Zapp<'T>) = composeRoute a b
+    static member inline (?<-) (ComposeExtension, a:RouteBase<'T> , b:Zapp<'T>) = composeBase a b
+    //static member inline (?<-) (ComposeExtension, a:RouteBase<'T> , b:PipeLine<'T>) = composeBasePipe a b
+    static member inline (?<-) (ComposeExtension, a:RouteBase<'T> , b:ChooseWrap<'T>) = composeRouteChoose a b
+    static member inline (?<-) (ComposeExtension, a:RouteBase<'T,'a> , b:'a -> Zapp<'T>) = composeBase1 a b
+    static member inline (?<-) (ComposeExtension, a:RouteBase<'T,'a,'b> , b:'a -> 'b -> Zapp<'T>) = composeBase2 a b
+    static member inline (?<-) (ComposeExtension, a:RouteBase<'T,'a,'b,'c> , b:'a -> 'b -> 'c -> Zapp<'T>) = composeBase3 a b
+    
+    //static member inline (?<-) (ComposeExtension, a:Action<'T> , b:PipeLine<'T>) = composeActFn a b
+    
+        
+/// **Action Binder** : dynamically binds different kinds of actions/choices together 
+/// eg:
+///  (a:Zapp<'T> , b:Zapp<'T>) 
+///  (a:PipeLine<'T> , b:Zapp<'T>) 
+///  (a:PipeLine<'T> , b:PipeLine<'T>) 
+let inline (=>) a b = (?<-) ComposeExtension a b

+ 12 - 0
frameworks/FSharp/Zebra/src/App/Encoding.fs

@@ -0,0 +1,12 @@
+module EncodeHelper
+open System.Text
+open System.IO
+
+// tempory writer
+let StreamWrite (str:string,stream:Stream) =
+    for char in str do
+        if int char < 128 then // vast majority of encoding <128 
+            stream.WriteByte(byte char)
+        else
+        for byte in Encoding.UTF8.GetBytes([|char|]) do
+            stream.WriteByte byte

+ 37 - 0
frameworks/FSharp/Zebra/src/App/ExecNodes.fs

@@ -0,0 +1,37 @@
+module ExecNodes
+open State
+
+/// Node Types
+/// //////////////////
+                     
+type ChoiceNode<'T>(inext:INode<'T>,ifail:INode<'T>,current:Zapp<'T>) =
+    let mutable next = inext
+    let mutable fail = ifail
+
+    interface INode<'T> with
+        member __.Next with get () = next and set v = next <- v
+        member __.Fail with get () = fail and set v = fail <- v
+        member x.Apply (state:State<'T>) =
+            state.DNode <- x // inject choices
+            current state  // run next/fail test    
+
+// the finish node sets result on the state-machine task  
+and FinishNode<'T>() =
+    interface INode<'T> with
+        member __.Apply (state:State<'T>) = 
+            state.SetComplete()
+        member __.Next with get () = Unchecked.defaultof<INode<'T>> and set _ = ()
+        member __.Fail with get () = Unchecked.defaultof<INode<'T>> and set _ = ()
+
+// the fail node will me a single instance put in all end fails
+and FailNode<'T>(fail:State<'T> -> unit,finishNode:INode<'T>) =
+    interface INode<'T> with
+        member x.Apply (state:State<'T>) = 
+            // if state.Buffer.Length = 0L then
+                state.DNode <- x
+                fail state
+            // else
+                // finishNode.Apply state
+
+        member __.Next with get () = finishNode and set _ = ()
+        member __.Fail with get () = finishNode and set _ = ()

+ 34 - 0
frameworks/FSharp/Zebra/src/App/Middleware.fs

@@ -0,0 +1,34 @@
+module Middleware
+
+open State
+open Router
+open ExecNodes
+open System.Threading.Tasks
+open System.Runtime.CompilerServices
+open Microsoft.AspNetCore.Http
+open Microsoft.AspNetCore.Hosting
+open Microsoft.AspNetCore.Builder
+
+type ZebraMiddleware<'T>(
+                        next          : RequestDelegate,
+                        Dependencies  : 'T,
+                        failAction    : State<'T> -> unit,
+                        AppBuilder    : PipeLine<'T>
+                        ) =
+    let finishNode = FinishNode() :> INode<'T>
+    let failNode = FailNode<'T>(failAction,finishNode)
+    let appNode = AppBuilder(finishNode,failNode) // build App node
+
+    do System.GC.Collect() // AppBuilder creates alot of garbage
+    
+    member __.Invoke (ctx : HttpContext) = 
+        
+        let amb = AsyncTaskMethodBuilder()
+        let state = State<'T>(ctx,Dependencies,amb)
+        appNode.Apply state
+        amb.Task
+
+
+type IApplicationBuilder with
+    member x.UseZebraMiddleware<'T>(dependencies:'T,fallback:Zapp<'T>,app:PipeLine<'T>) = 
+        x.UseMiddleware<ZebraMiddleware<'T>> [|box dependencies;box fallback;box app|] 

+ 162 - 0
frameworks/FSharp/Zebra/src/App/Parsers.fs

@@ -0,0 +1,162 @@
+module Parsers
+
+open System
+open NonStructuralComparison // needed for parser performance, non boxing of struct equality
+open OptimizedClosures       // needed to apply multi-curry args at once with adapt (invoke method)
+
+type Range =
+    struct
+        val Start  : int
+        val Finish : int
+    new(s,f)={Start=s;Finish=f}
+    end
+
+type ValueOption<'T> =
+    struct
+        val HasValue : bool
+        val Value: 'T
+    new(a,b) = {HasValue = a; Value = b}
+    end
+let inline VSome v = ValueOption<_>(true,v)
+let inline VNone () = ValueOption<'T>(false,Unchecked.defaultof<'T>)
+
+
+type Parser = FSharpFunc<string, int, int, ValueOption<obj>>
+
+let inline private between x l u = (x - l) * (u - x) >= LanguagePrimitives.GenericZero
+
+let inline private rtrn (o : 'T) = ValueOption<'T>(true, o)
+let inline private failure () = ValueOption<'T>(false,Unchecked.defaultof<'T>)
+
+/// Private Range Parsers that quickly try parse over matched range (all r.Finish checked before running in preceeding functions)
+
+let stringParse (path : string,r:Range) = path.Substring(r.Start, r.Finish - r.Start + 1) |> rtrn
+
+let charParse (path : string,r:Range) = path.[r.Start] |> rtrn // this is not ideal method (but uncommonly used)
+let boolParse (path : string,r:Range) =
+    match path.Substring(r.Start, r.Finish - r.Start) with
+    | "true"  | "True"  | "TRUE"  -> true  |> rtrn
+    | "false" | "False" | "FALSE" -> false |> rtrn
+    | _ -> failure ()
+
+let intParse (path : string,r:Range) =
+    let mutable result = 0
+    let mutable negNumber = false
+    let rec go pos =
+        let charDiff = int path.[pos] - int '0'
+        if between charDiff 0 9 then
+            result <- (result * 10) + charDiff
+            if pos = r.Finish then
+                if negNumber then - result else result
+                |> rtrn
+            else go (pos + 1)       // continue iter
+        else failure ()
+    //Start Parse taking into account sign operator
+    match path.[r.Start] with
+    | '-' -> negNumber <- true ; go (r.Start + 1)
+    | '+' -> go (r.Start + 1)
+    | _   -> go (r.Start)
+
+let int64Parse (path : string,r:Range) =
+    let mutable result = 0L
+    let mutable negNumber = false
+    let rec go pos =
+        let charDiff = int64 path.[pos] - int64 '0'
+        if between charDiff 0L 9L then
+            result <- (result * 10L) + charDiff
+            if pos = r.Finish then
+                if negNumber then - result |> rtrn else result |> rtrn
+            else go (pos + 1)       // continue iter
+        else failure ()
+    //Start Parse taking into account sign operator
+    match path.[r.Start] with
+    | '-' -> negNumber <- true ; go (r.Start + 1)
+    | '+' -> go (r.Start + 1)
+    | _   -> go (r.Start)
+
+let private decDivide =
+    [| 1.; 10.; 100.; 1000.; 10000.; 100000.; 1000000.; 10000000.; 100000000.; 100000000. |]
+    |> Array.map (fun d -> 1. / d) // precompute inverse once at compile time
+
+let floatParse (path : string,r:Range) =
+    let mutable result    = 0.
+    let mutable decPlaces = 0
+    let mutable negNumber = false
+
+    let rec go pos =
+        if path.[pos] = '.' then
+            decPlaces <- 1
+            if pos < r.Finish then go (pos + 1) else failure ()
+        else
+            let charDiff = float path.[pos] - float '0'
+            if between charDiff 0. 9. then
+                if decPlaces = 0 then
+                    result <- (result * 10.) + charDiff
+                else
+                    //result <- result + charDiff
+                    result <- result + (charDiff * decDivide.[decPlaces]) // char is divided using multiplication of pre-computed divisors
+                    decPlaces <- decPlaces + 1
+                if pos = r.Finish || decPlaces > 9 then
+                    if negNumber then - result else result
+                    |> rtrn
+                else go (pos + 1)   // continue iter
+            else failure () // Invalid Character in path
+
+    //Start Parse taking into account sign operator
+    match path.[r.Start] with
+    | '-' -> negNumber <- true ; go (r.Start + 1)
+    | '+' -> go (r.Start + 1)
+    | _   -> go (r.Start)
+
+let private guidMap = [| 3; 2; 1; 0; 5; 4; 7; 6; 8; 9; 10; 11; 12; 13; 14; 15 |]
+
+let guidParse (path : string,r:Range) =
+    let byteAry = Array.zeroCreate<byte>(16)
+    let mutable bytePos = 0
+    let mutable byteCur = 0uy
+    let mutable atHead  = true
+    let rec go pos =
+        if path.[pos] = '-' then // skip over '-' chars
+            if pos < r.Finish then go (pos + 1) else failure ()
+        else
+            let cv =  byte path.[pos]
+
+            let value =
+                if  cv >= byte '0' then
+                    if cv <= byte '9' then cv - byte '0'
+                    elif cv >= byte 'A' then
+                        if cv <= byte 'F' then cv - byte 'A' + 10uy
+                        elif cv >= byte 'a' then
+                            if cv <= byte 'f' then cv - byte 'a' + 10uy
+                            else 255uy
+                        else 255uy
+                    else 255uy
+                else 255uy
+
+            if value = 255uy then
+                failure ()
+            else
+                if atHead then
+                    byteCur <- value <<< 4
+                    atHead  <- false
+                    go (pos + 1)   // continue iter
+                else
+                    byteAry.[guidMap.[bytePos]] <- byteCur ||| value
+                    if bytePos = 15 then
+                        Guid(byteAry) |> rtrn
+                    else
+                        byteCur <- 0uy
+                        atHead  <- true
+                        bytePos <- bytePos + 1
+                        go (pos + 1)   // continue iter
+    //Start Parse
+    go (r.Start)
+
+type Parse = Parse with
+    static member inline ($) (Parse, _:string) : _ -> ValueOption<string> = stringParse
+    static member inline ($) (Parse, _:int)    : _ -> ValueOption<int> = intParse
+    static member inline ($) (Parse, _:float)  : _ -> ValueOption<float> = floatParse
+    static member inline ($) (Parse, _:bool)   : _ -> ValueOption<bool>  = boolParse
+    static member inline ($) (Parse, _:Guid)   : _ -> ValueOption<Guid>  = guidParse
+    static member inline ($) (Parse, _:int64)   : _ -> ValueOption<int64>  = int64Parse
+    static member inline ($) (Parse, _:char)   : _ -> ValueOption<char>  = charParse

+ 38 - 0
frameworks/FSharp/Zebra/src/App/Program.fs

@@ -0,0 +1,38 @@
+module App.App
+
+open State
+open Router
+open ExecNodes
+open Middleware
+open System.Threading.Tasks
+open System.Runtime.CompilerServices
+open Microsoft.AspNetCore.Http
+open Microsoft.AspNetCore.Hosting
+open Microsoft.AspNetCore.Builder
+
+
+
+[<CLIMutable>][<Struct>] 
+type JsonStructMessage = { message : string }
+
+
+[<EntryPoint>]
+let main _ = 
+    
+    let fallback : Zapp<_> = (fun ctx -> ctx {
+        text "Url Not Found"
+        status 404        
+    })
+
+    let webapp = 
+        router [
+            get "/plaintext" => (fun ctx -> ctx { text "Hello, World!" } )
+            get "/json"      => (fun ctx -> ctx { json {JsonStructMessage.message = "Hello, World!"} } )
+        ]
+
+    WebHostBuilder()
+        .UseKestrel()
+        .Configure(fun app -> app.UseZebraMiddleware<string>("zebra",fallback,webapp) |> ignore)
+        .Build()
+        .Run()
+    0

+ 225 - 0
frameworks/FSharp/Zebra/src/App/RouteNode.fs

@@ -0,0 +1,225 @@
+module RouteNode
+open State
+open ExecNodes
+open Parsers
+open System.Collections.Generic
+open System.Text
+
+
+type IRouteNode<'T> =
+    abstract Parse : Range [] * State<'T> -> unit
+
+let validFormats = set ['s';'i';'b';'f';'o'] // the PrintfFormat already valids but we check in pattern parse to be sure
+
+// Helper Functions
+////////////////////////
+
+/// Tail Clip: clip end of 'str' string staring from int pos -> end
+let inline (-|) (str:string) (from:int) = str.Substring(from,str.Length - from)
+let commonPathIndex (str1:string) (idx1:int) (str2:string) =
+    let rec go i j =
+        if i < str1.Length && j < str2.Length then
+            if str1.[i] = str2.[j]
+            then go (i + 1) (j + 1)
+            else j
+        else j
+    go idx1 0
+
+let commonPath (str1:string) (str2:string) =
+    let rec go i =
+        if i < str1.Length && i < str2.Length then
+            if str1.[i] = str2.[i]
+            then go (i + 1)
+            else i
+        else i
+    go 0
+
+type PathMatch =
+| SubMatch of int
+| PathInToken
+| TokenInPath
+| ZeroToken
+| ZeroMatch
+| FullMatch
+
+let private getPathMatch (path:string) (token:string) =
+    if token.Length = 0 then ZeroToken
+    else
+        let cp = commonPath path token
+        let tokenMatch = cp = token.Length
+        let pathMatch = cp = path.Length
+        if cp = 0 then ZeroMatch
+        elif tokenMatch && pathMatch then FullMatch
+        elif tokenMatch then TokenInPath
+        elif pathMatch  then PathInToken
+        else SubMatch cp
+
+// New Methods filter
+type METHODS =
+| GET   = 0uy
+| POST  = 1uy
+| PUT   = 2uy
+| DELETE = 3uy
+| PATCH  = 4uy
+| UNKNOWN = 5uy
+let inline methodMatch (ctx:State<'T>,method:METHODS ) = 
+        method =
+            match ctx.HttpContext.Request.Method with
+            | "GET"     -> METHODS.GET 
+            | "POST"    -> METHODS.POST
+            | "PUT"     -> METHODS.PUT
+            | "DELETE"  -> METHODS.DELETE
+            | "PATCH"   -> METHODS.PATCH
+            | _         -> METHODS.UNKNOWN
+///////
+
+type RNode<'T>(token:string) =
+    let mutable midFns : Cont<'T> list = []
+    let mutable endFns : Cont<'T> list = []
+
+    let addMidFn (mfn:Cont<'T>) = midFns <- mfn :: midFns |> List.sortBy (fun f -> f.Precedence)
+    let addEndFn (efn:Cont<'T>) = endFns <- efn :: endFns |> List.sortBy (fun f -> f.Precedence)
+
+    let mutable edges = Dictionary<char,RNode<'T>>()
+    member __.Edges
+        with get() = edges
+        and set v = edges <- v
+    member val Token = token with get,set
+    member __.MidFns
+        with get() = midFns
+        and set v = midFns <- v
+    member __.AddMidFn = addMidFn
+    member __.EndFns
+        with get()  = endFns
+        and set v = endFns <- v
+    member __.AddEndFn = addEndFn
+    member __.EdgeCount
+        with get () = edges.Count
+    member __.GetEdgeKeys = edges.Keys
+    member __.TryGetValue v = edges.TryGetValue v
+    override x.ToString() =
+        let sb = StringBuilder()
+        x.ToString(0, sb)
+        sb.ToString()
+
+    member x.ToString (depth:int, sb:StringBuilder) =
+            sb  .Append("(")
+                .Append(x.Token)
+                .Append(",{")
+                .Append(sprintf "%A" midFns)
+                .Append("|")
+                .Append(sprintf "%A" endFns)
+                .Append("},[")          |> ignore
+            if x.Edges.Count = 0 then
+                sb.Append("])\n")          |> ignore
+            else
+                sb.Append("\n")         |> ignore
+                for kvp in x.Edges do
+                    for _ in 0 .. depth do sb.Append("\t") |> ignore
+                    sb  .Append(kvp.Key)
+                        .Append(" => ") |> ignore
+                    kvp.Value.ToString(depth + 1,sb)
+                for _ in 0 .. depth do sb.Append("\t") |> ignore
+                sb.Append("])\n")    |> ignore
+                
+    static member AddFn (node:RNode<'T>) fn =
+        match fn with
+        | Empty -> ()
+        // Mid Functions
+        | InitialMatch _
+        | ApplyMatch _
+        | MatchComplete _ -> node.MidFns <- fn :: node.MidFns |> List.sortBy (fun f -> f.Precedence)
+        // End Functions
+        | HandlerMap _
+        | Complete   _    -> node.EndFns <- fn :: node.EndFns |> List.sortBy (fun f -> f.Precedence)
+
+    static member Split (node:RNode<'T>) (pos:int) =
+        // need to split existing node out
+        let sedges = node.Edges //get ref to pass to split node
+        let baseToken = node.Token.Substring(0,pos) //new start base token
+        let childToken = (node.Token -| pos)
+        let snode = RNode(childToken)
+        node.Edges <- Dictionary<_,_>() //wipe edges from node before adding new edge
+        node.Edges.Add(childToken.[0],snode)
+        //node.Add childToken Empty // create split node
+        node.Token <- baseToken
+        snode.Edges <- sedges //pass old edges dictionary to split node
+        //copy over existing functions
+        snode.MidFns <- node.MidFns
+        snode.EndFns <- node.EndFns
+        //clear functions from existing node
+        node.MidFns <- List.empty
+        node.EndFns <- List.empty
+
+    static member ExtendPath (node:RNode<'T>) (path:string) (rc:Cont<'T>) =
+        if path = "" then
+            RNode.AddFn node rc
+            node
+        else
+            match node.TryGetValue path.[0] with
+            | true, cnode ->
+                RNode.AddPath cnode path rc // recursive path scan
+            | false, _    ->
+                let nnode = RNode(path)
+                node.Edges.Add(path.[0], nnode)
+                RNode.AddFn nnode rc
+                nnode
+
+    static member AddPath (node:RNode<'T>) (path:string) (rc:Cont<'T>) =
+
+        //printfn "'%s' -> %s" path (node.ToString())
+
+        match getPathMatch path node.Token with
+        | ZeroToken ->
+            // if node empty/root
+            node.Token <- path
+            RNode.AddFn node rc
+            node
+        | ZeroMatch ->
+            failwith <| sprintf "path passed to node with non-matching start in error:%s -> %s\n\n%s\n" path node.Token (node.ToString())
+        | FullMatch ->
+            RNode.AddFn node rc
+            node
+        | PathInToken ->
+            RNode.Split node (path.Length)
+            RNode.AddFn node rc
+            node
+        | TokenInPath ->
+            //path extends beyond this node
+            let rem = path -| (node.Token.Length)
+            match node.TryGetValue rem.[0] with
+            | true, cnode ->
+                RNode.AddPath cnode rem rc // recursive path scan
+            | false, _    ->
+                let nnode = RNode(rem)
+                node.Edges.Add(rem.[0], nnode)
+                RNode.AddFn nnode rc
+                nnode
+        | SubMatch (i) ->
+            RNode.Split node (i)
+            let rem = path -| i
+            let nnode = RNode(rem)
+            node.Edges.Add(rem.[0],nnode)
+            RNode.AddFn nnode rc
+            nnode
+
+// Route Continuation Functions
+////////////////////////////////
+and Cont<'T> = 
+| Empty
+// MID
+| InitialMatch of (int * char * (char []) * RNode<'T>) // (max requried range array,next match chars,next node)
+| ApplyMatch of  (int * char * (char []) * RNode<'T>) // (range array position, format char, next match chars, next node)
+| MatchComplete of METHODS * int * IRouteNode<'T>
+// End
+| HandlerMap of METHODS * INode<'T>
+| Complete   of METHODS * IRouteNode<'T>
+    member x.Precedence
+        with get () =
+            match x with
+            | HandlerMap _      -> 0
+            | InitialMatch _    -> 1            
+            | ApplyMatch _      -> 2
+            | MatchComplete _   -> 3             
+            | Complete _        -> 4
+            | Empty             -> 5

+ 558 - 0
frameworks/FSharp/Zebra/src/App/Router.fs

@@ -0,0 +1,558 @@
+module Router
+
+open System.Collections.Generic
+open System.Text
+open Printf
+open OptimizedClosures       // needed to apply multi-curry args at once with adapt (invoke method)
+open Microsoft.AspNetCore.Http
+open Parsers
+open State
+open ExecNodes
+open RouteNode
+
+
+type Zapp<'T> = State<'T> -> unit
+
+
+
+
+// RouteNodes
+//////////////////
+type ParseFn<'a> = string * Range -> ValueOption< 'a> 
+
+type RouteNode1<'T,'a>(inext:INode<'T>,ifail:INode<'T>,current:'a -> Zapp<'T>,parseA:ParseFn<'a>) =
+    let mutable next = inext
+    let mutable fail = ifail
+    //let parseA : string * Range -> ValueOption< ^a> = Parse $ Unchecked.defaultof< ^a>
+    let fnOpt = FSharpFunc<_,_,_>.Adapt current
+    
+    // Parse like a specialised Apply so routing functions need concrete type
+    interface IRouteNode<'T> with
+        member x.Parse (range:Range [],ctx:State<'T>) =
+            let path = ctx.HttpContext.Request.Path.Value
+            let v1 = parseA(path,range.[0])
+            if v1.HasValue then
+                ctx.DNode <- x 
+                fnOpt.Invoke(v1.Value,ctx)
+            else fail.Apply ctx
+
+    interface INode<'T> with
+        member __.Next with get () = next and set v = next <- v
+        member __.Fail with get () = fail and set v = fail <- v
+        member x.Apply (_) = failwith "Who's calling apply on a route node !? ya dipshit"
+
+///////////////////      
+
+type RouteNode2<'T,'a,'b>(inext:INode<'T>,ifail:INode<'T>,fn:^a -> ^b -> Zapp<'T>,
+                            parseA:ParseFn<'a>,parseB:ParseFn<'b>) =
+    let mutable next = inext
+    let mutable fail = ifail
+    let fnOpt = FSharpFunc<_,_,_,_>.Adapt fn
+    // Parse like a specialised Apply so routing functions need concrete type
+    interface IRouteNode<'T> with 
+        member x.Parse (range:Range [],ctx:State<'T>) =
+            let path = ctx.HttpContext.Request.Path.Value
+            let v1 = parseA(path,range.[0])
+            if v1.HasValue then
+                let v2 = parseB(path,range.[1])
+                if v2.HasValue then
+                    ctx.DNode <- x
+                    fnOpt.Invoke(v1.Value,v2.Value,ctx)
+                else fail.Apply ctx    
+            else fail.Apply ctx
+
+    interface INode<'T> with
+        member __.Next with get () = next and set v = next <- v
+        member __.Fail with get () = fail and set v = fail <- v
+        member x.Apply (_) = failwith "Who's calling apply on a route node !? ya dipshit"    
+
+type RouteNode3<'T,'a,'b,'c>(inext:INode<'T>,ifail:INode<'T>,fn:'a -> 'b -> 'c -> Zapp<'T>,
+                                parseA:ParseFn<'a>,parseB:ParseFn<'b>,parseC:ParseFn<'c>) =
+    let mutable next = inext
+    let mutable fail = ifail
+    let fnOpt = FSharpFunc<_,_,_,_,_>.Adapt fn
+    // Parse like a specialised Apply so routing functions need concrete type
+    interface IRouteNode<'T> with 
+        member x.Parse (range:Range [],ctx:State<'T>) =
+            let path = ctx.HttpContext.Request.Path.Value
+            let v1 = parseA(path,range.[0])
+            if v1.HasValue then
+                let v2 = parseB(path,range.[1])
+                if v2.HasValue then
+                    let v3 = parseC(path,range.[2])
+                    if v3.HasValue then
+                        ctx.DNode <- x
+                        fnOpt.Invoke(v1.Value,v2.Value,v3.Value,ctx)
+                    else fail.Apply ctx
+                else fail.Apply ctx    
+            else fail.Apply ctx
+
+    interface INode<'T> with
+        member __.Next with get () = next and set v = next <- v
+        member __.Fail with get () = fail and set v = fail <- v
+        member x.Apply (_) = failwith "Who's calling apply on a route node !? ya dipshit"    
+
+
+type RouteNode4<'T,'a,'b,'c,'d>(inext:INode<'T>,ifail:INode<'T>,fn:'a -> 'b -> 'c -> 'd -> Zapp<'T>,
+                                parseA:ParseFn<'a>,parseB:ParseFn<'b>,parseC:ParseFn<'c>,parseD:ParseFn<'d>) =
+    let mutable next = inext
+    let mutable fail = ifail
+    let fnOpt = FSharpFunc<_,_,_,_,_,_>.Adapt fn
+    // Parse like a specialised Apply so routing functions need concrete type
+    interface IRouteNode<'T> with 
+        member x.Parse (range:Range [],ctx:State<'T>) =
+            let path = ctx.HttpContext.Request.Path.Value
+            let v1 = parseA(path,range.[0])
+            if v1.HasValue then
+                let v2 = parseB(path,range.[1])
+                if v2.HasValue then
+                    let v3 = parseC(path,range.[2])
+                    if v3.HasValue then
+                        let v4 = parseD(path,range.[3])
+                        if v4.HasValue then
+                            ctx.DNode <- x
+                            fnOpt.Invoke(v1.Value,v2.Value,v3.Value,v4.Value,ctx)
+                        else fail.Apply ctx
+                    else fail.Apply ctx
+                else fail.Apply ctx    
+            else fail.Apply ctx
+
+    interface INode<'T> with
+        member __.Next with get () = next and set v = next <- v
+        member __.Fail with get () = fail and set v = fail <- v
+        member x.Apply (_) = failwith "Who's calling apply on a route node !? ya dipshit"    
+
+
+// --------------------------------------
+// Routing Node Map Functions used to build trie
+// --------------------------------------
+
+
+// ** explicit individual cases for each method & #no of params needed to enforce types between PrintfFormat & parse functions, composition ternary operator works creating these temporary classes 
+
+type RouteBase<'T>(method:METHODS,pattern:string) =
+    member __.Method with get () = method
+    member __.Pattern with get () = pattern
+
+type RouteBase<'T,'a>(method:METHODS,pattern:string) =
+    member __.Method with get () = method
+    member __.Pattern with get () = pattern
+
+type RouteBase<'T,'a,'b>(method:METHODS,pattern:string) =
+    member __.Method with get () = method
+    member __.Pattern with get () = pattern
+
+type RouteBase<'T,'a,'b,'c>(method:METHODS,pattern:string) =
+    member __.Method with get () = method
+    member __.Pattern with get () = pattern
+
+type RouteBase<'T,'a,'b,'c,'d>(method:METHODS,pattern:string) =
+    member __.Method with get () = method
+    member __.Pattern with get () = pattern
+
+
+
+// base route base map function
+let route (method:METHODS) (path:string) = 
+    fun (next:INode<'T>,fail:INode<'T>) (root:RNode<'T>) ->
+    // Simple route that iterates down nodes and if function found, execute as normal
+        RNode.ExtendPath root path ( HandlerMap(method,next) )
+
+// // Method filtered route 
+
+let inline get (path:string)    = RouteBase<'T>( METHODS.GET,path)
+let inline post (path:string)   = RouteBase<'T>( METHODS.POST, path)
+let inline delete (path:string) = RouteBase<'T>( METHODS.DELETE, path)
+let inline put (path:string)    = RouteBase<'T>( METHODS.PUT, path)
+let inline patch (path:string)  = RouteBase<'T>( METHODS.PATCH, path)
+
+
+
+// --------------------------------------
+// Helper Functions
+// --------------------------------------
+
+// temporary compose out handler to allow composition out of route functions, same as wraping in () or using <|
+//let inline (=>) (a:HttpHandler -> Node -> Node) (b:HttpHandler) = a b
+
+let inline private addCharArray (c:char) (ary:char []) =
+    if ary |> Array.exists (fun v -> v = c) then
+        ary
+    else
+        let nAry = Array.zeroCreate<_>(ary.Length + 1)
+        Array.blit ary 0 nAry 0 ary.Length
+        nAry.[ary.Length] <- c
+        nAry
+
+// helper to get child node of same match format (slow for now, needs optimisation)
+let inline private getPostMatchNode argCount pcount (fmt:char) (nxt:char) (ils:Cont<'T> list) =
+    let rec go (ls:Cont<'T> list) (acc:Cont<'T> list) (no:RNode<'T> ValueOption) =
+        match ls with
+        | [] ->
+            if no.HasValue then
+                no.Value, acc |> List.sortBy (fun fn -> fn.Precedence)
+            else
+                let n = RNode("")
+                if pcount = 0 then
+                    n ,(InitialMatch(argCount,fmt,[|nxt|],n)) :: acc |> List.sortBy (fun fn -> fn.Precedence) // lets runtime know how big a range array to allocate
+                else
+                    n ,(ApplyMatch(pcount,fmt,[|nxt|],n)) :: acc |> List.sortBy (fun fn -> fn.Precedence) // the parameter count will let runtime know where to slot in range 
+        | hfn :: tfns ->
+            match hfn with
+            | ApplyMatch (pcount',f,ncl,n) ->
+                if f = fmt then
+                    let nncl = addCharArray nxt ncl
+                    go tfns (ApplyMatch(pcount',f,nncl,n)::acc) (VSome n)
+                    // finished as found matched format but need to complete acc list
+                else go tfns (hfn::acc) no
+            | _ -> go tfns (hfn::acc) no
+    go ils [] (VNone ())
+
+
+
+// base parameter parsing map apply funciton
+let routef (method:METHODS) (pattern : string) (fn:IRouteNode<'T>) (argCount:int) (root:RNode<'T>) =
+
+// parsing route that iterates down nodes, parses, and then continues down further notes if needed
+    let last = pattern.Length - 1
+
+    let rec go (i:int,ts:int,pcount,node:RNode<'T>) =
+        let pl = pattern.IndexOf('%',i)
+        if pl < 0 || pl = last then
+            //Match Complete (no futher parse '%' chars
+            if pcount = 0 then
+                failwith "'routef' (route Parse) used with no arguments? please add % format args or change to simple 'route' for non-parse routes"
+            else
+                RNode.ExtendPath node (pattern -| ts) ( Complete( method,fn ))  //todo: boxing & upcasting bad for performance, need to fix
+        else
+            let fmtChar = pattern.[pl + 1]
+            // overrided %% -> % case
+            if fmtChar = '%' then
+                //keep token start (+1 just one %), skip
+                go(pl + 2, ts + 1,pcount, node)
+            // formater with valid key
+            else if validFormats.Contains fmtChar then
+
+                if pl + 1 = last then // if finishes in a parse
+                    // if node.MidFns |> List.exists (function | ApplyMatchAndComplete(c,_,_) -> fmtChar = c | _ -> false )
+                    // then sprintf "duplicate paths detected '%s', Trie Build skipping..." pattern |> failwith
+                    // else
+                        RNode.ExtendPath node (pattern.Substring(ts,pl - ts)) (MatchComplete( method,pcount,fn ))
+                else //otherwise add mid pattern parse apply
+                    //get node this parser will be on
+                    let nnode = RNode.ExtendPath node (pattern.Substring(ts,pl - ts)) Empty
+                    let cnode,midFns = getPostMatchNode argCount pcount fmtChar pattern.[pl+2] nnode.MidFns
+                    nnode.MidFns <- midFns //update adjusted functions
+                    go(pl + 2, pl + 2, pcount + 1, cnode)
+            // badly formated format string that has unknown char after %
+            else
+                failwith (sprintf "Routef parsing error, invalid format char identifier '%c' , should be: b | c | s | i | d | f" fmtChar)
+                go(pl + 1, ts, pcount, node)
+
+    go(0, 0, 0, root)
+
+
+
+
+// GET Functions
+////////////////////////////////
+
+/// **get1**: GET Method filtered route with **one** parameter to be parsed and applied
+let inline get1 (fmt:PrintfFormat< ^a -> Zapp<'T>,_,_,Zapp<'T>>) = 
+    RouteBase<'T, ^a>(METHODS.GET,fmt.Value)
+
+/// **get2**: GET Method filtered route with **two** parameter to be parsed and applied
+let inline get2 (fmt:PrintfFormat< ^a -> ^b -> Zapp<'T>,_,_,Zapp<'T>>) = 
+    RouteBase<'T, ^a, ^b>(METHODS.GET,fmt.Value)
+
+/// **get3**: GET Method filtered route with **three** parameter to be parsed and applied
+let inline get3 (fmt:PrintfFormat< ^a -> ^b -> ^c -> Zapp<'T>,_,_,Zapp<'T>>) =
+    RouteBase<'T, ^a, ^b, ^c>(METHODS.GET,fmt.Value)
+
+/// **get4**: GET Method filtered route with **four** parameter to be parsed and applied
+let inline get4 (fmt:PrintfFormat< ^a -> ^b -> ^c -> ^d -> Zapp<'T>,_,_,Zapp<'T>>) =
+    RouteBase<'T, ^a, ^b, ^c>(METHODS.GET,fmt.Value)
+
+
+// POST Functions
+//////////////////////////////////
+
+/// **post1**: POST Method filtered route with **one** parameter to be parsed and applied
+let inline post1 (fmt:PrintfFormat< ^a -> Zapp<'T>,_,_,Zapp<'T>>) = 
+    RouteBase<'T, ^a>(METHODS.POST,fmt.Value)
+
+/// **post2**: POST Method filtered route with **two** parameter to be parsed and applied
+let inline post2 (fmt:PrintfFormat< ^a -> ^b -> Zapp<'T>,_,_,Zapp<'T>>) = 
+    RouteBase<'T, ^a, ^b>(METHODS.POST,fmt.Value)
+
+/// **post3**: POST Method filtered route with **three** parameter to be parsed and applied
+let inline post3 (fmt:PrintfFormat< ^a -> ^b -> ^c -> Zapp<'T>,_,_,Zapp<'T>>) (fn: ^a -> ^b -> ^c -> Zapp<'T>) =
+    RouteBase<'T, ^a, ^b, ^c>(METHODS.POST,fmt.Value)
+
+/// **post4**: POST Method filtered route with **four** parameter to be parsed and applied
+let inline post4 (fmt:PrintfFormat< ^a -> ^b -> ^c -> ^d -> Zapp<'T>,_,_,Zapp<'T>>) (fn: ^a -> ^b -> ^c -> ^d -> Zapp<'T>) =
+    RouteBase<'T, ^a, ^b, ^c>(METHODS.POST,fmt.Value)
+
+// PUT Functions
+///////////////////////////////////
+
+/// **put1**: PUT Method filtered route with **one** parameter to be parsed and applied
+let inline put1 (fmt:PrintfFormat< ^a -> Zapp<'T>,_,_,Zapp<'T>>) = 
+    RouteBase<'T, ^a>(METHODS.PUT,fmt.Value)
+
+/// **put2**: PUT Method filtered route with **two** parameter to be parsed and applied
+let inline put2 (fmt:PrintfFormat< ^a -> ^b -> Zapp<'T>,_,_,Zapp<'T>>) = 
+    RouteBase<'T, ^a, ^b>(METHODS.PUT,fmt.Value)
+
+/// **put3**: PUT Method filtered route with **three** parameter to be parsed and applied
+let inline put3 (fmt:PrintfFormat< ^a -> ^b -> ^c -> Zapp<'T>,_,_,Zapp<'T>>) (fn: ^a -> ^b -> ^c -> Zapp<'T>) =
+    RouteBase<'T, ^a, ^b, ^c>(METHODS.PUT,fmt.Value)
+
+/// **put4**: PUT Method filtered route with **four** parameter to be parsed and applied
+let inline put4 (fmt:PrintfFormat< ^a -> ^b -> ^c -> ^d -> Zapp<'T>,_,_,Zapp<'T>>) (fn: ^a -> ^b -> ^c -> ^d -> Zapp<'T>) =
+    RouteBase<'T, ^a, ^b, ^c>(METHODS.PUT,fmt.Value)
+
+// DELETE Functions
+///////////////////////////////////
+
+/// **delete1**: delete Method filtered route with **one** parameter to be parsed and applied
+let inline delete1 (fmt:PrintfFormat< ^a -> Zapp<'T>,_,_,Zapp<'T>>) = 
+    RouteBase<'T, ^a>(METHODS.DELETE,fmt.Value)
+
+/// **delete2**: delete Method filtered route with **two** parameter to be parsed and applied
+let inline delete2 (fmt:PrintfFormat< ^a -> ^b -> Zapp<'T>,_,_,Zapp<'T>>) = 
+    RouteBase<'T, ^a, ^b>(METHODS.DELETE,fmt.Value)
+
+/// **delete3**: delete Method filtered route with **three** parameter to be parsed and applied
+let inline delete3 (fmt:PrintfFormat< ^a -> ^b -> ^c -> Zapp<'T>,_,_,Zapp<'T>>) (fn: ^a -> ^b -> ^c -> Zapp<'T>) =
+    RouteBase<'T, ^a, ^b, ^c>(METHODS.DELETE,fmt.Value)
+
+/// **delete4**: delete Method filtered route with **four** parameter to be parsed and applied
+let inline delete4 (fmt:PrintfFormat< ^a -> ^b -> ^c -> ^d -> Zapp<'T>,_,_,Zapp<'T>>) (fn: ^a -> ^b -> ^c -> ^d -> Zapp<'T>) =
+    RouteBase<'T, ^a, ^b, ^c>(METHODS.DELETE,fmt.Value)
+
+
+// Runtime Processor
+/////////////////////////////////////////
+
+type checkCompletionPathResult<'T> =
+    struct
+        val Success :bool
+        val Position : int
+        val Node : RNode<'T>
+    new(a,b,c) = {Success = a;Position = b;Node = c}    
+    end
+
+type getNodeCompletionResult<'T> =
+    struct
+        val Success :bool
+        val Prend : int
+        val Nxtpos : int 
+        val Nxtnode : RNode<'T>
+    new(a,b,c,d) = {Success = a;Prend = b;Nxtpos = c;Nxtnode =d}  
+    end
+
+let private emptyRange = Unchecked.defaultof<Range []>
+
+let processPath (abort:INode<'T>) (root:RNode<'T>) : Zapp<'T> =
+
+    fun ctx ->
+
+        //let abort  = setStatusCode 404 >=> text "Not found"
+
+        let path : string = ctx.HttpContext.Request.Path.Value
+        let last = path.Length - 1
+
+        let rec crawl (pos:int , node:RNode<'T>, mf , ef) =
+            if node.Token.Length > 0 then
+                let cp = commonPathIndex path pos node.Token
+                if cp = node.Token.Length then
+                    let nxtChar = pos + node.Token.Length
+                    if (nxtChar - 1 ) = last then //if have reached end of path through nodes, run HandlerFn
+                        ef(node.EndFns, pos, emptyRange )
+                    else
+                        match node.TryGetValue path.[nxtChar] with
+                        | true, cnode ->
+                            if (pos + cnode.Token.Length ) = last then //if have reached end of path through nodes, run HandlerFn
+                                ef(cnode.EndFns, pos + node.Token.Length, emptyRange )
+                            else                //need to continue down chain till get to end of path
+                                crawl (nxtChar,cnode,mf,ef)
+                        | false, _ ->
+                            // no further nodes, either a static url didnt match or there is a pattern match required
+                            mf( node.MidFns, nxtChar, emptyRange )
+                else
+                    abort.Apply ctx
+            elif node.Token.Length = 0 then
+                match node.TryGetValue path.[pos] with
+                | true, cnode ->
+                    crawl (pos,cnode,mf,ef)
+                | false, _ ->
+                    // no further nodes, either a static url didnt match or there is a pattern match required
+                    mf( node.MidFns, pos , emptyRange )
+            else
+                //printfn ">> failed to match %s path with %s token, commonPath=%i" (path.Substring(pos)) (node.Token) (commonPathIndex path pos node.Token)
+                abort.Apply ctx
+
+        let rec checkCompletionPath (pos:int,node:RNode<'T>) = // this funciton is only used by parser paths
+            //this function doesn't test array bounds as all callers do so before
+            let inline success(pos,node) = checkCompletionPathResult(true,pos,node)                         // todo: move out under type
+            let inline failure(pos)      = checkCompletionPathResult(false,pos,Unchecked.defaultof<RNode<'T>>)   // todo: move out under type
+
+            if commonPathIndex path pos node.Token = node.Token.Length then
+                let nxtChar = pos + node.Token.Length
+                if (nxtChar - 1) = last then //if this pattern match shares node chain as substring of another
+                    if node.EndFns.IsEmpty
+                    then failure pos //pos, None
+                    else success(nxtChar,node) //nxtChar, Some node
+                else
+                    match node.TryGetValue path.[nxtChar] with
+                    | true, cnode ->
+                        checkCompletionPath(nxtChar,cnode)
+                    | false, _ ->
+                        // no further nodes, either a static url didnt match or there is a pattern match required
+                        if node.MidFns.IsEmpty
+                        then failure pos
+                        else success(nxtChar,node)
+            else failure pos
+
+        /// (next match chars,pos,match completion node) -> (parse end,pos skip completed node,skip completed node) option
+        let rec getNodeCompletion (cs:char [], pos ,node:RNode<'T>) =
+            let inline success(prend,nxtpos,nxtnode) = getNodeCompletionResult (true,prend,nxtpos,nxtnode)              // todo: move out under type
+            let inline failure ()                    = getNodeCompletionResult (false,0,0,Unchecked.defaultof<RNode<'T>>)    // todo: move out under type
+
+            match path.IndexOfAny(cs,pos) with // jump to next char ending (possible instr optimize vs node +1 crawl)
+            | -1 -> failure ()
+            | x1 -> //x1 represents position of match close char but rest of chain must be confirmed
+                let cp = checkCompletionPath(x1,node) 
+                if cp.Success 
+                then success(x1 - 1,cp.Position,cp.Node)                 // from where char found to end of node chain complete
+                else getNodeCompletion(cs, x1 + 1, node) // char foundpart of match, not completion string
+
+
+        //let createResult (args:obj list) (argCount:int) (pfc:ParseFnCache) =
+
+
+        let rec processEnd (fns:Cont<'T> list, pos, range:Range []) =
+            match fns with
+            | [] -> abort.Apply ctx
+            | h :: t ->
+                match h with
+                | HandlerMap(method,inode) ->
+                    if methodMatch(ctx,method) then
+                        ctx.PathPosition <- pos
+                        inode.Apply ctx
+                    else
+                        abort.Apply ctx
+                | Complete (method,fn) -> 
+                    if methodMatch(ctx,method) then
+                        ctx.PathPosition <- pos
+                        fn.Parse(range,ctx)
+                    else
+                        abort.Apply ctx
+                | x -> failwithf "Cont Mapping failed: %A in processEnd" x                    
+
+        let rec processMid (fns:Cont<'T> list,pos, range) =
+
+            let inline applyMatchAndComplete pos pcount (range:Range []) (fn:IRouteNode<'T>) tail =
+                range.[pcount] <- Range(pos,last)
+                ctx.PathPosition <- pos
+                fn.Parse(range,ctx)  /// fall back on list not included and using default abort need tail fallback ???
+                // else processMid(tail, pos, range) // ??????????????????
+
+            let rec applyMatch pos pcount (f:char) (ca:char[]) n (range:Range []) tail  =
+                let nc = getNodeCompletion(ca, pos, n) 
+                match nc.Success with
+                | true -> //,fpos,npos,cnode)
+                    range.[pcount] <- Range(pos, nc.Prend)                    
+                    
+                    if nc.Nxtpos - 1 = last then //if have reached end of path through nodes, run HandlerFn
+                        processEnd(nc.Nxtnode.EndFns, nc.Nxtpos, range )
+                    else
+                        processMid(nc.Nxtnode.MidFns, nc.Nxtpos, range )
+                | false -> processMid(tail, pos, range) // subsequent match could not complete so fail
+
+
+            let inline InitialMatch argCount fmt nextChars node tail =
+                let range = Array.zeroCreate<Range> argCount    // Allocate range cursor
+                applyMatch pos 0 fmt nextChars node range tail  // Apply match using 0 inital paramter position
+
+            match fns with
+            | [] -> abort.Apply ctx
+            | h :: t ->
+                match h with
+                | InitialMatch (argCount,fmt,nextChars,node) -> InitialMatch argCount fmt nextChars node t                 
+                | ApplyMatch (pcount,fmt,nexts,node) -> applyMatch pos pcount fmt nexts node range t
+                | MatchComplete (method,pcount,fn) -> 
+                    if methodMatch(ctx,method) then
+                        if pcount = 0 then
+                            applyMatchAndComplete pos pcount [|Range(pos,last)|] fn t  //<< HACK
+                        else
+                            applyMatchAndComplete pos pcount range fn t 
+                    else abort.Apply ctx
+                | x -> failwithf "Cont Mapping failed: %A in processMid" x 
+
+        // begin path crawl process
+        crawl(ctx.PathPosition,root,processMid,processEnd)
+
+
+type RouterNode<'T>(inext:INode<'T>,ifail:INode<'T>,routes:((INode<'T> * INode<'T>) -> RNode<'T> -> RNode<'T>) list) =
+
+    let mutable next = inext
+    let mutable fail = ifail
+
+    let inode = RNode("")    // Create a new base node for each route group, state pathpos allows autonomy
+
+    do // build out the route tree from the routes, keeping reference to the base node 
+        for routeFn in routes do
+            routeFn (next,fail) inode |> ignore        
+
+    interface INode<'T> with
+        member __.Next with get () = Unchecked.defaultof<INode<'T>> and set _ = ()
+        member __.Fail with get () = Unchecked.defaultof<INode<'T>> and set _ = ()
+        member x.Apply (state:State<'T>) = 
+            processPath fail inode state
+
+
+let inline router (routes:((INode<'T> * INode<'T>) -> RNode<'T> -> RNode<'T>) list) =
+    fun (next:INode<'T>,fail:INode<'T>) ->
+        RouterNode<'T>(next,fail,routes) :> INode<'T>
+
+
+let subRoute (path:string) (fns:((INode<'T> * INode<'T>) -> RNode<'T>->RNode<'T>) list) = 
+    fun (itree:INode<'T> * INode<'T>) (parent:RNode<'T>) ->
+        let child = RNode.ExtendPath parent path Empty
+        for fn in fns do
+            fn itree child |> ignore
+        child
+
+type ChooseWrap<'T>(fns:PipeLine<'T> list) =
+    member x.Apply(next:INode<'T>,fail:INode<'T>) =
+        let rec go (ls:PipeLine<'T> list) =
+            match ls with
+            | [] -> fail
+            | h :: t ->
+                h(next,go t)
+        go fns
+
+/// **Description**
+///     Choose provides a list of options the app attempts in order listed, returning false in any pipeline will proceed to the next pipeline on the list 
+/// **Parameters**
+///   * `fns` - parameter of type `PipeLine<'T> list` 
+///     *Pipelines required so if using a single Handler, use `pipeline` function to convert/wrap handler to pipeline*
+///
+/// **Output Type**
+///   * `ChooseWrap<'T>` - A Temporary type wrapper used for composition binding
+///
+/// **Exceptions**
+///
+/// 
+type PipelineList<'T> = PipeLine<'T> list
+type ActionList<'T> = (State<'T> -> unit) list
+
+let choose (fns:PipeLine<'T> list) = ChooseWrap<'T>(fns)
+
+// Wraps a Handler (Zapp) in a Pipeline function to allow binding in choose and other fixed seq type scenarios 
+let pipeline (fn:Zapp<'T>) =
+    fun (next:INode<'T>,fail:INode<'T>) ->
+        ChoiceNode(next,fail,fn) :> INode<'T>
+

+ 210 - 0
frameworks/FSharp/Zebra/src/App/State.fs

@@ -0,0 +1,210 @@
+module State
+
+open System.Threading.Tasks
+open System
+open System.Runtime.CompilerServices
+open System.Threading
+open Microsoft.AspNetCore.Http
+open Microsoft.Extensions.Primitives
+open System.Xml.Serialization
+open StreamBuffer
+open System.IO
+open EncodeHelper
+open System.Net.Mime
+
+[<Struct>]
+type GenericStateAwaiter<'T>(awaiter:TaskAwaiter<'T> ,continuation:'T -> unit,methodBuilder:AsyncTaskMethodBuilder) =
+    interface IAsyncStateMachine with
+        member __.MoveNext() =
+                continuation ( awaiter.GetResult() )  // runs cont, will update awaiter & continuation
+        member __.SetStateMachine (sm) = methodBuilder.SetStateMachine sm 
+
+[<Struct>]
+type PlainStateAwaiter(continuation:unit -> unit,methodBuilder:AsyncTaskMethodBuilder) =
+    interface IAsyncStateMachine with
+        member __.MoveNext() =
+                continuation ()  // runs cont, will update awaiter & continuation
+        member __.SetStateMachine sm = methodBuilder.SetStateMachine sm
+
+[<Struct>]
+type FinishStateAwaiter(methodBuilder:AsyncTaskMethodBuilder,ms:MemoryStream) =
+    interface IAsyncStateMachine with
+        member __.MoveNext() =         
+            MemoryStreamCache.Release ms
+            methodBuilder.SetResult()
+        member __.SetStateMachine sm = methodBuilder.SetStateMachine sm  
+
+
+////////////////
+// nodes are embedded in list so on each node load there is no additional calls to fucntions
+type ContentType =
+| Text = 0uy
+| Json = 1uy
+| Xml = 2uy
+| Custom = 3uy
+
+
+type INode<'T> =
+    abstract member Next  : INode<'T> with get, set
+    abstract member Fail  : INode<'T> with get, set
+    abstract member Apply : State<'T> -> unit
+
+and State<'T>(hctx: HttpContext, deps: 'T,amb: AsyncTaskMethodBuilder) =
+        let buffer = MemoryStreamCache.Get()
+        
+        member val MethodBuilder : AsyncTaskMethodBuilder = amb with get
+        member val HttpContext              = hctx with get,set
+        member val Dependencies             = deps with get // alternative dependency injection system
+        member val DNode : INode<'T>        = Unchecked.defaultof<INode<'T>> with get, set // <<<TEMP
+
+        member val PathPosition             = 0 with get, set // pos starts 0 and mutates via routers
+
+        member val Disposables              = List.empty<IDisposable> with get, set
+        member __.Buffer with get () = buffer
+        member val ContentType = ContentType.Text with get, set
+
+        member inline x.Next   () = x.DNode.Next.Apply x
+        member inline x.Fail   () = x.DNode.Next.Apply x
+                   
+        // Computaion Expresssion Members
+        /////////////////////////////////
+        member inline x.Run(_:unit) = () // runs last on customOperations, runs after first bind
+
+        member inline x.Run(n:INode<'T>) = // runs last on customOperations, runs after first bind
+            n.Apply x
+
+
+        member inline x.Zero() =
+            // Zero presumes a true, failing must be explicit, progressing is implicit 
+            x.DNode.Next.Apply x
+
+        member inline x.Return(_:unit) = x.DNode.Next 
+        member inline x.Return(result: bool) = // runs after last bind
+            if result then
+                x.DNode.Next.Apply x
+            else
+                x.DNode.Fail.Apply x
+        member inline x.Return(a) = () // passthrough bound values for now
+
+        member inline x.Bind<'a,'b>(n:INode<'T>,b:unit -> unit) = b () // input is output of last custom op
+
+        member inline x.Bind(task:Task, continuation : unit -> unit) : unit =
+            let awt : TaskAwaiter = task.GetAwaiter()
+            if awt.IsCompleted then
+                continuation ()
+            else
+                let mutable awtref = awt
+                let mutable smref = PlainStateAwaiter(continuation,x.MethodBuilder)
+                x.MethodBuilder.AwaitUnsafeOnCompleted(&awtref,&smref)
+
+        member inline x.Bind(configurableTaskLike:Task< ^inp>, continuation : ^inp -> unit) : unit =
+            let awt : TaskAwaiter< ^inp> = configurableTaskLike.GetAwaiter() 
+            if awt.IsCompleted then
+                continuation (awt.GetResult())
+            else
+                let mutable awtref = awt
+                let mutable smref =  GenericStateAwaiter< ^inp>(awt,continuation,x.MethodBuilder)
+                x.MethodBuilder.AwaitUnsafeOnCompleted(&awtref,&smref)          
+
+        member inline x.Using(disp : #IDisposable, continuation : #IDisposable -> unit) =
+            x.Disposables <- disp :> IDisposable :: x.Disposables
+            continuation disp
+               
+        member inline x.SetComplete() =
+            let inline contType (v:string) = x.HttpContext.Response.Headers.["Content-Type"] <- StringValues v
+            
+            if x.Buffer.Length > 0L then
+                x.HttpContext.Response.Headers.["Content-Length"] <- StringValues(x.Buffer.Length.ToString())
+                x.HttpContext.Response.StatusCode <- 200
+                match x.ContentType with
+                | ContentType.Custom -> ()
+                | ContentType.Text -> contType "text/plain" 
+                | ContentType.Json -> contType "application/json"
+                | ContentType.Xml  -> contType "application/xml"
+                | _                -> contType "text/plain"
+
+                x.Buffer.Seek(0L,SeekOrigin.Begin) |> ignore
+
+                let t = x.Buffer.CopyToAsync(x.HttpContext.Response.Body)
+                let awt = t.GetAwaiter()
+                let mutable awtref = awt
+                let mutable smref = FinishStateAwaiter(x.MethodBuilder,x.Buffer)
+                x.MethodBuilder.AwaitUnsafeOnCompleted(&awtref,&smref)
+
+            else                
+                x.HttpContext.Response.Headers.["Content-Length"] <- StringValues("0")
+                x.HttpContext.Response.StatusCode <- 404
+                x.MethodBuilder.SetResult()
+
+            // at end of pipeline when buffer written, we can start disposing waiting for Async copy to finish                         
+            for disp in x.Disposables do disp.Dispose()   // may need to be pushed out to FinishStateAwaiter
+        
+        // Custom Ops
+        /////////////////////////////
+        
+
+        // Text
+        ////////
+        member inline x.Text(text: string ) = StreamWrite(text,x.Buffer) ;    
+
+        [<CustomOperation("text",MaintainsVariableSpaceUsingBind=true)>] //,MaintainsVariableSpaceUsingBind = true)>]
+        
+        /// **Description**
+        ///   Writes text to the state buffer that will be flushed at the end  
+        /// **Parameters**
+        ///   * `
+        ///   * `text` - parameter of type `string`
+
+        member inline x.Text<'a>(n:INode<'T>, text: string ) =  x.Text(text); n
+
+        // Json
+        //////////
+        
+        member inline x.Json< ^a>(value: ^a ) =
+            Utf8Json.JsonSerializer.Serialize< ^a>(x.Buffer,value)
+            x.ContentType <- ContentType.Json
+        [<CustomOperation("json",MaintainsVariableSpaceUsingBind=true)>]//,MaintainsVariableSpaceUsingBind = true)>]
+        member inline x.Json< ^a>(n:INode<'T>, value: ^a ) =  x.Json< ^a>(value: ^a); n
+
+
+        // XML
+        /////////
+        member inline x.Xml< ^a>(value: ^a ) =
+            let ser = XmlSerializer(typeof< ^a>).Serialize(x.Buffer,value)   // <<< should find better serialiser
+            x.ContentType <- ContentType.Xml
+
+        [<CustomOperation("xml",MaintainsVariableSpaceUsingBind=true)>]//,MaintainsVariableSpaceUsingBind = true)>]
+        member inline x.Xml< ^a>(n:INode<'T>, value: ^a ) = x.Xml< ^a>(value: ^a) ; n
+        
+        // setHeader
+        ////////////
+        member inline x.SetHeader(header: string, value:string ) =
+            x.HttpContext.Response.Headers.[header] <- StringValues(value)  
+
+        [<CustomOperation("setHeader",MaintainsVariableSpaceUsingBind=true)>]//,MaintainsVariableSpaceUsingBind = true)>]
+        member inline x.SetHeader(n:INode<'T>, header: string, value:string ) = x.SetHeader(header, value) ; n
+
+        // status code
+        ///////////////
+        member inline x.SetStatus(value:int ) =
+            x.HttpContext.Response.StatusCode <- value
+
+        [<CustomOperation("status",MaintainsVariableSpaceUsingBind=true)>]//,MaintainsVariableSpaceUsingBind = true)>]
+        member inline x.SetStatus(n:INode<'T>, value:int ) = x.SetStatus(value) ; n
+
+        // content_type
+        ////////////////////
+        member inline x.SetContentType(value:string ) =
+            x.ContentType <- ContentType.Custom
+            x.HttpContext.Response.Headers.["Content-Type"] <- StringValues value
+
+        
+        [<CustomOperation("content_type",MaintainsVariableSpaceUsingBind=true)>]//,MaintainsVariableSpaceUsingBind = true)>]
+        member inline x.SetContentType(_:unit, value:string ) = x.SetContentType(value)
+
+
+
+and Zapp<'T> = State<'T> -> unit
+and PipeLine<'T> = (INode<'T> * INode<'T>) -> INode<'T>
+
+

+ 12 - 0
frameworks/FSharp/Zebra/zebra.dockerfile

@@ -0,0 +1,12 @@
+FROM microsoft/dotnet:2.1-sdk-stretch AS build
+WORKDIR /app
+COPY src/App .
+RUN dotnet publish -c Release -o out
+
+FROM microsoft/dotnet:2.1-aspnetcore-runtime AS runtime
+ENV ASPNETCORE_URLS http://+:8080
+ENV COMPlus_ReadyToRun 0
+WORKDIR /app
+COPY --from=build /app/out ./
+
+ENTRYPOINT ["dotnet", "App.dll"]