keys*/keys with inlined value specs

267 views Asked by At

I want to write a spec with keys/keys* but being able to inline the value specs, which is not supported by design, and I get the reasoning behind it. However sometimes you do want (or simply have, by legacy or 3rd-party) coupling between keys and values when there is specific context to the map.

I'm still new to spec and this is just my first time integrating it with an existing project and it constantly gives me issues because it is assuming way too much, especially because of the reason mentioned above. E.g. imagine a map that describes a time period and has an until key for a date, and in the same ns there's a map for list processing and there's an until that takes a predicate function. I now need to mess with manually writing fully namespaced keys for namespaces that don't even exist (aliasing is cute but it would have to be duplicated constantly across multiple namespaces/files). Aside from being irritating I feel like it's also error-prone.

And another place where keys/keys* assumes too much is if I even want keywords as my keys. I'm writing a DSL for non-programmers but technical users right now, and bottom line is that I want to spec a map with symbols as keys. That doesn't seem to be supported whatsoever.

Is there something I'm not getting? or is spec really missing essential functionality?

2

There are 2 answers

2
Alex Miller On BEST ANSWER

You can spec a map with symbols as keys either with map-of:

(s/def ::sm (s/map-of symbol? any?))

or by spec'ing the map as a collection of entries:

(s/def ::sm (s/every (s/tuple symbol? any?) :kind map? :into {}))

The latter is particularly interesting as instead of a single tuple you can s/or many different kinds of tuples to describe more interesting maps. You can even connect those symbols to other existing specs in this manner:

(s/def ::attr1 int?)
(s/def ::attr2 boolean?)
(s/def ::sm (s/every (s/or :foo (s/tuple #{'foo} ::attr1)
                           :bar (s/tuple #{'bar} ::attr2))
              :kind map? :into {}))
(s/valid? ::sm {'foo 10 'bar true}) ;; => true
1
JR Heard On

I now need to mess with manually writing fully namespaced keys for namespaces that don't even exist

I've been using this approach as well, and I think I actually like it more than I like making sure that your keywords' namespaces always correspond to real Clojure NS forms. I use keywords like :business-domain-concept/a-name rather than :my-project.util.lists/a-name.

You can make keywords with arbitrary namespaces that don't map to any Clojure NS. For instance, in your until situation, you could define a :date/until spec that describes dates, and a (perhaps there's a better name for this one) :list/until spec that describes your list processing map's field.

It sounds like you're already aware of this arbitrary-keyword-namespace approach - in particular, I buy that it feels error-prone, since you're typing this stuff out by hand and spec doesn't appear to choke if you feed your s/keys a :fate/until by accident. FWIW, though, I think you're currently feeling the pain that namespaced keywords are specifically intended to solve: you're in one Clojure file, you've got two maps with keys named until, and they mean two completely different things.

I'm writing a DSL for non-programmers but technical users right now, and bottom line is that I want to spec a map with symbols as keys.

I think that map-of is what you want here:

user=> (s/def ::valid-symbols #{'foo 'bar 'baz})
:user/valid-symbols
user=> (s/def ::symbol-map (s/map-of ::valid-symbols int?))
:user/symbol-map
user=> (s/valid? ::symbol-map {'foo 1 'bar 3})
true
user=> (s/valid? ::symbol-map {'foo 1 'quux 3})
false