Last active
November 9, 2015 14:39
-
-
Save owskio/8aaf1dc09fb4c3cb7f27 to your computer and use it in GitHub Desktop.
FParsec + HttpListener + Pattern Matching = Simple 'REST'
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// EXAMPLES: | |
// | |
// http://localhost:8888/people/chefs/Jimbo?eggs=2&cheese=4 | |
// http://localhost:8888/people/chefs/Jimbo?cheese=4&eggs=2 | |
// http://localhost:8888/people/chefs/Jimbo?cheese=4&eggs=2&bacon=5 | |
// | |
// USAGE: | |
// C:\RestForFree>fsi restForFree.fsx | |
// | |
// SEE ALSO: | |
// https://www.branded3.com/blog/creating-a-simple-http-server-with-f | |
#I @"C:\RestForFree" | |
#r @"FParsec.1.0.2\lib\net40-client\FParsecCS.dll" | |
#r @"FParsec.1.0.2\lib\net40-client\FParsec.dll" | |
//////////////////////////////////////////////////////////////////////////////// | |
//Naive, but its just a pedantic example | |
module parser = | |
open System; open FParsec | |
//Just give me my result, parsec! | |
let (-=>) input someParser = | |
run someParser input | |
|> function | |
| Success(result,_,_) -> result | |
| _ -> Unchecked.defaultof<'T> //Equivalent to C#'s default(T), | |
//both match paths need same return type | |
type url = { | |
pathString: string | |
queryString: string } | |
let not = (<>) //Just eliminating parens | |
let psplitBy char = | |
let keep = manySatisfy(not char) | |
let toss = pstring(string(char)) | |
sepBy(keep)(toss) | |
let pathParser = psplitBy('/') | |
let keyOrVal = many(noneOf "&=") |>> String.Concat | |
let keyValuePair = keyOrVal .>> pstring "=" .>>. keyOrVal | |
let queryParser = sepBy(keyValuePair)(pstring "&") | |
let preamble = parse { | |
do! pstring "http" |>> ignore //Proto | |
do! skipString "://" // | |
do! many(noneOf ":/") |>> String.Concat |>> ignore //host | |
do! pstring(":") >>. manySatisfy(Char.IsDigit) <|>% "" |>> ignore } //port? | |
let urlParser = parse { | |
do! preamble | |
let! pathString = pstring("/") >>. manySatisfy(not '?') <|>% "" | |
let! queryString = pstring("?") >>. restOfLine(true) <|>% "" | |
return { | |
pathString = pathString | |
queryString = queryString } } | |
//////////////////////////////////////////////////////////////////////////////// | |
module listener = | |
open System; open System.Net; open System.Text | |
//Eliminate .NET line noise, | |
type HttpListenerResponse with | |
member me.WriteAsync str = | |
Encoding.ASCII.GetBytes(s=str) | |
|> me.OutputStream.AsyncWrite | |
//Config | |
let root,host = @"C:\RestForFree","http://localhost:8888/" | |
//.NET init stuff | |
let initListener = | |
let l = new HttpListener() | |
l.Prefixes.Add host | |
l.Start() | |
l | |
//As it turns out, Async.* was recently, to accomodate futures/deferreds | |
//However, HttpListener uses 'the IAsyncResult design pattern [sic]', | |
//which is older/more-lower-level | |
//https://msdn.microsoft.com/en-us/library/system.iasyncresult(v=vs.110).aspx | |
let listen = | |
let l = initListener //Called once upon definition of 'listen' | |
async { //Is accessed repeatedly, however | |
let b,e = l.BeginGetContext, l.EndGetContext | |
let! context = Async.FromBeginEnd(b, e) | |
return context.Request,context.Response } | |
let listening requestHandler = | |
Async.Start <| async { //Non-blocking | |
while true do | |
let! request, response = listen //Get the context asynchronously | |
do! requestHandler request response } //Handle the context async | |
Console.ReadLine() |> ignore //Retain console for printfn | |
//////////////////////////////////////////////////////////////////////////////// | |
module http = | |
open System.Net; open listener | |
//Parens for sake of type inferencer confused by partial application | |
let httpWrapper (restHandler)(request)(response:HttpListenerResponse) = | |
async { | |
response.StatusCode <- int HttpStatusCode.OK //Response packaging | |
response.ContentType <- "text/html" //Response packaging | |
let! mainOutput = restHandler request response | |
do! response.WriteAsync mainOutput | |
response.Close() } //Resource management :-) | |
//TODO: Find IDisposables | |
// and 'use' them. | |
//////////////////////////////////////////////////////////////////////////////// | |
open parser; open listener; open http | |
//Have to sort tuples by key alphabetically, lest there be no match! :-( | |
let (|ATOZ|) = ATOZ List.sortBy fst | |
let pretty s = sprintf "%A" s | |
listening(httpWrapper(fun request response -> | |
//Thank you Microsoft, but I will parse the url myself | |
request.Url.AbsoluteUri -=> urlParser | |
|> function | |
| { url.pathString = pathString; url.queryString = queryString } -> | |
//'Routes' | |
pathString -=> pathParser | |
|> function | |
//"href=/people/chefs/Jimbo" | |
| [ "people"; "chefs"; chef ] -> | |
//Remote Procedure Call (RPC)-style 'rest' endpoints | |
queryString -=> queryParser | |
|> function | |
| ATOZ[ "cheese",b ; "eggs",a ] -> async { | |
return | |
chef + " cannot make omlettes :-( ... <br />" + | |
"But, Dexter can make " + string(int a + int b + 1000) + | |
" omlettes du fromage!! <br />" + | |
"because he is a boy-genious, with a very large laboratory." } | |
//b, then c, then e, here, but not in the url! | |
| ATOZ[ "bacon",b ; "cheese",c ; "eggs",a ] -> async { | |
return | |
chef + " can make " + string(int a + int b + int c) + " omlets!" } | |
//b, then c, then e, here, but not in the url! | |
| ATOZ[ "bacon",b ; "cheese",c ; "eggs",a ; "mushrooms",m ] -> async { | |
return "Mushrooms...Disgusting!" } | |
| ATOZ query -> async { | |
return "NO QUERY MATCH FOR: " + pretty query } | |
| path -> async { return "NO PATH MATCH FOR: " + pretty path } | |
)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment