Fork me on GitHub
Andrew Byala04:11:56

Hi, everyone. I have a beginner question about Spec's namespacing as it pertains to maps. I'd like to create two definitions within the namespace, such that the following passes:

(s/conform ::account     {:account-id "abc123",   :amount 50})
(s/conform ::transaction {:from-account "abc123", :to-account "def456", :amount 25})
Note that both definitions reference the same non-namespaced key of amount, but I'd like to define the amount of an account to be an int between -100 and 100, and the amount of a transaction to be any positive int. Furthermore, I'd like to use the same spec for the account-id on an account, as well as both the from-account and to-account keys in a transaction. I can't see any way within a map to say that the value of a particular key corresponds to a specification by name. I'm trying to find a way to do something like this, but can't see how:
; This is not working code... I'm trying to find a way to get here.
(s/def ::transaction-amount pos-int?)
(s/def ::balance-amount #(<= -100 % 100))
(s/def ::account-id string?)

; Both :from-account and :to-account should be mapped to the spec for ::account-id
(s/def ::transaction (s/keys :req-un [[:from-account :as ::account-id]
                                      [:to-account :as ::account-id]
                                      [:amount :as ::transaction-amount]]))
; The :amount here is a ::balance-amount, but the :amount for a transaction is a ::transaction-amount
(s/def ::account (s/keys :req-un [::account-id
                                  [:amount :as ::balance-amount]]))


(s/def :account/amount  ...)
(s/def :transaction/amount ...)
Then you can use those qualified names in the :req-un part and it will match :account in both cases, with different semantics.


Although a lot of examples use :: that's just shorthand for a qualified name that "happens to match" the current namespace -- but qualified keywords should really really reflect their domain semantics -- they don't need to match code namespaces.


With the account IDs, you can define a common ::account-id and then just define the other names as aliases:

(s/def ::account-id ..)
(s/def ::from-account ::account-id)
(s/def ::to-account ::account-id)
And, again, as above you can use qualified names that match the domain semantics rather than :: for the code namespace.

Andrew Byala04:11:38

Nifty, @seancorfield. This seems to work for everything I'm looking for:

; This is reused for both transactions and accounts.
(s/def ::account-id string?)

(s/def :transaction/from-account ::account-id)
(s/def :transaction/to-account ::account-id)
(s/def :transaction/amount pos-int?)
(s/def ::transaction (s/keys :req-un [:transaction/from-account :transaction/to-account :transaction/amount]))

(s/def :account/account-id ::account-id)
(s/def :account/amount #(<= -100 % 100))
(s/def ::account (s/keys :req-un [:account/account-id :account/amount]))

Andrew Byala04:11:45

Is that what you were suggesting?


That looks great, yes!


And now you can see how the qualified keys provide separate semantic "namespaces" rather than code namespaces.

Andrew Byala04:11:49

Yep, I see that. The piece I was missing was how :req-un would strip that semantic namespace back off again, letting the actual key be whatever I wanted, Super helpful!

Andrew Byala04:11:14

Separate question -- do you find you tend to create a separate namespace just to hold specs for other namespaces, especially if they're shared? Or do you tend to put specs within the namespaces containing other logic?


Specs for data structures tend to live in their own namespace, no code. Specs for functions live in the same namespace as the function. Roughly.


I wrote about our use of Spec here based on what we do at World Singles Networks.

thanks3 1

Has it been hammocked (officially or not) to give the notion of coverage to specs? e.g. for an (s/or), verify at runtime, integrated with a given test suite, that all branches of the or have been exercised. (I know that generative testing is a thing! I want it the other way around here: start by code and reach the spec, not vice versa)


One big use case, is that for any defn that returns s/or :result ,,, :error ,,,I should rest assured that the test suite has coverage for the green and red paths of the spec alike

Alex Miller (Clojure team)13:11:13

Haven't talked about it

Colin P. Hill13:11:20

Is there a writeup anywhere about the reason that instrument doesn't validate :ret and :fn? I was wondering if this was considered and decided against, or there were some non-obvious difficulties. There's a library out there that does this, but I've got this nagging suspicion that it was left out of the first-party spec library for a reason.

Colin P. Hill14:11:24

Ah my bad, should have looked around for a FAQ, of course it's on there. Thank you!