Fork me on GitHub
#clojure-spec
<
2020-01-29
>
ag02:01:36

I need a spec for a map like {:a :b} where :b has to be a required key, but only when value of :a is not nil. Can someone help me with that?

seancorfield03:01:41

user=> (s/def ::ab (s/and (s/keys :opt-un [::a ::b]) #(or (nil? (:a %)) (contains? % :b))))
:user/ab
user=> (s/valid? ::ab {:x 1})
true
user=> (s/valid? ::ab {:a nil})
true
user=> (s/valid? ::ab {:a 1})
false
user=> (s/valid? ::ab {:a 1 :b 2})
true
@ag how about that?

seancorfield03:01:41

If :a should be required, use (s/keys :req-un [::a] :opt-un [::b]) I guess.

ag03:01:18

ah, I was playing around, come up with something like this:

(s/def ::a string?)
(s/def ::b string?)
(s/def ::base-map (s/keys :req-un [::a ::b]))

(s/def
  :foo/bar
  (s/merge
   ::base-map
   (s/and
    (fn [m]
      (if (= (m :a) "bobo-included")
       (contains? m :bobo)
        true
        )))))
there’s probably better way of doing this Basically now :foo/bar is a spec for a map that must have :a and :b, keys, but when value of :a is equal to “bobo-included”, it must have :bobo, key, otherwise, :bobo key is optional

seancorfield03:01:14

s/merge is intended for two key specs -- I don't think you should use it for a key spec and a predicate.

seancorfield03:01:52

And it looks like you have s/and with just a single predicate there?

seancorfield03:01:29

Now that you've restated the problem, it sounds like you want to look at multi-specs.

ag03:01:08

ah… yeah. let me try digging in there

seancorfield03:01:08

Those are intended to select different specs based on some function of a value, in your case, the value of the :a key.

ag03:01:25

well, yeah, I guess I didn’t exactly follow my own requirement, but the approach you showed me probably would work for me. I’m gonna refresh my memory on multi-specs anyway.

ag03:01:29

Thank you Sean!

lambdam10:01:43

Hello, I was playing a bit with spec 2 on an Advent of Code exercise and bumped into this case:

(def dam
  {:firstname "Dam"
   :age 35})

(s/def ::user (s/schema {:firstname string?
                         :lastname string?}))

(s/explain (s/select ::user [*])
           dam)
;; => {:firstname "Dam", :age 35} - failed: (fn [m] (contains? m :lastname))

(s/explain (s/select {:firstname string?
                      :lastname string?}
                     [*])
           dam)
;; => Success!

(s/explain (s/select [{:firstname string?
                       :lastname string?}]
                     [*])
           dam)
;; => {:firstname "Dam", :age 35} - failed: (fn [m] (contains? m :lastname))

(s/explain (s/select [{:firstname string?}
                      {:lastname string?}]
                     [*])
           dam)
;; => {:firstname "Dam", :age 35} - failed: (fn [m] (contains? m :lastname))
The second explain is weird (considering https://github.com/clojure/spec-alpha2/wiki/Schema-and-select#literal-schemas). Is there a subtle detail that I'm missing? (ping @alexmiller) Thanks

alexmiller13:01:57

Dunno, probably a bug

lambdam10:01:53

Also, is there a specific reason not to instrument :ret and :fn in fdef (even optionally) in spec 2? I use https://github.com/jeaye/orchestra with spec 1 and find it very useful.

lambdam14:01:16

Ah ok. Thanks.

lambdam11:01:03

Also

(s/valid?
  (s/schema {:foo string?})
  {:foo 1})
=> false

(s/assert
  (s/schema {:foo string?})
  {:foo 1})
=> {:foo 1}
... weird

zclj15:01:26

I get an exception when using a schema referring a spec with an indirection to another spec. It works as expected if I just use the spec. Am I doing anything wrong here or is it a bug?

(s/def ::thing string?)
(s/def ::other-thing ::thing)
(gen/sample (s/gen ::other-thing)) ;; => works as expected

(s/def ::foo (s/schema [::thing]))
(s/def ::bar (s/schema [::other-thing]))
(gen/sample (s/gen ::foo)) ;; => works as expected
(gen/sample (s/gen ::bar)) ;; => Exception below
  
  No implementation of method: :conform* of protocol:
   #'clojure.alpha.spec.protocols/Spec found for class:
   clojure.lang.Keyword

alexmiller15:01:56

this is a bug in spec 2, so won't work yet

kenny20:01:30

Does spec2 let you write a spec for a qualified keyword such that its definition changes depending on the context? For example, if I were to spec Datomic's :db/id, it would only ever be a nat-int? when part of the result is from a pull query. When transacting to Datomic, :db/id could be a lookup ref (e.g., (s/tuple keyword? some?) ), a nat-int? , or a string?. One could spec :db/id using s/or but that means everything that takes a db id as an input needs to handle all of those cases, which does not always make sense.

alexmiller20:01:15

in short, no

alexmiller20:01:58

in long, the general recommendation is to try to give attributes specs that truly reflect the data

alexmiller20:01:31

within certain contexts, you can add on additional specs that narrow the scope if needed

kenny21:01:18

By this do you mean you can add additional predicates to the :db/id spec in different places?

alexmiller21:01:05

yes, you could s/valid while s/and'ing in an additional narrower predicate for example

kenny21:01:41

Oh cool. Is there an example of what that looks like?

alexmiller21:01:58

just what I said?

kenny21:01:01

Oh - that works if the :db/id is taken as an argument itself. I was imagining a function that takes a map (perhaps nested) that has :db/ids on the maps.

kenny21:01:33

Or do you mean needing to write a predicate that walks that structure validating db/id against this new predicate?

alexmiller21:01:01

you can still s/and a predicate to a map that checks a value in the map

alexmiller21:01:13

it's also an option to just not spec :db/id