Fork me on GitHub
#clojure-spec
<
2017-07-30
>
camdez00:07:48

Apologies because I’m sure this gets asked all the time but…if I have nested data which reuses keys, like this…

{:id 214566,
 :name "Fake Co",
 :phone_number {:id 141683, :phone_number "555 123 4567"}}
…if I don’t want to do something gross like a semantic-defying s/or, is my only option to make have two phone_number specs in separate namespaces?

camdez00:07:04

And, assuming that is right, any advice on where those additional namespaces live? Do you actually go with a proliferation of tiny files? Or create the namespaces without creating files? And then do you alias those namespaces or just go with something like this?:

(ns foo.specs
  (:require [clojure.spec :as s]))

(s/def ::id integer?)
(s/def ::name string?)
(s/def :foo.specs.phone_number/phone_number string?)
(s/def ::phone_number (s/keys :req-un [::id :foo.specs.phone_number/phone_number]))
(s/def ::business (s/keys :req-un [::id ::name ::phone_number]))

bfabry01:07:23

@camdez you don't need to actually create namespaces to use them, you can just

(alias 'foo.customers 'customers) 
(alias 'foo.customers.phone_numbers 'phone_numbers)
and then do
(s/def ::phone_numbers/phone_number string?)
(s/def ::customers/phone_number (s/keys :req-un [::phone_numbers/phone_number))

bfabry01:07:32

standard disclaimer that if you can figure a way to make your app use namespaced keywords using :req and :opt rather than :req-un and :opt-un you'll probably have a better time in the long run

camdez02:07:04

Thanks, @bfabry. A couple small notes for posterity after experimenting a bit… 1. The argument order for alias is actually the other way: (alias 'phone_numbers 'foo.customers.phone_numbers) 2. The namespace actually needs to exist before you can alias it (one could (create-ns 'foo.customers.phone_numbers)), even though the namespaced keywords used for the spec registry don’t necessarily need to exist. I appreciate the disclaimer but I’m trying to wrap a spec around an API response I don’t control, so it is what it is.

camdez02:07:22

This all makes me wonder even more how people are actually doing things. Does your spec registry tend to match actual namespaces? Are those in the same namespaces as your code? Or a parallel set of namespaces? Inquiring minds want to know. 😛

camdez02:07:37

IMHO Spec seems broad enough in its scope to require not just how can I but how should I documentation.

seancorfield04:07:56

@camdez We have a combination of keywords where the namespace matches a code namespace and others where they just use a unique single-segment prefix.

camdez04:07:02

@seancorfield Thanks! What’s the deciding factor between the former and the latter?

seancorfield04:07:59

I think the recommended approach for alias currently, if you want to introduce a new ns is (alias 'alias (create-ns 'the.full.name)) FYI.

seancorfield04:07:38

@camdez It Depends(tm) 🙂

seancorfield04:07:16

For specs that are internal to a ns, the obvious choice is just to use ::name and it'll be in that ns. For specs that are intended to be used across nses, then you need a "unique enough" name for the intended usage. So if that's a subsystem of your own application, you can just use a simple qualifier. For example, we use wsbilling for World Singles Billing. You could use a prefix that matched a DB table, for example, or a component in your app.

camdez04:07:47

@seancorfield Thanks. Good explanation.

seancorfield04:07:54

For specs that are intended to be more global, then the prefix also needs to be more global.

seancorfield04:07:27

I think it's a bit early yet with clojure.spec for "best practices" to have truly settled down.

seancorfield04:07:23

We've been using it heavily in production for quite a while. We've defined the data model for a few systems with it and we use those for both test generation of data and validation of some inputs. We also have a series of coercing specs defined around one of our REST APIs: we s/conform the raw parameters and get either ::s/invalid or the valid parameters conformed to longs, doubles, strings, keywords etc.

camdez05:07:40

@seancorfield I definitely agree RE best practices but it feels like such a buffet that we definitely need to be having the conversations about what might become best practices.

camdez05:07:29

Despite some valiant documentation efforts (largely around what’s possible) I think it’s still hard for a spec beginner to know where to start.

seancorfield06:07:31

Yes, definitely hard. I actually submitted a talk for one of the Clojure conferences about our use of spec, but after talking to Alex Miller, I pulled it because I wasn't sure we were doing things a good way (I've since changed my mind again 🙂 )

seancorfield06:07:12

(to be fair, I had a few reasons for pulling the talk but that was the main one)

seancorfield06:07:09

I'm looking forward to hearing other people's spec talks at future Clojure conferences!

matan08:07:02

Hi all, are we approaching a release of clojure 1.9 with the finalized spec in it? I still see in the docs a mention of :clojure.spec.alpha/invalid, will I have to change my code to use a differently named value later on or is it just out-of-date documentation? https://clojure.org/guides/spec

camdez15:07:10

@matan I’m not speaking with any authority here, but I don’t believe the docs are out of date (see latest here: https://github.com/clojure/spec.alpha). I believe you will have to use a different named value later, but if you’re requiring spec with an alias (e.g. (:require [clojure.spec.alpha :as s])) and using that alias to refer to the keyword (e.g. :s/invalid) then you’ll only need to change the require statement later.

camdez15:07:52

I’m assuming that code living under an alpha namespace is not subject to the standard guidelines about accretion and breakage (indeed, that’s likely the exact meaning of the alpha namespace), and thus clojure.spec.alpha will become clojure.spec in a non-backwards compatible way.

stathissideris18:07:13

this is how to generate params for a function based on its spec:

(gen/generate (s/gen (:args (s/get-spec (resolve `my-fn)))))