xyncro / freya

Freya Web Stack - Meta-Package
https://freya.io
Other
329 stars 30 forks source link

Typed Routes #105

Open panesofglass opened 9 years ago

panesofglass commented 9 years ago

We may be able to leverage functionality from https://github.com/SuaveIO/suave once/if ever we share a common core. /cc @theimowski

Vidarls commented 8 years ago

So, I've shaved some yaks...

One of my favourite aspects of using F# is the types, and how little effort adding a new type is.

I tend to create specific types for identifiers, even if I could have used a GUID or int to keep me from passing in the wrong one. Also it makes composite Id's a lot easier to work with, as these can encapsulate a set if int's or similar.

A common use case is /get/{myIdentifier} in urls to get a resource by its id.

I really would like to be able to get the Id in a strongly typed manner, even if the type is not a primitive.

Long story short, I've hacked together something I am building of myself, and thought maybe could be interesting to refine and pull into the Freya stack:

[<AutoOpen>]
module UrlParams = 
    type UrlParam<'a> = UrlParam of string * (string option -> 'a option)

    // Extending UriTemplate from Freya to accept typed params
    type UriTemplate
        with
            static member ParseTyped (pattern, (UrlParam (p1,_))) =
                UriTemplate.Parse (sprintf pattern p1) 
            static member ParseTyped (pattern, (UrlParam (p1,_)), (UrlParam (p2,_))) =
                UriTemplate.Parse (sprintf pattern p1 p2)
            static member ParseTyped (pattern, (UrlParam (p1,_)), (UrlParam (p2,_)), (UrlParam (p3,_))) =
                UriTemplate.Parse (sprintf pattern p1 p2 p3)

    let tryGetUrlParam (UrlParam (name, _)) = 
        Freya.memo (Freya.Lens.getPartial (Route.Atom_ name))

    let tryConvert converter value = 
        value 
        |> Option.map converter 
        |> Option.bind (function | (true, value) -> Some(value) | _ -> None)

    let tryConvertInt32 = tryConvert Int32.TryParse
    let tryConvertDate = tryConvert DateTime.TryParse
    let tryConstruct cons value = 
        Option.map cons value

    //Helpers / accessors to get / reason about url params
    type UrlParam 
        with
            member this.TryGetValue =
                let (UrlParam (name, cons)) = this
                Freya.memo (cons <!> (tryGetUrlParam this))
            member this.Value = 
                Freya.memo (Option.get <!> this.TryGetValue)
            member this.Exists
                with get () =
                    Freya.memo (Option.isSome <!> this.TryGetValue)

    let haveParam (p:UrlParam<_>) = 
        p.Exists

    //Example parameters
    [<RequireQualifiedAccess>]
    module Params = 
        let clinicId = UrlParam ("clinicid", tryConvertInt32 >> tryConstruct ClinicId)
        let userId = UrlParam ("userid", tryConvertInt32 >> tryConstruct UserId)
        let date = UrlParam("date",tryConvertDate >> tryConstruct (fun d -> d))

        //Attempt to implement a composite parameter:
        module animalId  =
            [<RequireQualifiedAccess>]
            module Params  =
                let clinic = UrlParam("animalclinicid", tryConvertInt32 >> tryConstruct (ClinicId >> CustomerClinic))
                let id = UrlParam("animalid", tryConvertInt32 >> tryConstruct (fun i -> i))
            let TryGetValue = 
                freya {
                    let! animalClinic = Params.clinic.TryGetValue
                    let! animal = Params.id.TryGetValue
                    return match (animalClinic, animal) with
                           | Some(c), Some(a) -> Some (AnimalId(c,a))
                           | _                -> None
                } |> Freya.memo
            let Exists = 
                Freya.memo (Option.isSome <!> TryGetValue)
            let Value = 
                Freya.memo (Option.get <!> TryGetValue)

Defining routes:

//From inside a router def:

resource (UriTemplate.ParseTyped ("/user/{%s}", Params.userId)) notImplemented

// Checking for existance (inside machine def)

exists (Params.userId.Exists)

// Getting the value:
freya {
  let! value = Params.userId.Value
}

Feel free to use as inspiration, or discard :-)

Vidarls commented 8 years ago

looks like I need to read the docs more closely to see if I can get more inline with the uri template way of thinking. (http://docs.freya.io/en/latest/router/examples.html) any feedback is welcome though..

kolektiv commented 8 years ago

Oh that looks like some really interesting thinking :smile: I'd definitely like to experiment with something like that, maybe as an add-on or extension to the router. I think optics could be the way to do some of the typing work, I'm going to have to experiment!

Thank you for sharing, there's definitely something worth working on here. I want to get 3.0 out and sorted, would you be interested in working out a direction to take this forward once I've got that done? (Hopefully not too far away!)