Routing syntax

79 views
Skip to first unread message

Brian Adkins

unread,
Apr 4, 2016, 2:08:16 PM4/4/16
to Racket Users
I've been looking into an appropriate syntax for routing web requests. For each route, I'll need the following information:

* URL Pattern (required) e.g.
"/"
"/:org_shortname/product_image/:style/:filename"
"/:org_shortname/products/(*id).json"

* Function to handle request (required)

* Accepted HTTP verbs (optional, defaults ot all)

* Name (optional) - used to do "reverse routing" i.e. produce a URL from parameters

* Contraints (optional) - further refine the pattern matching e.g. type/format of params

Here's an example from Rails:

Foo::Application.routes.draw do
match "/:org_shortname/product_image/:style/:filename" => 'product_images#product_image',
:as => :product_image, :via => :get, :constraints => { :filename => /.jpg$/ }

# URL pattern: "/:org_shortname/product_image/:style/:filename"
# Function: ProductImagesController.product_image
# Name: :product_image
# Verbs: GET
# Contraints: filename param must end in .jpg

match '/products/(*id).json' => 'products#show', :via => :get

# URL pattern: '/products/(*id).json'
# (*id) globs everything between /products/ and .json including / chars
# Function: ProductsController.show
# Verbs: GET
end

There are many other aspects (much of which is just syntax sugar) of routing in Rails that I'm ignoring initially.

I'm thinking of something like the following:

(routes
("/" home-page)
("/:org_shortname/product_image/:style/:filename" product-image #:verbs (get)
#:name prod-image #:when (regexp-match #rx".jpg$" filename))
("/products/(*id).json" product-show #:verbs (get)))

The first two arguments (url-pattern function) are required, so they're positional. The optional arguments are specified with keywords.

Instead of: #:verbs (get) would it be better to have #:verbs get and dynamically check for an atom vs. list ?

My initial goal is just for the "base" syntax. Sugar can be added later for some cases.

Any feedback is appreciated.

Thanks,
Brian

Matt Jadud

unread,
Apr 4, 2016, 3:39:13 PM4/4/16
to Brian Adkins, Racket Users
Hi Brian,

This looks similar to what "dispatch" does?


I've never extended the bi-directional patterns that are available, but there is an extension mechanism built in, so that your #:when notions might be part of an extended/new url component matcher?

Cheers,
M




--
You received this message because you are subscribed to the Google Groups "Racket Users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to racket-users...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

antoine

unread,
Apr 4, 2016, 3:44:24 PM4/4/16
to racket...@googlegroups.com
Hello,

I would go for 'methods instead of 'verbs, a HTTP vocabulary instead of
a REST one.

For the pathinfo:

When using a string you use :pattern as place holder as far as i know
there is no such convention in racket, maybe you should explore ~pattern
(like format), #:pattern, 'pattern, #'pattern (like macro templating) or
at-expr?

For constraints on placeholders you may want to limit them to regexp
because:

1. 99.999% of the time they will satisfy the developer needs.
2. You give you room to make your router fast implementing a minimal
final state machine (a regexp).

example:

("/~org-shortname/product_image/~style/~filename"
#:method POST
#:constraints ([filename ".*\\.xml$"])
;; a guard that break optimisations and expand
#:when (not (equal? "abc" org-shortname))
#:controller my-controller)


Or maybe use a list like dispatch-rule

((org-shortname "product_image" style filename)
#:method POST
#:constraints ([filename ".*\\.xml$"])
;; a guard that break optimisations and expand
#:when (not (equal? "abc" org-shortname))
#:controller my-controller)

Brian Adkins

unread,
Apr 4, 2016, 3:51:07 PM4/4/16
to Racket Users, lojic...@gmail.com, ma...@jadud.com
On Monday, April 4, 2016 at 3:39:13 PM UTC-4, Matt Jadud wrote:
> Hi Brian,
>
>
> This looks similar to what "dispatch" does?
>
>
> https://docs.racket-lang.org/web-server/dispatch.html
>
>
>
> I've never extended the bi-directional patterns that are available, but there is an extension mechanism built in, so that your #:when notions might be part of an extended/new url component matcher?
>
>
> Cheers,
> M

They both have a similar role, but I think there are significant differences (unless I'm misunderstanding):

* It's not uncommon to rearrange the components of a URL pattern. Without name binding, you would need to change both the pattern and the handler function.

* Typing is optional for mine

* I didn't see a way to define further constraints (besides the type of an argument) i.e. ensuring an argument matches a pattern

I thought about not providing the #:name parameter, and that might be possible. Rails provides name_url() and name)_path() functions for absolute and relative url creation - I'll have to weigh the pros/cons of using the name of the handler function as the prefix for those.

Brian Adkins

unread,
Apr 4, 2016, 4:33:14 PM4/4/16
to antoine, racket...@googlegroups.com
> On Apr 4, 2016, at 3:44 PM, antoine <antoin...@sfr.fr> wrote:
>
> Hello,
>
> I would go for 'methods instead of 'verbs, a HTTP vocabulary instead of a REST one.

Fair point. I suppose a couple additional characters won't kill me.

> For the pathinfo:
>
> When using a string you use :pattern as place holder as far as i know there is no such convention in racket, maybe you should explore ~pattern (like format), #:pattern, 'pattern, #'pattern (like macro templating) or at-expr?

I'm open to other ideas, but :variable is used in a number of routing string patterns in other languages/frameworks and seems fairly standard.

> For constraints on placeholders you may want to limit them to regexp because:
>
> 1. 99.999% of the time they will satisfy the developer needs.
> 2. You give you room to make your router fast implementing a minimal final state machine (a regexp).

I'm not sure I understand how *limiting* the constraint to be a regex is faster than *allowing* the constraint to be a regex i.e. in either case, the URL needs to be parsed and then a function called to compare a variable to a regex pattern. Am I misunderstanding?

I probably didn't communicate very well with my example, but when I first started thinking about constraints, I assumed I'd follow the Rails idea of an elaborate construct to declare all the constraints, but then I thought that using a single lambda to indicate whether the route matched might be better. I figured syntax sugar can be added later for common cases. For example:

("/:org_shortname/product_image/:style/:filename" product-image
#:methods get
#:when verify-filename)

(define (verify-filename params)
(regexp-match #rx".jpg$" (hash-ref params 'filename)))

or

("/:org_shortname/product_image/:style/:filename" product-image
#:methods get
#:when verify-filename-and-style)

(define (verify-filename-and-style params)
(and
(regexp-match #rx".jpg$" (hash-ref params 'filename))
(exact-integer? (hash-ref params 'style))))

> example:
>
> ("/~org-shortname/product_image/~style/~filename"
> #:method POST
> #:constraints ([filename ".*\\.xml$"])
> ;; a guard that break optimisations and expand
> #:when (not (equal? "abc" org-shortname))
> #:controller my-controller)

I'm not sure what you're suggesting here. Is your idea to provide both #:constraints and #:when ?

> Or maybe use a list like dispatch-rule
>
> ((org-shortname "product_image" style filename)
> #:method POST
> #:constraints ([filename ".*\\.xml$"])
> ;; a guard that break optimisations and expand
> #:when (not (equal? "abc" org-shortname))
> #:controller my-controller)

The list style might disallow something like:

"/product/item-:id" e.g. /product/item-473

or

"/products/(*id).json" e.g. /products/foo/bar/baz.json => id = 'foo/bar/baz'
> --
> You received this message because you are subscribed to a topic in the Google Groups "Racket Users" group.
> To unsubscribe from this topic, visit https://groups.google.com/d/topic/racket-users/wZ07AM8SBuw/unsubscribe.
> To unsubscribe from this group and all its topics, send an email to racket-users...@googlegroups.com.

David Vanderson

unread,
Apr 4, 2016, 8:15:33 PM4/4/16
to racket...@googlegroups.com
On 04/04/2016 02:08 PM, Brian Adkins wrote:
> (routes
> ("/" home-page)
> ("/:org_shortname/product_image/:style/:filename" product-image #:verbs (get)
> #:name prod-image #:when (regexp-match #rx".jpg$" filename))
> ("/products/(*id).json" product-show #:verbs (get)))
>
> Any feedback is appreciated.
>
Good start! One thing I liked about dispatch-rules is that it breaks
the url parts up, so you don't have to parse the string. If you could
name them, then the part names could be racket identifiers in scope, so
you don't have misspelling issues in the #:when function. I'm imagining
that the route is essentially a match clause against a url split by
'/'. Not sure if I'm making sense, but something like:

(routes
(route ("")
home-page)
(route (org_shortname "product_image" style filename)
product-image
#:name prod-image #:methods (get put)
#:when (regexp-match #rx".jpg$" filename)))

Maybe there's a way to tag url parts with predicates, sort of like
dispatch-rules or syntax-parse, but it might obscure the url too much:

(route ((org_shortname string?)
"product_image"
(style number?)
(filename (lambda () (regexp-match #rx".jpg$" filename))))
#:name prod-image #:methods (get put))

Hope this helps,
Dave

Brian Adkins

unread,
Apr 4, 2016, 9:02:56 PM4/4/16
to David Vanderson, racket...@googlegroups.com
On Apr 4, 2016, at 8:15 PM, David Vanderson <david.v...@gmail.com> wrote:
>
> On 04/04/2016 02:08 PM, Brian Adkins wrote:
>> (routes
>> ("/" home-page)
>> ("/:org_shortname/product_image/:style/:filename" product-image #:verbs (get)
>> #:name prod-image #:when (regexp-match #rx".jpg$" filename))
>> ("/products/(*id).json" product-show #:verbs (get)))
>>
>> Any feedback is appreciated.
>>
> Good start!

Thanks :)

> One thing I liked about dispatch-rules is that it breaks the url parts up, so you don't have to parse the string.

My goal for the syntax is to make it as easy on the developer (i.e. user of the framework) as possible vs. being easy on me as the framework author. I expect the string can be parsed at compile time via a macro. I have to admit that in ten years of Rails development, I don't recall offhand ever having a parameter that wasn't an entire "node" of the URL, so my previous example of "/product/item-:id" may be a red herring. On the other hand, I don't see a reason to make that impossible. I do think I've used route globbing w/in a URL node, so I'd like to keep the string representation.

> If you could name them, then the part names could be racket identifiers in scope, so you don't have misspelling issues in the #:when function.

That should be able to be checked at compile time via a macro, right?

> I'm imagining that the route is essentially a match clause against a url split by '/'. Not sure if I'm making sense, but something like:

The url will split on '/', but nodes may be combined via globbing or a node may have a substring that's a parameter. For example:

"/products/(*id).json" e.g. /products/foo/bar/baz.json => id = 'foo/bar/baz'

or "/product/item-:id" as mentioned above where /product/item-7 would result in id = '7'

>
> (routes
> (route ("")
> home-page)
> (route (org_shortname "product_image" style filename)
> product-image
> #:name prod-image #:methods (get put)
> #:when (regexp-match #rx".jpg$" filename)))
>
> Maybe there's a way to tag url parts with predicates, sort of like dispatch-rules or syntax-parse, but it might obscure the url too much:
>
> (route ((org_shortname string?)
> "product_image"
> (style number?)
> (filename (lambda () (regexp-match #rx".jpg$" filename))))
> #:name prod-image #:methods (get put))
>

I can think of a number of ways to make this much easier on myself, but as I mentioned, I'm trying to maximize ease for the framework user. Now, if I encounter serious roadblocks while implementing this, I may need to make concessions :)

> Hope this helps,
> Dave

antoine

unread,
Apr 5, 2016, 6:51:00 AM4/5/16
to racket...@googlegroups.com
> I'm open to other ideas, but :variable is used in a number of routing string patterns in other languages/frameworks and seems fairly standard.

My point was being standard with other racket work not other web framework.

> I'm not sure I understand how *limiting* the constraint to be a regex is faster than *allowing* the constraint to be a regex i.e. in either case, the URL needs to be parsed and then a function called > to compare a variable to a regex pattern. Am I misunderstanding?
The point is to make the routing use something like https://swtch.com/~rsc/regexp/regexp1.html to match a route, and thus can be done only if placeholder are regexp and not an arbitrary racket expression. (That is where the #:when comes if you can't express you constraints with regexp you could use a guard).

Using a list for expressing the pathinfo is the less limiting you can express "/product/item-:id" as ("product" ["item-" id]).
Antoher point you can use a symbol instead of a string removing the two ".


> That should be able to be checked at compile time via a macro, right?

It want something like:

"/some/route/:param"
#:when (equal? "abc" param)

instead of #:when (equal? "abc" (hash-ref some-predefine-binding "param"))

so it will exepand to something like:

(let ([param (hash-ref parameters "param")])
(equal? "abc" param))

Brian Adkins

unread,
Apr 5, 2016, 7:58:14 AM4/5/16
to antoine, racket...@googlegroups.com

> On Apr 5, 2016, at 6:51 AM, antoine <antoin...@sfr.fr> wrote:
>
> > I'm open to other ideas, but :variable is used in a number of routing string patterns in other languages/frameworks and seems fairly standard.
>
> My point was being standard with other racket work not other web framework.

I'd like to stick to at most one prefix character, but ~ is a possibility.

> > I'm not sure I understand how
> *limiting* the constraint to be a regex is faster than *allowing*
> the constraint to be a regex i.e. in either case, the URL needs to be parsed and then a function called > to compare a variable to a regex pattern. Am I misunderstanding?
>
> The point is to make the routing use something like https://swtch.com/~rsc/regexp/regexp1.html to match a route, and thus can be done only if placeholder are regexp and not an arbitrary racket expression. (That is where the #:when comes if you can't express you constraints with regexp you could use a guard).

I see. I suspect that constraint checking is nowhere near the critical path, but I'll do some performance testing once I have some code implemented. One option would be to start with the more general guard, and if warranted, I could add a more specific constraint that's required to be a regex.

> Using a list for expressing the pathinfo is the less limiting you can express "/product/item-:id" as ("product" ["item-" id]).
> Antoher point you can use a symbol instead of a string removing the two ".

It's something to keep in mind, but I prefer typing "/product/item-:id" and it seems more readable as I'm scanning a long list of routes since it more closely matches the actual URL.

> > That should be able to be checked at compile time via a macro, right?
>
> It want something like:
>
> "/some/route/:param"
> #:when (equal? "abc" param)
>
> instead of #:when (equal? "abc" (hash-ref some-predefine-binding "param"))
>
> so it will exepand to something like:
>
> (let ([param (hash-ref parameters "param")])
> (equal? "abc" param))

I'll keep that in mind as I continue. I like being able to see the guard expression directly in the route as in the above, and I suspect the guard will always be limited to an expression referencing the parameters, so passing a more general lambda may not buy me much in terms of flexibility.

Thanks for the feedback!

Reply all
Reply to author
Forward
0 new messages