clojure-spec

About: http://clojure.org/about/spec Guide: http://clojure.org/guides/spec API: https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html
seancorfield 2020-05-12T02:09:03.340200Z

As long as you're not building anything with it that is more than a personal toy project @rafael...

fmnoise 2020-05-12T09:22:16.341800Z

hi everyone, is it possible to add meta to spec?

(s/def ::my-custom-int ^{:error-fmt #(str % " is not valid int")} int?)
when I call (meta (s/get-spec ::my-custom-int)) it doesn't contain my meta

2020-05-12T09:26:58.343600Z

@fmnoise I don’t know the answer, but I’m pretty sure you’re checking the wrong thing. Try (meta ::my-custom-int)

vlaaad 2020-05-12T09:31:26.344Z

(s/def ::my-int (with-meta (s/spec int?) {:err-format #(str % " is not a valid int")}))

vlaaad 2020-05-12T09:32:02.344500Z

@fmnoise then (meta (sp/get-spec ::my-int)) will work

fmnoise 2020-05-12T09:56:18.344900Z

thanks @vlaaad, this works!

alexmiller 2020-05-12T12:45:59.346700Z

While this may work, please that it won’t work on all specs and some spec transformations like with-gen will not preserve meta. This is more accidental than intentional

1
alexmiller 2020-05-12T12:49:09.347800Z

Adding meta and doc string support to specs is the most requested thing in the tracker and I expect we will add it in spec 2

🤘 3
2020-05-12T18:16:32.351Z

I have a design question - I have a Kafka Streams topology where I use conformance to automatically sanitise input, then split the stream into two - those where conformance resulted in ::s/invalid and a happy path. In the happy path, I call a pure function that transforms the conformed message. This transformer has a function spec attached to it, but because I have already conformed the input according to the arg spec before calling the function, it will fail the conformance check.

2020-05-12T18:17:08.351900Z

I seem to be facing two options - either remove the function spec, or create yet another spec for the already-conformed input, a "derived spec" of sorts. Neither really feels satisfactory.

2020-05-12T18:18:03.353100Z

Yet another alternative would be to simply validate with s/valid? earlier in the stream, then conform inside the pure function - which feels like the right thing to do, but it also feels like I am validating the data twice effectively. What would be the cleanest solution here?

seancorfield 2020-05-12T18:59:34.354700Z

@sgerguri The approach we've taken at work is to treat that initial Spec layer as a boundary and assume it maps from "external data format" to "internal data format" and is applied consistently. Thus everything "inside" is Spec'd in terms of the (already conformed) data produced at the boundary.

2020-05-12T19:00:37.355600Z

So if I understand it correctly you have a second spec layer for the internal representation, that represents things that have been conformed, correct?

seancorfield 2020-05-12T19:07:42.358400Z

Yeah, an API Spec, a Domain Spec, and actually a Persistence Spec. The API Spec is conforming, from external data (often strings) to internal data (numeric, Boolean, date, etc). The Domain Spec describes the data formats used inside the API. The Persistence Spec describes the data that goes into the database (flat hash maps with JDBC-compatible types). We don't use all three in all cases, but it seems to have become a useful way to split things up -- and it clearly identifies two boundaries (API input and database output).

2020-05-12T19:09:06.360700Z

That's excellent, thanks. I have used a similar approach in another service, the only thing I'm struggling with in this one is the fact that the internal layer is very thin and quite close to the input layer, thus it feels like duplicating the specs, but might be worth it for the greater clarity.

mpenet 2020-05-12T19:10:26.362100Z

spec-coerce is quite good for this too

seancorfield 2020-05-12T19:11:05.363700Z

I would caution against leaning too heavily on conforming with Spec -- I don't really like what spec-coerce enables.

mpenet 2020-05-12T19:12:07.366Z

By default it will do the right thing to coerce x to your spec expeceted values, and the other way around (serializing to db type, ex keyword -> string) just requires to override those cases

mpenet 2020-05-12T19:12:36.367300Z

No spec duplication that way

mpenet 2020-05-12T19:12:46.367800Z

No polluted specs either (no conform abuse)

2020-05-12T19:13:30.369300Z

I have used it for some fairly tricky transformations in the past, but I've found it really works well for emulating pattern matching along with multimethods by tagging individual cases through an s/or. In this particular example I only wanted to trim whitespace but for some reason am having trouble fitting it neatly in without overhauling one part of the service or another.

2020-05-12T19:15:06.371Z

This particular situation also left me wondering whether conformance should either be the final station in your transformation/validation stack or whether it should yield something that also has a spec for it (without further conformers), like @seancorfield described.

2020-05-12T19:15:45.371800Z

In the grand scheme of things I could just trim the fields I need to but with spec around that just feels like the wrong approach.

seancorfield 2020-05-12T19:16:10.372100Z

There are folks on both sides of that decision 🙂

seancorfield 2020-05-12T19:17:09.373200Z

Some feel that input should be cleaned and parsed first, then checked for validity with Spec (and therefore next to no conforming). Others feel that input conformance to valid data is a reasonable use of Spec.

mpenet 2020-05-12T19:17:51.374600Z

I used to have dual specs like you mention, but it requires more work and is more brittle imho

mpenet 2020-05-12T19:18:03.375100Z

Then as you say ymmv

seancorfield 2020-05-12T19:18:17.375500Z

I am slightly on the conforming side of center. spec-coerce is quite a lot further out on the conforming side. I think Alex (and maybe others at Cognitect) are on the non-conforming side of center.

alexmiller 2020-05-12T19:18:56.376700Z

I actually like spec-coerce :)

👍 1
seancorfield 2020-05-12T19:19:17.377200Z

Really? I was sure you had complained about it in principle in the past? 🙂

alexmiller 2020-05-12T19:19:48.377400Z

I don't like spec-tools

2020-05-12T19:21:50.378300Z

Food for thought. Thank you everyone, always a pleasure to come here for another opinion on how folk do things. 🙇

➕ 1
alexmiller 2020-05-12T19:26:39.379200Z

spec-coerce builds a separate registry of coercions that leverages specs, which I think is a good approach

alexmiller 2020-05-12T19:27:20.379600Z

I haven't used it in anger but that seems in line with how I would approach it

mpenet 2020-05-12T19:29:19.381800Z

It's gotten better lately, it was missing multi-specs, merge & tuple until recently but now it's quite feature complete

mpenet 2020-05-12T19:30:16.383100Z

It's also 300ish lines of code, easy to modify, fit to your taste if needed

seancorfield 2020-05-12T19:43:51.385Z

@alexmiller Ah, thanks for the clarification. Then I suspect I'm getting the two libraries confused and maybe I should look at spec-coerce in more detail? 🙂

mpenet 2020-05-12T20:06:22.386400Z

You contributed to it a few years ago https://github.com/wilkerlucio/spec-coerce/pull/11 :)

Amir Eldor 2020-05-12T20:31:02.386700Z

Hello, I hope it's ok to post some code. I'm trying to spec a function that has ehmm, this optional arguments thing? I'm not sure how it's called in Clojure. I'm having some trouble with the spec for it. Right now I specifically use (st/instrument) in a test as suggested and the following code breaks with the following exceptions: (ship (:id p)) ; (:id p) is surely a uuid || :cause "class java.util.UUID cannot be cast to class java.lang.Number (java.util.UUID and java.lang.Number are in module java.base of loader 'bootstrap')"

fmnoise 2020-05-13T10:36:45.394900Z

that was interesting case, thanks @amir and @seancorfield

Amir Eldor 2020-05-12T20:32:36.387Z

It seems like it's related to the ::speed thing, being an integer. I must have made something funny in the spec, if someone can notice. Thanks!

Amir Eldor 2020-05-12T20:46:14.387200Z

Ah yes, thet exception I gave is invoked on the s/def of ::speed 😕

fmnoise 2020-05-12T21:35:26.387400Z

what is (random-id) ?

Amir Eldor 2020-05-12T21:40:14.387600Z

It's my own function, which calls (UUID/randomUUID) wth java.util.UUID

seancorfield 2020-05-12T21:40:53.387800Z

Wow, I don't even remember that. Looks like I'd started to look at spec-coerce as a possible alternative to our existing "web specs" at World Singles Networks -- and I guess by the time that PR got accepted I'd decided not to go down that particular path for some reason...

Amir Eldor 2020-05-12T21:43:53.388Z

The code itself works without the spec, I must be defining something badly

seancorfield 2020-05-12T21:45:18.388200Z

I've made a note to revisit it, but here's what we ended up doing at work https://github.com/worldsingles/web-specs

fmnoise 2020-05-12T21:45:56.388500Z

what about default-ship-speed?

Amir Eldor 2020-05-12T21:46:58.388700Z

(defonce default-ship-speed 20)

seancorfield 2020-05-12T21:51:08.388900Z

What is DateTime?

Amir Eldor 2020-05-12T21:51:32.389100Z

(:import [org.joda.time DateTime]
           [java.util UUID]))
From Java, too

Amir Eldor 2020-05-12T21:51:55.389300Z

Though I use clj-time, so I might be wrong here when I think of it

seancorfield 2020-05-12T21:52:12.389600Z

'k... Joda Time is deprecated. If you're on Java 8 or later, you should use Java Time really.

seancorfield 2020-05-12T21:52:19.389800Z

clj-time is also deprecated.

seancorfield 2020-05-12T21:52:24.390Z

(for the same reason)

Amir Eldor 2020-05-12T21:52:51.390200Z

Oh, thanks for letting me know. I'll google for Java Time

fmnoise 2020-05-12T21:53:04.390400Z

(s/def ::speed (fn [v] (and (int? v) (> v 0)))) this definition works

fmnoise 2020-05-12T21:54:07.390600Z

but dunno why it assumes uuid there by default when doing coercion

seancorfield 2020-05-12T21:57:37.390900Z

The problem is that all three optional arguments are optional independently

seancorfield 2020-05-12T21:58:15.391100Z

So dest-planet-id could be omitted and the speed and departure-time could still be provided.

seancorfield 2020-05-12T21:59:09.391300Z

In other words, it tries to check (ship src-planet-id src-planet-id) against a signature that is essentially [uuid? #(> % 0)] and that's causing the exception.

seancorfield 2020-05-12T22:00:24.391500Z

And that's why adding (and (int? %) ...) into ::speed "works" -- it guards the > operation from being called on non-numeric values.

seancorfield 2020-05-12T22:01:07.391700Z

Because then the ::speed spec successfully fails to match (instead of blowing up) and Spec goes on to try the other options.

seancorfield 2020-05-12T22:01:44.391900Z

So...

seancorfield 2020-05-12T22:02:48.392100Z

I'd suggest spec'ing ship a bit differently, perhaps using s/alt over the four arities, each spec'd with no optional parameters.

seancorfield 2020-05-12T22:03:10.392300Z

Or at least across the first three and leave just :departure-time as s/?

Amir Eldor 2020-05-12T22:03:37.392500Z

Thanks a lot! However I don't fully understand how this happens: > dest-planet-id could be omitted and the `speed` and `departure-time` could still be provided.

Amir Eldor 2020-05-12T22:05:16.392700Z

I'm sorry I have to go now as it's getting late. I hope it's ok if I ping you tomorrow in this thread if I still have some trouble. Thanks!

seancorfield 2020-05-12T22:09:18.392900Z

You have src dest? speed? time? -- each of those three are optional, so each could be omitted while the others are passed.

seancorfield 2020-05-12T22:09:44.393100Z

so src speed is "valid", as is src time or src dest time or src speed time.

seancorfield 2020-05-12T22:09:54.393300Z

That's what your fdef says.

seancorfield 2020-05-12T22:10:56.393500Z

So when Spec sees (ship src-planet-id src-planet-id) it's going to try src speed, src time, and src dest in some random order.

seancorfield 2020-05-12T22:11:26.393700Z

Since your speed spec just tries to compare the value > it will throw an exception if passed a non-number: which a UUID is.

seancorfield 2020-05-12T22:11:49.393900Z

Because Spec encounters an exception, it won't try the other options -- it just propagates the exception.

seancorfield 2020-05-12T22:15:03.394100Z

(! 629)-> clj -A:test -Sdeps '{:deps {clj-time {:mvn/version "RELEASE"}}}'
Clojure 1.10.1
user=> (require '[clojure.spec.alpha :as s] '[clojure.spec.test.alpha :as st])
nil
user=> (import '(org.joda.time DateTime) '(java.util UUID))
java.util.UUID
user=> (defonce default-ship-speed 20)
#'user/default-ship-speed
user=> (defn random-id [] (UUID/randomUUID))
#'user/random-id
user=> (s/def ::id uuid?)
:user/id
user=> (s/def ::src-planet-id uuid?)
:user/src-planet-id
user=> (s/def ::dest-planet-id uuid?)
:user/dest-planet-id
user=> (s/def ::speed #(> % 0))
:user/speed
user=> (s/def ::departure-time #(or (nil? %) (instance? DateTime %)))
:user/departure-time
user=> (s/def ::resources #(>= % 0))
:user/resources
user=> (s/def ::ship (s/keys :req-un [::id ::src-planet-id ::dest-planet-id ::speed ::departure-time ::resources]))
:user/ship
user=> (defrecord Ship [id src-planet-id dest-planet-id speed departure-time resources])
user.Ship
user=> (s/fdef ship
        :args (s/alt :arity1 (s/cat :src-planet-id ::src-planet-id)
                     :arity2 (s/cat :src-planet-id ::src-planet-id :dest-planet-id ::dest-planet-id)
                     :arityN (s/cat :src-planet-id ::src-planet-id :dest-planet-id ::dest-planet-id :ship-speed ::speed :departure-time (s/? ::departure-time)))
        :ret ::ship)
user/ship
user=> (defn ship
  ([src-planet-id]
   (ship src-planet-id src-planet-id))

  ([src-planet-id dest-planet-id]
   (ship src-planet-id dest-planet-id default-ship-speed))

  ([src-planet-id dest-planet-id ship-speed]
   (ship src-planet-id dest-planet-id ship-speed nil))

  ([src-planet-id dest-planet-id ship-speed departure-time]
   (->Ship (random-id) src-planet-id dest-planet-id (if (nil? ship-speed) default-ship-speed ship-speed) departure-time 0)))
#'user/ship
user=> (st/instrument)
[user/ship]
user=> (ship (random-id))
#user.Ship{:id #uuid "3ad92050-2b9b-499e-b7e3-f933053d7b1c", :src-planet-id #uuid "79cd0367-d78d-4774-9f5f-3a29a8c77851", :dest-planet-id #uuid "79cd0367-d78d-4774-9f5f-3a29a8c77851", :speed 20, :departure-time nil, :resources 0}
user=> 

seancorfield 2020-05-12T22:15:38.394300Z

^ That shows the s/alt structure that would work @amir