Hi, Racket Discourse.
I have been tinkering with this idea for the past couple of weeks, because I realized I was abstracting the URLs of API endpoints at the wrong level in my HTTP requests library, which made it hard to patch features as they became necessary.
The new system allows one to extend URLs incrementally (via copying), which was not possible with the previous iteration.
I haven't gotten around to properly implementing the "file" aspects of URL parsing in the url
library, but for now the feature-set is complete enough to start using again.
There is quite a bit of room for optimization left on the table, in terms of the way values are converted to and from strings in the macro's internals, but that will hopefully become more elegant as time goes on.
The macro is called url-builder
and looks something like this:
(url-builder
#:http (as sky) (at www) (on 801)
#:path / cgi-bin / finger [xyz #false]
#:query [name "shriram"] [host "nw"]
#: top)
;=> (url "http" "sky" "www" 801 #t (list (path/param "cgi-bin" '()) (path/param "finger" '("xyz"))) '((name . "shriram") (host . "nw")) "top")
Using the example URL from the docs, we can see that in this macro:
- the scheme is indicated by the keyword,
#:http
, - the user,
sky
, is indicated by the(as ...)
syntax, - the host,
www
, is indicated by the(at ...)
syntax, - the port,
801
, is indicated by the(on ...)
syntax, - the path is reasonably close to a normal URL path, except that the parameters look like:
[name arg ...]
;; which is equivalent to "name=arg, ..."
- the query follows the same logic, except that it's parameters have no path-element component,
- the fragment is indicated by the empty keyword,
#:
.
One is free to use strings and identifiers as literal values in the syntax, such as sky
or www
, but unquoting and splicing values are also permitted:
(define subdomain 'www)
(define domain 'host)
(define tld 'com)
(define path* '(cgi-bin finger))
(define param* '(() ("xyz")))
(url-builder
#:http (as "sky") (at ,subdomain ,domain ,tld) (on 801)
#:path / ,path* ,param* ... ...
#:query [name "shriram"] [host "nw"]
#: top)
;=> (url "http" "sky" "www.host.com" 801 #t (list (path/param "cgi-bin" '()) (path/param "finger" '("xyz"))) '((name . "shriram") (host . "nw")) "top")
Furthermore, one may unpack the values of a URL:
(define a-url (string->url "http://sky@www:801/cgi-bin/finger;xyz?name=shriram;host=nw#top"))
(url-builder
#:scheme `(scheme ,a-url) (as `(user ,a-url)) (at `(host ,a-url)) (on `(port ,a-url))
#:path / `(path ,a-url) ...
#:query `(query ,a-url) ...
#: `(fragment ,a-url))
;=> (url "http" "sky" "www" 801 #t (list (path/param "cgi-bin" '()) (path/param "finger" '("xyz"))) '((name . "shriram") (host . "nw")) "top")
Attempting to use an invalid field is a syntax error:
(url-builder
#:scheme `(scheme ,a-url) (as `(user ,a-url)) (at `(host ,a-url)) (on `(port ,a-url))
#:path / `(path ,a-url) ...
#:query `(thing ,a-url) ...
#: `(fragment ,a-url))
;=>
url-builder: expected a foreign clone expression from `url-query'
parsing context:
while parsing a sequence of url query parameters, optionally ending on a query splice expression
while parsing an optional, keyword-delimited url query in: (quasiquote (thing (unquote a-url)))
In a case such as the above, where one is only copying from a single URL, a shorthand exists to use it as a "prototype":
(url-builder
#:use a-url
#:scheme `* (as `*) (at `*) (on `*)
#:path / `* ... #:query `* ... #: `*)
;=> (url "http" "sky" "www" 801 #t (list (path/param "cgi-bin" '()) (path/param "finger" '("xyz"))) '((name . "shriram") (host . "nw")) "top")
Further miscellanies include that:
- the scheme can be set via an unquote after the
#:scheme
keyword, - the host may contain multiple values, which are concatenated by ".",
- the host may specify an IPv6 value, as in
(at [2001:db8::7])
, - the path can be spliced in three different ways:
<elem> <param> ...
(one path element, multiple parameters)<elems> <params> ... ...
(multiple path elements, multiple parameters)<path> ...
(complete path/param list)
- non-obvious prototype path-copying includes:
`(elems ,x) `(params ,x) ... ...
;; as opposed to
`(path ,x) ...
- relative paths can be indicated by dropping the leading
/
:
(url-builder
#:https (at www)
#:path `(path ,a-url) ...)
;=> (url "https" #f "www" #f #f (list (path/param "cgi-bin" '()) (path/param "finger" '("xyz"))) '() #f)
- the query may end in a query-splice expression:
(url-builder
#:https (at www)
#:path #:query [param #false] `(query ,a-url) ...)
;=> (url "https" #f "www" #f #f '() '((param . #f) (name . "shriram") (host . "nw")) #f)
There is still some work to be done with the unquoting operations, to ensure that it does not fail silently (which is very confusing to say the least), but apart from that--and the inefficient conversions--it seems pretty solid.
Pros:
- the syntax is more "tangible" than using strings, and may catch some errors before runtime,
- the syntax allows one to freely interpolate strings, symbols and in certain cases numbers,
- the result is a (hopefully) valid
url
struct, which is used ubiquitously.
Cons:
- @-syntax exists, if interpolating strings is that big of a deal,
- the procedures are not very efficient at this point in time,
- the syntax is much more elaborate than a URL-string, although the jury is out on whether the added sophistication outweighs this cost,
- no exhaustive test-suite as of yet, which makes a lot of these statements speculation, at best.
So, what do you think about this syntax: Does it add anything to the problem of building URLs that you would consider to be a boon, or do you feel like this is much ado about nothing?
Have you come across similar macros for building URLs, or perhaps implemented some of your own?
P.S. I have uploaded the code to a gist, but I emphasize that it will probably not remain this way for very long.