Is it considered bad practice to use namespace-qualified keywords with nonexistent namespaces, for defining specs? I'd like to have entity maps defined in common domain namespace... so to avoid loosing data when merging specs, I've used convention :entity/attribute instead of ::entity-attribute for attributes and standard ::entity for entities. It aligns nicer to database tables and columns. Each entity in a separate namespace reminds me of Java classes, doesn't sound like a good idea.
(s/def :country/id ::nilable-nat-int)
(s/def :country/name ::non-empty-string)
(s/def ::country
(s/keys :req [:country/id
:country/name]))
;; ----------------------------------------
(s/def :location/id ::nilable-nat-int)
(s/def :location/name ::non-empty-string)
(s/def :location/zipcode ::nilable-non-empty-string)
(s/def ::location
(s/merge
(s/keys :req [:location/id
:location/name
:location/zipcode])
(s/or :country ::country
:country-id
(s/keys :req [:country/id]))))
As #glts commented, here is the right answer: mailing list.
I've decided to make keywords more specific, added this to the domain namespace:
(doseq [ns ["entity-1" ,,, "entity-n"]]
(->> (str "project.domain." ns)
(symbol)
(create-ns)
(alias (symbol ns))))
And then ::entity-n/attribute evaluates to :project.domain.entity-n/attribute.
Only one additional : is needed for the attributes from the question-example:
(s/def ::location/id ::nilable-nat-int)
Related
I'm learning Clojure, all by myself and I've been working on a simple toy project to create a Kakebo (japanese budgeting tool) for me to learn. First I will work on a CLI, then an API.
Since I'm just begining, I've been able to "grok" specs, which seems to be a great tool in clojure for validation. So, my questions are:
People test their own written specs?
I tested mine like the following code. Advice on get this better?
As I understand, there are ways to automatically test functions with generative testing, but for the bare bones specs, is this sort of test a good practice?
Specs file:
(ns kakebo.specs
(:require [clojure.spec.alpha :as s]))
(s/def ::entry-type #{:income :expense})
(s/def ::expense-type #{:fixed :basic :leisure :culture :extras})
(s/def ::income-type #{:salary :investment :reimbursement})
(s/def ::category-type (s/or ::expense-type ::income-type))
(s/def ::money (s/and double? #(> % 0.0)))
(s/def ::date (java.util.Date.))
(s/def ::item string?)
(s/def ::vendor (s/nilable string?))
(s/def ::entry (s/keys :req [::entry-type ::date ::item ::category-type ::vendor ::money]))
Tests file:
(ns kakebo.specs-test
(:require [midje.sweet :refer :all]
[clojure.spec.alpha :as s]
[kakebo.specs :refer :all]))
(facts "money"
(fact "bigger than zero"
(s/valid? :kakebo.specs/money 100.0) => true
(s/valid? :kakebo.specs/money -10.0) => false)
(fact "must be double"
(s/valid? :kakebo.specs/money "foo") => false
(s/valid? :kakebo.specs/money 1) => false))
(facts "entry types"
(fact "valid types"
(s/valid? :kakebo.specs/entry-type :income) => true
(s/valid? :kakebo.specs/entry-type :expense) => true
(s/valid? :kakebo.specs/entry-type :fixed) => false))
(facts "expense types"
(fact "valid types"
(s/valid? :kakebo.specs/expense-type :fixed) => true))
As a last last question, why can't I access the specs if I try the following import:
(ns specs-test
(:require [kakebo.specs :as ks]))
(fact "my-fact" (s/valid? :ks/money 100.0) => true)
I personally would not write tests at that are tightly coupled to the code whether I'm using spec or not. That's almost a test for each line of code - which can be hard to maintain.
There are a couple of what look to be mistakes in the specs:
;; this will not work, you probably meant to say the category type
;; is the union of the expense and income types
(s/def ::category-type (s/or ::expense-type ::income-type))
;; this will not work, you probably meant to check if that the value
;; is an instance of the Date class
(s/def ::date (java.util.Date.))
You can really get a lot out of spec by composing the atomic specs you have there into higher level specs that do the heavy lifting in your application. I would test these higher level specs, but often they may be behind regular functions and the specs may not be exposed at all.
For example, you've defined entry as a composition of other specs:
(s/def ::entry (s/keys :req [::entry-type ::date ::item ::category-type ::vendor ::money]))
This works for verifying all required data is present and for generating tests that use this data, but there are some transitive dependencies within the data such as :expense can not be of type :salary so we can add this to the entry spec:
;; decomplecting the entry types
(def income-entry? #{:income})
(def expense-entry? #{:expense})
(s/def ::entry-type (clojure.set/union expense-entry? income-entry?))
;; decomplecting the category types
(def expense-type? #{:fixed :basic :leisure :culture :extras})
(def income-type? #{:salary :investment :reimbursement})
(s/def ::category-type (clojure.set/union expense-type? income-type?))
(s/def ::money (s/and double? #(> % 0.0)))
(s/def ::date (partial instance? java.util.Date))
(s/def ::item string?)
(s/def ::vendor (s/nilable string?))
(s/def ::expense
(s/cat ::entry-type expense-entry?
::category-type expense-type?))
(s/def ::income
(s/cat ::entry-type income-entry?
::category-type income-type?))
(defn expense-or-income? [m]
(let [data (map m [::entry-type ::category-type])]
(or (s/valid? ::expense data)
(s/valid? ::income data))))
(s/def ::entry
(s/and
expense-or-income?
(s/keys :req [::entry-type ::date ::item
::category-type ::vendor ::money])))
Depending on the app or even the context you may have different specs that describe the same data. Above I combined expense and income into entry which may be good for output to a report or spreadsheet but in another area of the app you may want to keep them completely separate for data validation purposes; which is really where I use spec the most - at the boundaries of the system such as user input, database calls, etc.
Most of the tests I have for specs are in the area of validating data going into the application. The only time I test single specs is if they have business logic in them and not just data type information.
I need to validate the shape of clojure maps that have been converted from json strings. The json strings are messages of a protocol I'm implementing.
For this I'm trying out clojure.spec.alpha.
I'm using s/keys. Multiple messages in my
protocol have the same key names, but differently shaped values attached to those keys, so they cannot be validated by the same spec.
An example:
;; Here status should have the same key name, but their shape
;; is different. But definining another spec forces me to register it with a
;; different keyword, which breaks the "should have name 'status'" requirement.
(s/def ::a-message
(s/keys :req [::status]))
(s/def ::another-message
(s/keys :req [::status]))
I think I could define the :status spec in different namespaces, but it seems overkill to me.
After all it's just different messages in the same protocol and i just have a couple of clashes.
Is there a way for (s/keys) to separate the name of the key whose presence is being checked
from the name of the spec that is validating it?
In spec, qualified keywords are used to create global semantics (via the spec) whose name is the qualified keyword. If you use the same qualified keyword with different semantics, I'd say you should change your code to use different qualifiers :ex1/status and :ex2/status for different semantics.
If you are using unqualified keywords (not uncommon when coming from JSON), you can use s/keys and :req-un to map different specs to the same unqualified keyword in different parts of your data.
(s/def :ex1/status string?)
(s/def :ex2/status int?)
(s/def ::a-message (s/keys :req-un [:ex1/status]))
(s/def ::another-message (s/keys :req-un [:ex2/status]))
(s/valid? ::a-message {:status "abc"}) ;; true
(s/valid? ::another-message {:status 100}) ;; true
I want to create a clojure spec for a map that has rules about the presence of particular keys.
The map must have a :type and can have either :default or :value but not both. I tried:
(s/def ::propertyDef
(s/keys :req [::type (s/or ::default ::value) ] :opt [::description ::required]))
but I got
CompilerException java.lang.AssertionError: Assert failed:
spec/or expects k1 p1 k2 p2..., where ks are keywords
(c/and (even? (count key-pred-forms)) (every? keyword? keys)),
compiling:(C:\Users\MartinRoberts\AppData\Local\Temp\form-init4830956164341520551.clj:1:22)
but the or gave me an error as it is in the wrong format. I have to admit to not really understanding in the documentation for s/or.
First: you are using s/or to specify either a ::default or a ::value in your list of required keys. s/or requires :label spec pairs, and you are giving only the specs themselves, which is the cause of the error.
To solve, simply use or instead:
(s/def ::propertyDef (s/keys :req [::type (or ::default ::value)]
:opt [::description ::required]))
This allows both ::default and ::value to be present in the map, but this is almost always okay. The code which actually uses the map can simply check for the presence of ::value and use that, and if it's not there, then use ::default (or whatever your logic happens to be). This is usually done as such:
(let [myvalue (or (::value mymap) (::default mymap))] ...)
There could be thousands of keys in the map, and it would not affect your ability to extract the keys you need. This is why spec does not provide a built-in way to specify keys that should not be in the map, only ways to specify which keys should be present (namely, :req and :req-un in s/keys). Think of how most http servers work: you can give them nonsensical header keys and values, but they don't refuse to service the request; they just ignore them and return a response.
So, you likely don't need to enforce that only one or the other be present, but if you must, you can define an exclusive or function:
(defn xor
[p q]
(and (or p q)
(not (and p q))))
and then add this as an additional predicate on the spec:
(s/def ::propertyDef (s/and (s/keys :req [::type (or ::default ::value)]
:opt [::description ::required])
#(xor (::default %) (::value %))))
(s/valid? ::propertyDef {::type "type" ::default "default"})
=> true
(s/valid? ::propertyDef {::type "type" ::value "value"})
=> true
(s/valid? ::propertyDef {::type "type" ::default "default" ::value "value"})
=> false
I'm using Clojure to implement a (written) standards document. In general I'm pleased with the way Clojure allows me to write code that lines up with the different parts of the standard. With an eye on the future I am experimenting with writing a clojure.spec for it. In the document they define various structured data elements with named fields. However fields in different structures have the same name, for example the 'red' structure has a 'value' field which is a string, but the 'blue' structure has a 'value' field which is an integer. How can I handle this when it comes to writing specs?
(s/def ::value ???)
(s/def ::red (s/keys :req [::value ...]))
(s/def ::blue (s/keys :req [::value ...]))
The official advice, as I understand it, is that named keys should have the same semantics everywhere.
How should I approach this? I could call them 'red-value' and 'blue-value' but this makes the correspondence between the code and the standard less clear. Could I put every structure in its own namespace?
Your example is using the current namespace for all of your spec names, but you should leverage namespaces to disambiguate names.
(s/def ::red (s/keys :req [:red/value ...]))
(s/def ::blue (s/keys :req [:blue/value ...]))
You can use these specs with maps like:
(s/valid? ::red {:red/value "foo"})
(s/valid? ::blue {:blue/value 100})
Additionally, s/keys supports :req-un option to link named specs to unqualified attribute names, if that's what you have to work with.
(s/def ::red (s/keys :req-un [:red/value ...]))
(s/def ::blue (s/keys :req-un [:blue/value ...]))
You could validate with values like:
(s/valid? ::red {:value "foo"})
(s/valid? ::blue {:value 100})
After writing this answer, I was inspired to try to specify Clojure's destructuring language using spec:
(require '[clojure.spec :as s])
(s/def ::binding (s/or :sym ::sym :assoc ::assoc :seq ::seq))
(s/def ::sym (s/and simple-symbol? (complement #{'&})))
The sequential destructuring part is easy to spec with a regex (so I'm ignoring it here), but I got stuck at associative destructuring. The most basic case is a map from binding forms to key expressions:
(s/def ::mappings (s/map-of ::binding ::s/any :conform-keys true))
But Clojure provides several special keys as well:
(s/def ::as ::sym)
(s/def ::or ::mappings)
(s/def ::ident-vec (s/coll-of ident? :kind vector?))
(s/def ::keys ::ident-vec)
(s/def ::strs ::ident-vec)
(s/def ::syms ::ident-vec)
(s/def ::opts (s/keys :opt-un [::as ::or ::keys ::strs ::syms]))
How can I create an ::assoc spec for maps that could be created by merging together a map that conforms to ::mappings and a map that conforms to ::opts? I know that there's merge:
(s/def ::assoc (s/merge ::opts ::mappings))
But this doesn't work, because merge is basically an analogue of and. I'm looking for something that's analogous to or, but for maps.
You can spec hybrid maps using an s/merge of s/keys and s/every of the map as tuples. Here's a simpler example:
(s/def ::a keyword?)
(s/def ::b string?)
(s/def ::m
(s/merge (s/keys :opt-un [::a ::b])
(s/every (s/or :int (s/tuple int? int?)
:option (s/tuple keyword? any?))
:into {})))
(s/valid? ::m {1 2, 3 4, :a :foo, :b "abc"}) ;; true
This simpler formulation has several benefits over a conformer approach. Most importantly, it states the truth. Additionally, it should generate, conform, and unform without further effort.
You can use s/conformer as an intermediate step in s/and to transform your map to the form that’s easy to validate:
(s/def ::assoc
(s/and
map?
(s/conformer #(array-map
::mappings (dissoc % :as :or :keys :strs :syms)
::opts (select-keys % [:as :or :keys :strs :syms])))
(s/keys :opt [::mappings ::opts])))
That will get you from e.g.
{ key :key
:as name }
to
{ ::mappings { key :key }
::opts { :as name } }