Comments (15)
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.
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.
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.
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.
@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 rootPath
s 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.
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.
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.
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.
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.
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.
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.
Bit late here in London now so will get back to any more questions tomorrow again! Thanks!
from giraffe.
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.
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.
That's a very nice suggestion @slang25.
from giraffe.
Related Issues (20)
- Migrate to System.Text.Json HOT 1
- Participate on hacktoberfest 2023? HOT 1
- Never decalre reader with `use` on `ctx.Request.Body` HOT 4
- Upgrade to .NET 8 HOT 6
- LinkGenerator doesn't work with routef HOT 1
- Giraffe 6.2 is breaks against Microsoft.IO.RecyclableMemoryStream 3.0.0 HOT 11
- Returning streams, either with WriteStreamAsync or WriteFileStreamAsync or their handler equivalents, is extremely slow HOT 4
- Question: How to approach outside in testing of a micro service? HOT 2
- Guid pattern in endpoint router matches invalid values, throws System.FormatException HOT 6
- Follow-up from "Minor code optimisation #567"
- Update SECURITY.md
- Start using fantomas to validate code submissions with CI HOT 6
- [README] Suggestion for "Getting Started"'s "Doing it manually" HOT 2
- [question] Would it be possible to make `Giraffe.EndpointRouting` case-sensitive? HOT 3
- Remove NuGet API key from the repository HOT 3
- 6.4.0 release references PR for updating to .NET 7 instead of .NET 8 HOT 1
- EndpointRouting - Create endpoint for multiple http verbs
- Request/discussion: WriteStreamAsync buffer size autotuning and/or increase default buffer size HOT 1
- Update `.vscode` debug configuration to point to existent projects
- Returning 406 when mustAccept fails HOT 8
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from giraffe.