Transforming a dot notation string into an array of keywords

Hi guys!

TL;DR: I want to convert something like ["api.api-key" "api.base-url"] into [[:api :api-key] [:api :base-url]]

I know how to do this:

(def required-keys ["api.api-key" "api.base-url" "foo.bar.baz"])

(defn dot-str-to-vec-keywords [s]
  ; Splits a string with dot notation and transforms the result into a vector of keywords
  (vec (map keyword (clojure.string/split s #"\."))))

(into [] (map dot-str-to-vec-keywords required-keys)) ; [[:api :api-key] [:api :base-url] [:foo :bar :baz]]

Here is a link if you’d like to test online

The thing is: is there a more idiomatic way to do that?

How about:

(into []
  (comp
    (map #(clojure.string/split #”\.”)) ; yields a seq of pairs
    (map (juxt (comp keyword first)
                     (comp keyword second)))) ; converts entries to pairs of keywords
  required-keys)

Disclaimer: This is written without testing and entirely without a repl available, so it might need some massaging.

The idea is to use transducers to perform first the splitting and keywording in separate steps. Now that I look at it, there are probably prettier ways than using juxt here, but juxt makes it explicit that the expected output is a seq of pairs. Using map instead of juxt in the second step could be viable as well, but would no longer be explicit about the pair structure.

1 Like

Mostly, except I’d say in your case the use of into [] and wrapping map inside vec isn’t idiomatic, use mapv for both instead. Also, using functions fully qualified isn’t idiomatic, use require with :as instead. Lastly, a comment as doc-string isn’t idiomatic as well, use the doc-string feature of defn instead:

(require '[clojure.string :as str])

(defn dot-str-to-vec-keywords
  "Splits a string with dot notation and transforms the result into a vector of keywords"
  [s]
  (mapv keyword (str/split s #"\.")))

(mapv dot-str-to-vec-keywords required-keys)

And you could also try with threading, I wouldn’t say it is more or less idiomatic, just a different style:

(->> required-keys
 (mapv #(-> %
         (str/split #"\.")
         (->> (mapv keyword)))))
1 Like

Depending on your usage, you would consider using namespaced keyword instead of array of keywords:

(->> required-keys
     (map namespaced-keyword))
;; => (:api/api-key :api/base-url :foo.bar/baz)

With that, it may make your code more idiomatic further down the road. For example, group by namespace:

(->> required-keys
     (map namespaced-keyword)
     (group-by namespace))
;; => {"api" [:api/api-key :api/base-url], "foo.bar" [:foo.bar/baz]}

Or use it with destructruring:

(let [{:api/keys     [api-key base-url]
       :foo.bar/keys [baz]
       :or           {base-url "https://clojureverse.org"}} some-args]
  (when (valid-key api-key base-url)
    baz))

Note: namespace-keyword is a simple function to convert dotted notation to Clojure namespaced keyword:

(defn namespaced-keyword [s]
  (when-let [[_ n k] (re-matches #"(.*)\.((?:.(?!\.))+)$" s)]
    (keyword n k)))