With clojure.spec, is there a way to define a more “human-readable” spec for nested maps? The following doesn't read very well:
(s/def ::my-domain-entity (s/keys :req-un [:a :b])
(s/def :a (s/keys :req-un [:c :d]))
(s/def :b boolean?)
(s/def :c number?)
(s/def :d string?)
Given that the shape of a conforming entity is something like
{:a {:c 1 :d "hello"} :b false}
My complaint is that it becomes hard(er) to read a spec if it has any sort of nested maps or any deep structure… because you are chasing keys up and down a file and they aren’t “in place” declarations.
To compare, something like schema allows a more readable nested syntax that closely mirrors the actual data shape:
(m/defschema my-domain-entity {:a {:c sc/number :d sc/string} :b sc/bool})
Can this be done in clojure.spec?
One of spec's value propositions is that it does not attempt to define an actual schema. It does not bind the definition of an entity to the definition of its components. To quote from the spec rationale:
Most systems for specifying structures conflate the specification of the key set (e.g. of keys in a map, fields in an object) with the specification of the values designated by those keys. I.e. in such approaches the schema for a map might say :a-key’s type is x-type and :b-key’s type is y-type. This is a major source of rigidity and redundancy.
In Clojure we gain power by dynamically composing, merging and building up maps. We routinely deal with optional and partial data, data produced by unreliable external sources, dynamic queries etc. These maps represent various sets, subsets, intersections and unions of the same keys, and in general ought to have the same semantic for the same key wherever it is used. Defining specifications of every subset/union/intersection, and then redundantly stating the semantic of each key is both an antipattern and unworkable in the most dynamic cases.
So to answer the question directly, no, spec does not provide this type of specification, as it was designed specifically that way. You trade off some level of human readability that you would have in a schema-like definition, for a more dynamic, composable, flexible specification.
Though it was not in your question, consider the benefit of using a system that decouples the definition of an entity from the definition of its components. It's contrived, but consider defining a car (keeping it simple to save space here, just using tires and chassis):
(s/def ::car (s/keys :req [::tires ::chassis]))
We define it once, and we can put any configuration of tires we want on it:
(s/def ::tires (s/coll-of ::tire :count 4))
(s/def ::tire (s/or :goodyear ::goodyear}
:michelin ::michelin))
(s/def ::goodyear #{"all-season" "sport" "value"})
(s/def ::michelin #{"smooth ride" "sport performance"})
(s/def ::chassis #{"family sedan" "sports"})
The following are different configurations, but all are valid cars:
(s/valid? ::car {::tires ["sport" "sport" "sport" "sport"]
::chassis "sports"})
(s/valid? ::car {::tires ["smooth ride" "smooth ride"
"smooth ride" "smooth ride"]
::chassis "family sedan"})
It's contrived, but clear to see that there is flexibility in defining the components as separate from what the components come together to form. Tires have their own specifications, and their specification is not what defines a car, even though they are components of the car. It's more verbose, but much more flexible.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With