This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2018-08-05
Channels
- # architecture (23)
- # bangalore-clj (2)
- # beginners (93)
- # cider (27)
- # cljsjs (2)
- # clojure (66)
- # clojure-russia (9)
- # clojure-spec (28)
- # clojure-uk (3)
- # clojurescript (47)
- # cursive (2)
- # data-science (2)
- # datomic (10)
- # editors (9)
- # emacs (4)
- # figwheel (3)
- # figwheel-main (1)
- # hyperfiddle (2)
- # jobs (1)
- # nrepl (59)
- # off-topic (2)
- # onyx (10)
- # pedestal (1)
- # re-frame (13)
- # reagent (9)
- # reitit (17)
- # shadow-cljs (8)
- # tools-deps (4)
- # vim (2)
What's an example of a well or appropriately specced codebase?
I don’t think there’s only one correct approach but here’s a popular lib with specs https://github.com/clojure/java.jdbc/blob/master/src/main/clojure/clojure/java/jdbc/spec.clj
Note: that deliberately puts all the specs in a separate namespace so that they are optional and clojure.java.jdbc
can still be used with earlier versions of Clojure.
Also, if you instrument that library, there's a noticeable overhead in performance (running the test suite takes much, much longer) and that's partly due to the complexity of some of the specs. I have a ticket open to look into optimizing them.
Another point to consider: those specs focus on functions. Spec is amazing with data structures -- and that's mostly how we use it at work: to specify data structures (including API parameters) and to validate values against those specs (with some coercions). We also use specs for tests, but that's less of our focus.
Yes, we have specs for all our (public/external) APIs and then s/conform
the data on entry to the API. If that is s/invalid?
then we use s/explain-data
to construct appropriate error codes and messages for the client/caller, otherwise we go forward with the conformed data.
Our specs include some coercion, since API input data can be either strings or "values" -- so we conform strings to values (and pass values through). So true
is acceptable where we expect a Boolean, but so is "true"
and that is conformed to true
. Same with long, double, and date values. We have custom generators that produce strings (mostly they just use the "expected" value spec as a generator and then call fmap str
on the result).
@U04V70XH6 thanks for the detailed reply! considering that specs are arbitrary predicates, do you find it easy to construct human-readable error messages?
It took a while to develop heuristics for walking the explain data but, given our domain and the specific set of specs and predicates in use, it isn't too bad.
i think i've a vague idea of what you mean by focus on functions, could you elaborate on what you mean by focus on functions vs focus on data structures?
also, whats the difference between s/nilable
and s/?
?
@ackerleytng Sorry, I was off having dinner... s/nilable
wraps a spec or predicate and produces a new spec that allows a value to be nil
or conform to the wrapped spec.
s/?
is a regex spec (in an s/cat
sequence) for something that is optional.
So for a data structure (or argument list) that may have two or three elements, the third one is optional and would be specified with s/?
.
for an element that could be nil
or a string, you would use (s/nilable string?)
-- it's a value either way, it's just allowed to be nil
or else conform to string?
.
Thanks! So it's like... Elements can be nothing, nil or something. s/nilable
allows the element to be either nil or something, but s/?
allows the element to not be there at all.
Make sense?
As for focus on function vs data, it's about whether you're spec'ing function arguments (and return values and the relationship between them), vs whether you're using s/def
to spec entities that are part of a data structure (and the data structures themselves). Not sure how to explain it beyond that. Does that help @ackerleytng?
But without spec'ing functions, there's no way to trigger checks unless you manually call functions and use s/conform
on the outputs right? So we're better off spec'ing the functions then running check in a deftest
As in, I thought whether or not you spec functions, you would have to spec the data going in and out of functions
Since you kind of want to exercise the specs in some way, it makes sense to just spec the functions to run tests, does that make sense?
We call s/conform
explicitly because we're using spec in production code. And we conform just the data (arguments) we want to, whereas instrument
checks all of them -- so you have to write specs for all the arguments.
Spec'ing functions and spec'ing data are very different approaches. Typically the former is for dev/test since the overhead of instrument
can be high -- and it will use generative testing on fspec
function arguments. The latter, however, is a great way to do data validation in production code.
I see, thanks! So if i'm using spec in production, then s/conform
is the way to go, and if i'm using spec for testing, then fspec
is a good idea
could the Spec
s have a concise toString representation? example with expound:
-- Spec failed --------------------
{:coercion ...,
:middleware ...,
:summary ...,
:swagger ...,
:parameters {:body ..., :header #object[clojure.spec.alpha$map_spec_impl$reify__1931 0x5ac022 "clojure.spec.alpha$map_spec_impl$reify__1931@5ac022"]},
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
:responses ...,
:handler ...}
should satisfy
map?