I added functions to lambdaisland/uri for dealing with query strings, would love feedback on this. I'm holding off on an official release so we're still able to change things. https://github.com/lambdaisland/uri/blob/master/src/lambdaisland/uri.cljc#L124-L234
lambdaisland/uri never had much of an opinion on query strings because the RFC doesn't have much of an opinion on them. It's just a string, what you do with it is up to you. The typical ?foo=bar&aaa=bbb
key value format is just a common convention
but it's become so common that it makes sense to have first class support for it. There are some lower level functions now for parsing this stuff, but the main entry points for most use cases should be query-map
and assoc-query
(query-map "<http://example.com?foo=bar&aaa=bbb>")
;; => {:foo "bar", :aaa "bbb"}
(query-map "<http://example.com?foo=bar&aaa=bbb>" {:keywordize? false})
;; => {"foo" "bar", "aaa" "bbb"}
(assoc-query "<http://example.com?foo=bar&aaa=bbb>"
:foo "baq"
:hello "world")
;;=> #lambdaisland/uri "<http://example.com?foo=baq&aaa=bbb&hello=world>"
There's also a version of assoc-query
that takes a map instead of key-value pairs
(assoc-query* "<http://example.com?foo=bar&aaa=bbb>"
{:foo "baq"
:hello "world"})
;;=> #lambdaisland/uri "<http://example.com?foo=baq&aaa=bbb&hello=world>"
One question of potential contention is how to deal with something like ?id=1&id=2
, for instance ring-code will automatically turn these into a vector of values, which is the default for query-map
as well (ring-code: https://github.com/ring-clojure/ring-codec/blob/b01fcee9ffe35da85eeeb555ebecb24414d7d9a6/src/ring/util/codec.clj#L9)
(query-map "?id=1&id=2")
;; => {:id ["1" "2"]}
This kind of makes sense, but it is actually a pain for consumers, because now you need to check every time if what you're getting back is a collection or a scalar
So you can actually pick three strategies, :never
, :always
, :duplicates
so at least you get consistent results (:duplicates is the default)
(query-map "?id=1&id=2" {:multikeys :never})
;; => {:id "2"}
(query-map "?foo=bar&id=2" {:multikeys :always})
;; => {:foo ["bar"], :id ["2"]}
Similarly assoc-query
is able to round-trip this
it will check if something is a coll?
, and if so split it out into multiple entries
(assoc-query* "" (query-map "?id=1&id=2"))
;;=> #lambdaisland/uri "?id=1&id=2"
Another design decision in assoc-query
is that nil
values are ignored. This way you can use nil
as a kind of dissoc
(assoc-query "?id=1&name=jack" :name nil)
;;=> #lambdaisland/uri "?id=1"
I think that's all fairly elegant as API, what is completely missing is support for a convention that is quite common in some places e.g. the rails world, of using []
as indexes, so e.g. ?user[name]=jack
would become {:user {:name "jack"}}
in this style it is explicit if something should be a collection of values by using ?id[]=1&id[]=2
I got 2c to throw in regarding performance since I had to play with optimizing uri generation for some use case: You're using several intermediate collections and a ton of StringBuilders (every call to string) It's way better to have one string builder and use a reducing function into it with a transducer. It's even faster if you recycle the string builder between calls. What I did was instance a thread-local SB and clear it after getting the string out of it. It also reduces GC pressure.
I like the design. You ticked the things I think about in context.
@ben.sless I agree there's room for optimization, but that can happen without breaking the API. I'd like to focus on the API design right now, since for most people this is not their bottleneck, but PRs with performance improvements are of course very welcome!