GithubHelp home page GithubHelp logo

Comments (15)

slang25 avatar slang25 commented on July 4, 2024 3

I will attempt this evening to implement the following api, as suggested by a friend:

let app =
  choose [
    route "/ping" >>= text "pong"
    route "/" >>= text "index"
    subPath "/api" (choose [
        route "" >>= setStatusCode 204 >>= text "No Content"
        route "/users" >>= text "users"
      ])
    route "/docs" >>= text "docs"
  ]

So the change would be to add subPath that takes a path and a handler (no >>= required), that way it maintains the scope so that the HttpContext Item value doesn't bleed out into other handlers (and you could nest them), and to make route aware of this.

from giraffe.

dustinmoris avatar dustinmoris commented on July 4, 2024 1

Hi,

What about using the routeStartsWith handler?

let app =
    choose [
        route "/ping"   >>= text "pong"
        route "/"       >>= text "index"
        routeStartsWith "/api" >>=
            choose [
                route "/api"        >>= setStatusCode 204 >>= text "No Content"
                route "/api/users"  >>= text "users"
            ]
        route "/docs"   >>= text "docs"
    ]

This still requires the full route in the sub paths, but I think it probably solves most use cases. The routeStartsWith is useful when you want to add an additional handler above all /api routes only once (e.g. for authentication). It also doesn't require to store intermediate state in the items collection, which could be easily tampered with by other handlers.

I should probably document this somewhere, but I would like to keep the standard library as small as possible to avoid bloat over time. New handlers should ideally be useful to a broad range of users. When in doubt I rather risk not adding a useful handler in the beginning than adding too many too early, especially because most handlers are so small and quick to write that some missing functionality can always be added in a project where needed.

For example, currently I use Lambda myself for a couple projects and I added new handlers there as well, but refused to add them to the standard list yet, because I want to see if they really prove to be useful over time to deserve being added for everyone.

Having said that, I am open to the idea of your proposed handler. Could you perhaps explain the use case you have for it?

from giraffe.

slang25 avatar slang25 commented on July 4, 2024

I would love to see something like this. I would prefer it to be just one method, but because you are flowing the path through HttpContext you have to have this rigid ordering of these functions. Not sure if there's another way to achieve this?

from giraffe.

dustinmoris avatar dustinmoris commented on July 4, 2024

I was just thinking would adding a routeEndsWith handler satisfy your use case as well?

Example:

let app =
    choose [
        route "/ping"   >>= text "pong"
        route "/"       >>= text "index"
        routeStartsWith "/api" >>=
            choose [
                routeEndsWith "" >>= setStatusCode 204 >>= text "No Content"
                routeEndsWith "/users" >>= text "users"
            ]
        route "/docs"   >>= text "docs"
    ]

This looks similar to the original example by @slogsdon and doesn't require to store the route in the items collection.

I think this API would also be clearer and less likely to be misused. For example this wouldn't be working, but it wouldn't be immediately clear why:

let app =
  choose [
    route "/ping" >>= text "pong"
    route "/" >>= text "index"
    rootPath "/api" >>=
      rootPath "/v1" >>=
        choose [
          subPath "" >>= setStatusCode 204 >>= text "No Content"
          subPath "/users" >>= text "users"
        ]
    route "/docs" >>= text "docs"
  ]

What are your thoughts?

from giraffe.

slogsdon avatar slogsdon commented on July 4, 2024

@dustinmoris The use case behind something like this is to help reduce duplication in and improve composition of parts of an application, specifically around route specifications. One example could be a theoretical resource handler for REST APIs that defines routes for a particular resource type without needing to know the entire route path for the resource. There are some other considerations that would need to be made here, so I don't have a

Frankly, the last example you posted with nested rootPaths would actually work as rootPath accumulates parts, so it would work as expected. 👍

TBH, one of my first thoughts was to use routeStartsWith and a potential routeEndsWith, but the possibility of routes matching the beginning and end portions with a variable middle portion. For example, with:

let app =
    choose [
        route "/ping"   >>= text "pong"
        route "/"       >>= text "index"
        routeStartsWith "/api" >>=
            choose [
                routeEndsWith "" >>= setStatusCode 204 >>= text "No Content"
                routeEndsWith "/users" >>= text "users"
            ]
        route "/docs"   >>= text "docs"
    ]

it could match the following request paths:

  • /api
  • /api/users
  • /api/other/users
  • /api/something/that/does-not-exist

Using routeStartsWith alone would prompt me to still create helper functions within an application to reduce duplication in specifying routes.

Along with @slang25's thoughts, an ideal solution would be to have a single function to handle the functionality of rootPath/subPath, but I would also add that it would be ideal to have the functionality exposed today through route, routef, routeCi, routeCif, etc. supported as well.

Are there any negatives with storing request state within HttpContext?

from giraffe.

dustinmoris avatar dustinmoris commented on July 4, 2024

Ok I see what you say and it makes sense.

Actually thanks for pointing out, I forgot to paste something in my example from before. The current rootPath function would never return None, which is an issue I think. Correct me if I a wrong, but with this example we would never get a matching request or a v2 route, because the rootPath item would have /api/v1/v2 at that point, right?

let app =
  choose [
    route "/ping" >>= text "pong"
    route "/" >>= text "index"
    rootPath "/api" >>=
      rootPath "/v1" >>=
        choose [
          subPath "" >>= setStatusCode 204 >>= text "No Content"
          subPath "/users" >>= text "users"
        ]
      rootPath "/v2" >>=
        choose [
          subPath "" >>= setStatusCode 204 >>= text "No Content"
          subPath "/users" >>= text "users"
        ]
    route "/docs" >>= text "docs"
  ]

I think the rootPath function should already validate the request if it matches the part in the root path and if not then skip a whole block of sub paths and proceed to the next. If you'd modify the rootPath function to do that, then I think you could even have it all in one function like this (treat this as untested pseudo code):

let subPath (part : string) =
    fun (ctx : HttpHandlerContext) ->
        let key     = "subPath"
        let current = ctx.HttpContext.Items.Item key |> string
        let path    = current + part
        if ctx.HttpContext.Request.Path.ToString().StartsWith path 
        then
            ctx.HttpContext.Items.Item key <- path
            Some ctx
        else None
        |> async.Return

from giraffe.

dustinmoris avatar dustinmoris commented on July 4, 2024

And you are right, if the above snippet works then this should/could probably be integrated into the existing route handlers, so you could simply do something like:

let app =
    choose [
        route "/ping"   >>= text "pong"
        route "/"       >>= text "index"
        route "/api" >>=
            choose [
                route "" >>= setStatusCode 204 >>= text "No Content"
                routeCi "/users" >>= text "users"
            ]
        route "/docs"   >>= text "docs"
    ]

Is that something you think you'd want to do?

from giraffe.

slogsdon avatar slogsdon commented on July 4, 2024

I just tested locally, and while a request to /api/v1/* will succeed, any request to /api/v2/* will fail. I hadn't yet gone through any formal testing of this, so edge cases would need to be covered and tested for.

I definitely agree that it would beneficial to add validation inside of the handler to reduce any unnecessary computation.

from giraffe.

dustinmoris avatar dustinmoris commented on July 4, 2024

Ok, try this:

let route (path : string) =
    fun (ctx : HttpHandlerContext) ->
        let key        = "route"
        let storedPath = ctx.HttpContext.Items.Item key |> string
        let path'      = storedPath + path
        if ctx.HttpContext.Request.Path.ToString().StartsWith path'
        then
            ctx.HttpContext.Items.Item key <- path'
            Some ctx
        else None
        |> async.Return

I think this should work and then we could apply the same logic to the other route handlers. It would be a non breaking change to route and would allow you to do what you need as well.

from giraffe.

slogsdon avatar slogsdon commented on July 4, 2024

I just ran that locally, and it seems to match routes prematurely. It responded with the handler for / when requesting /api/v1/users.

I can work with this locally to get something that works for route, both keeping existing functionality and the new feature.

from giraffe.

dustinmoris avatar dustinmoris commented on July 4, 2024

Oh yeah true I didn't think about that. Well ok, thanks for looking into this. I am not opposed to have one or two additional handlers for this functionality (e.g. subRoute and subRouteCi). Have a look and see what you think makes most sense. When you add/update tests and update the README file then you can also submit a pull request and I can merge it after a review.

from giraffe.

dustinmoris avatar dustinmoris commented on July 4, 2024

Bit late here in London now so will get back to any more questions tomorrow again! Thanks!

from giraffe.

slang25 avatar slang25 commented on July 4, 2024

After giving it some thought, I think the original suggestion doesn't really fit, and is better solved with composition.

You could in your own library add the following:

let bindWithPath (path : string, handler : HttpHandler) (handler2 : string -> HttpHandler) =
    fun (ctx : HttpHandlerContext) ->
        async {
            let! result = handler ctx
            match result with
            | None      -> return None
            | Some ctx2 ->
                match ctx2.HttpContext.Response.HasStarted with
                | true  -> return  Some ctx2
                | false -> return! handler2 path ctx2
        }

let (>>==) = bindWithPath // naming stuff is hard, even operators :-D

let routePrefix (path : string) =
    path,
    fun (ctx : HttpHandlerContext) ->
        if ctx.HttpContext.Request.Path.ToString().StartsWith path 
        then Some ctx
        else None
        |> async.Return

Now we have an additional bind operator that can flow through the path prefix like this:

let app =
    choose [
        route "/ping"   >>= text "pong"
        route "/"       >>= text "index"
        routePrefix "/api" >>== fun p ->
            choose [
                route p >>= setStatusCode 204 >>= text "No Content"
                route <| p + "/users" >>= text "users"
            ]
        route "/docs"   >>= text "docs"
    ]

This is a little ugly, and you could probably drop the new bind operator all together (roll it into the routePrefix function). I am against building up a path in HttpContext FWIW.

EDIT: I have thought some more (dangerous), I think using HttpContext is a necessary evil here, this pattern is used by plenty of web middleware architectures. The only thing I am uncomfortable with is the Items value not getting reset at the end of it's scope (like middleware would do), but rather being overwritten each time a "new scope" is needed. Also, if you have scope and keep adding to it, rather than having 2 and only 2 functions you could have 1 function you could nest as many times as you want.

from giraffe.

dustinmoris avatar dustinmoris commented on July 4, 2024

Ok I see what you're doing. So the subPath handler would add the path to the Items collection of a copy of the HttpHandlerContext and pass it on to the sub handlers, but if none of the sub handlers returns Some HttpHandlerContext then it will not propagate the modified HttpContext to other http handlers?

I like this idea, it's a cleaner design and I like the feature as well. TBH this feature will probably make routeStartsWith sort of redundant, but let's see.

from giraffe.

ErikSchierboom avatar ErikSchierboom commented on July 4, 2024

That's a very nice suggestion @slang25.

from giraffe.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.