Fork me on GitHub
#lambdaisland
<
2020-04-01
>
plexus12:04:52

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

plexus12:04:45

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

plexus12:04:29

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

plexus12:04:49

(query-map "")
;; => {:foo "bar", :aaa "bbb"}

(query-map "" {:keywordize? false})
;; => {"foo" "bar", "aaa" "bbb"}

(assoc-query ""
             :foo "baq"
             :hello "world")
;;=> #lambdaisland/uri ""

plexus12:04:30

There's also a version of assoc-query that takes a map instead of key-value pairs

plexus12:04:56

(assoc-query* ""
              {:foo "baq"
               :hello "world"})
;;=> #lambdaisland/uri ""

plexus12:04:20

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)

plexus12:04:54

(query-map "?id=1&id=2")
;; => {:id ["1" "2"]}

plexus12:04:28

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

plexus12:04:06

So you can actually pick three strategies, :never, :always, :duplicates

plexus12:04:12

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"]}

plexus12:04:51

Similarly assoc-query is able to round-trip this

plexus12:04:34

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"

plexus12:04:59

Another design decision in assoc-query is that nil values are ignored. This way you can use nil as a kind of dissoc

plexus12:04:37

(assoc-query "?id=1&name=jack" :name nil)
;;=> #lambdaisland/uri "?id=1"

plexus12:04:51

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"}}

plexus12:04:26

in this style it is explicit if something should be a collection of values by using ?id[]=1&id[]=2

Ben Sless13:04:10

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.

dominicm14:04:34

I like the design. You ticked the things I think about in context.

plexus14:04:21

@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!

👍 4