Macro containing a list of elements that each have optional keyword options

18 views
Skip to first unread message

Brian Adkins

unread,
Aug 19, 2019, 3:35:00 PM8/19/19
to Racket Users
I'm working on a macro to allow the following:

(routes
 ("/foo" foo-handler #:method put)
 ("/bar" bar-handler #:methods (put update))
 ("/baz" baz-handler))

The idea is that you could restrict a route based on the HTTP method either for one method using #:method or for a list of methods using #:methods. I tried using some ideas from the following examples:



The attempt below does not work, and from my debugging I think it's probably the wrong approach. If there are examples of this sort of thing, I'd love to see them.

Thanks,
Brian Adkins

(define-syntax (routes stx)
  (syntax-parse stx
    [ (routes (route:string
               handler:id
               (~alt (~optional (~or* (~seq #:methods (methods:expr ...))
                                      (~seq #:method method:expr))
                                #:name "#:method, or #:methods option")
                     (~optional (~seq #:when guard:expr)
                                #:name "#:when option"))
               ...)
              ...)
      (with-syntax ([ name  (format-id stx "axio-routes") ])
        #'(define name (list (list route handler 'method (list 'methods ...)) ...)))]))

Brian Adkins

unread,
Aug 19, 2019, 4:43:24 PM8/19/19
to Racket Users
The following got me over the main hurdle:

(define-syntax (routes stx)
  (syntax-parse stx
    [ (routes (element ...) ...)
      (with-syntax ([ name  (format-id stx "axio-routes") ])
        #'(define name (list (route-element element ...) ...)))]))

(define-syntax (route-element stx)
  (syntax-parse stx
    [ (route-element route:string
                     handler:id
                     (~alt (~optional (~or* (~seq #:methods (methods:expr ...))
                                            (~seq #:method method:expr))
                                      #:name "#:method, or #:methods option")
                           (~optional (~seq #:when guard:expr)
                                      #:name "#:when option"))
                     ...)
      (with-syntax ([ m  (or (attribute method)  #'#f) ]
                    [ ms (or (attribute methods) #'#f) ])
        #'(list route handler 'm 'ms))]))

(routes
 ("/foo" foo-handler #:method put)
 ("/bar" bar-handler #:methods (put update))
 ("/baz" baz-handler))

axio-routes

==> '(("/foo" #<procedure:foo-handler> put #f) ("/bar" #<procedure:bar-handler> #f (put update)) ("/baz" #<procedure:baz-handler> #f #f))

Now I just need to normalize to a list of HTTP methods vs. having separate fields. 

Brian Adkins

unread,
Aug 19, 2019, 4:56:25 PM8/19/19
to Racket Users
(define-syntax (routes stx)
  (syntax-parse stx
    [ (routes (element ...) ...)
      (with-syntax ([ name  (format-id stx "axio-routes") ])
        #'(define name (list (route-element element ...) ...)))]))

(define-syntax (route-element stx)
  (syntax-parse stx
    [ (route-element route:string
                     handler:id
                     (~alt (~optional (~or* (~seq #:methods (methods:expr ...))
                                            (~seq #:method method:expr))
                                      #:name "#:method, or #:methods option")
                           (~optional (~seq #:when guard:expr)
                                      #:name "#:when option"))
                     ...)
      (with-syntax ([ method-list (let ([ m  (attribute method)  ]
                                        [ ms (attribute methods) ])
                                    (cond [ m    (list m) ]
                                          [ ms   ms       ]
                                          [ else '()      ]))]
                    [ guard-func (let ([ fun (attribute guard) ])
                                   (if fun
                                       fun
                                       #f)) ])
        #'(list route handler 'method-list guard-func))]))

(routes
 ("/foo" foo-handler #:method put)
 ("/bar" bar-handler #:methods (put update) #:when use-bar-handler?)
 ("/baz" baz-handler))

axio-routes

==>  '(("/foo" #<procedure:foo-handler> (put) #f) ("/bar" #<procedure:bar-handler> (put update) #<procedure:use-bar-handler?>) ("/baz" #<procedure:baz-handler> () #f))

I know it ain't pretty yet, but it works now, so I can enhance it later. I should probably use a struct for each route vs. a list.

Sorawee Porncharoenwase

unread,
Aug 19, 2019, 5:48:29 PM8/19/19
to Brian Adkins, Racket Users

You could use (splicing) syntax class to help with normalization:

#lang racket

(require syntax/parse/define
         (for-syntax syntax/parse
                     racket/syntax))

(begin-for-syntax
  (define-splicing-syntax-class methods-cls
    #:attributes (methods)
    (pattern (~seq #:methods (method:expr ...))
             #:with methods #'(list 'method ...))
    (pattern (~seq #:method method:expr)
             #:with methods #'(list 'method)))

  (define-syntax-class route-cls
    (pattern (route:string
              handler:id
              (~alt (~optional ms:methods-cls
                               #:name "#:method, or #:methods option")
                    (~optional (~seq #:when guard-e:expr)
                               #:name "#:when option")) ...)
             #:with methods #`#,(or (attribute ms.methods) #'(list))
             #:with guard #`#,(attribute guard-e))))

(define-simple-macro (routes :route-cls ...)
  #:with name (format-id this-syntax "axio-routes")
  (define name (list (list route handler methods guard) ...)))

(define (foo-handler) 0)
(define (bar-handler) 0)
(define (baz-handler) 0)
(define (use-bar-handler?) 0)

(routes
 ("/foo" foo-handler #:method put)
 ("/bar" bar-handler #:methods (put update) #:when use-bar-handler?)
 ("/baz" baz-handler))

axio-routes

--
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.
To view this discussion on the web visit https://groups.google.com/d/msgid/racket-users/ffc7ec25-f663-4d1d-a3d6-d3097d9c3963%40googlegroups.com.

Brian Adkins

unread,
Aug 19, 2019, 7:40:09 PM8/19/19
to Racket Users
Thanks your idea cleans it up nicely!  I've broken up the code into 4 file and placed them in a gist:


I'm not thrilled with the fact that the routes macro has to export the axio-routes binding. I expect there is a better way to communicate between the routes macro and find-route function.

I'll add interpolation to the URL paths next e.g.  (route "/user/~id" ...) 
Reply all
Reply to author
Forward
0 new messages