Fork me on GitHub
#clojure-spec
<
2018-11-28
>
valerauko04:11:00

i'd split the specs

valerauko04:11:32

in the geo-relevant ns check with a ::geo-users and in the other with ::email-users

misha10:11:48

@urzds and "structure" means "value of the keyword"? so you want shape (keys set) of the maps to be the same, but content (vals) be different? e.g. {::foo 1} and {::foo "str"}?

misha10:11:21

If yes, and if you are limiting yourself to :: - just define everything (key specs and map specs) in 2 different namespaces. You'd have to define map specs twice (for your X and Y cases), since ::name will resolve to different keywords in each namespace: :my.ns.x/name and :may.ns.y/name, so your map specs would look similar, but resolve/expand into different ones, depending on namespace they are in: (s/keys :req [::name]) -> (s/keys :req [:my.ns.x/name]) and (s/keys :req [:my.ns.y/name]). I would not chase the goal to save few lines of text here, and just define it "twice". At least you'll keep "go to spec definition" feature or your IDE working.

urzds12:11:46

@misha The other way round: The values should have the same requirements, but the structure / shape / keys required may be different.

urzds12:11:25

I always thought that if I (s/def ::foo ...), and validate a map that contains this exact fully qualified keyword, then spec would pick up the s/def automatically. And that this would stop working if the namespace part of the keyword would be different.

urzds12:11:25

That's why I assumed that I can only define one map structure (at least using spec how I understood it), because eventually spec will validate the keys inside that map using the global definition for that key, instead of using the local structure that I would like to use in that specific place.

urzds12:11:14

Or will that start to work if I use :req-un / :opt-un instead of :req and :opt? I.e. can I define multiple structures / shapes using the -un variants, because the match in the map only uses the keyword name, but the structural / shape validation happens using the namespace+name? So a structure / shape that is defined using :req and :opt is enforced everywhere and defines the complete data structure, but if it is defined :req-un and :opt-un, other definitions might exist in other places that have different structural / shape requirements?

favila14:11:44

@urzds Not sure I follow the convo here, but s/keys is about validating the presence and absence of keys on a map

favila14:11:05

the values of the keys are validated by the spec attached to that keyword

favila14:11:31

s/keys does not require that every keyword mentioned have a spec

favila14:11:42

so you can require a key but not spec its value for e.g.

favila14:11:19

also :req supports and and or for grouping requirements

favila14:11:29

(and :req-un)

urzds14:11:48

Given this data: {::foo {::bar 1 ::baz 2}}, how would you validate it two times using spec, such that for the first time {::foo {::bar ...}} is required and the 2nd time {::foo {::baz ...}} is required?

favila14:11:16

you cannot without a predicate

favila14:11:36

::foo must always have the same spec

urzds14:11:37

But in both cases, ::bar and ::baz should be int?, if they are present.

favila14:11:53

what determines whether ::bar or ::baz is required?

urzds14:11:16

Me, depending on the context.

mpenet14:11:29

spec of value of foo can be a multi-spec or a s/or

favila14:11:47

you either have to give up and get a least-common-denominator spec for ::foo (e.g. (s/keys :req [(or ::bar ::baz)]) or (s/keys :req [::bar ::baz])

favila14:11:16

or you have to assert your different requirements on the spec for the outer map using predicates that look inside ::foo

favila14:11:32

(s/def ::foo-map-with-bar (s/and (s/keys :req [::foo]) #(-> % ::foo (contains? ::bar)))

favila14:11:09

spec has ironclad devotion to the principle that the keyword of an item in a map is its spec

favila14:11:25

so you can only spec ::foo once

favila14:11:14

there are a few ways to do this, but in the end you must do it in such a way that ::foo has only one spec

favila14:11:45

(another possibility is making it a multispec and putting something on ::foo's map itself that determines if ::bar or ::baz is required)

urzds14:11:33

So even if I spec (s/def ::foo-with-bar (s/keys :req-un [::bar] ::opt-un [::baz]) and (s/def ::foo-with-baz (s/keys :req-un [::baz] ::opt-un [::bar])) and use the appropriate one in the given context, that will not work?

urzds14:11:53

multi-spec is if (s/def ... ...) has a 3rd argument?

favila14:11:05

no multi-spec is a different concept

favila14:11:12

dynamic dispatch to spec based on value

urzds14:11:46

Do you have a link to the library or docs on multi-spec?

favila14:11:28

if you give your two foo uses different keywords your outer spec is just (s/keys :req [::foo-with-bar]) or (s/keys :req [::foo-with-baz]) and everything is fine

favila14:11:54

the problem is you want those two different specs, BUT ALSO as the value of a map whose key is ::foo

favila14:11:03

spec is steadfastly opposed to that

favila14:11:16

^^ that is the principle stated formally

urzds15:11:09

The ::foo-with-* examples were broken. Should look like this:

(ns common)
(s/def ::bar int?)
(s/def ::baz int?)

(ns with-bar :require [common])
(s/def ::map-with-bar (s/keys :req-un [::foo]))
(s/def ::foo (s/keys :req-un [::common/bar] ::opt-un [::common/baz]))
(s/valid? ::map-with-bar {:foo {:bar 1}})

(ns with-baz :require [common])
(s/def ::map-with-baz (s/keys :req-un [::foo]))
(s/def ::foo (s/keys :req-un [::common/baz] ::opt-un [::common/bar]))
(s/valid? ::map-with-baz {:foo {:baz 2}})

favila15:11:34

unnamespacing the key is another possible workaround

urzds15:11:50

OK, seems they strongly disagree with me: > Defining specifications of every subset/union/intersection, and then redundantly stating the semantic of each key is both an antipattern and unworkable in the most dynamic cases.

urzds15:11:54

> unnamespacing the key is another possible workaround I thought I read somewhere that specs are forbidden from having non-namespaced keys.

favila15:11:12

no I mean the key in the value

favila15:11:21

I am saying exactly what you did is a possible workaround

favila15:11:37

but your maps can only look like {:foo ...}

favila15:11:56

you have deliberately introduced ambiguity about what the spec of :foo is

favila15:11:04

(also you don't need to introduce ns forms when specing; maybe you are just doing to save typing but it isn't strictly necessary)

urzds15:11:32

How else would you do it, without the ns forms?

favila15:11:47

:with-baz/map-with-baz for eg

favila15:11:08

:: is just sugar for "keyword in current namespace"

favila15:11:22

it's not spec-specific

favila15:11:52

::alias/sym is another thing that exists

favila15:11:40

if you (:require {my.long.ns :as n]), you can use ::n/x to mean :my.long.ns/x

favila15:11:08

these expansions occur at read-time

favila15:11:13

(so very early)

favila15:11:18

anyway, to deal with this exact problem you have I wrote a spec called keys+ which is a compromise: in a particular map, a particular key can get an additional (not replacement) spec to validate against and to use for generators

favila15:11:46

so spec ::foo with the widest spec

favila15:11:32

and contextually in certain s/keys specs, say it must conform to ::foo-with-bar or ::foo-with-baz also

favila15:11:53

I was dealing with data whose shape I could not control, and the alternative was renaming keys everywhere

favila15:11:51

so your example would look like (keys+ :req [::foo] :conf {::foo ::foo-with-bar})

favila15:11:11

the value in ::foo must validate against both ::foo and ::foo-with-bar

favila15:11:33

so ::foo is (s/keys :opt [::bar ::baz])

favila15:11:55

but you can see from the gnarliness of this code that spec really doesn't even want this to be allowed

favila15:11:10

I don't even necessarily recommend that you use this

eoliphant15:11:21

hi i’m working on an fspec for a macro. The macro itself, takes an argument that will become a spec basically something like (s/def spec-key spec-pred)` in the macro body. Trying to come up with a valid spec for :spec-pred since it could be a function or say (s/or … etc. etc.) . So far I have this (s/or :fn fn? :spec s/spec?)

favila15:11:15

you might need ifn? too (to catch stuff like #{1 2 3}

4
Alex Miller (Clojure team)15:11:01

You might want to look at CLJ-2112

4
Alex Miller (Clojure team)15:11:13

Specifically the patch in there

eoliphant18:11:25

having a peculiar issue . I’m working on a spec for a variadic function, the ‘rest’ should be members of a set. In my case the set members are maps, and the test always fails. replacing them with say keywords works fine.

(defmacro test-mac
  [one & many]
  `[~one ~@many])
... works fine for (test-mac "a" :a), etc
(def base-set #{:a :b})
(s/fdef test-mac 
  :args (s/cat :one string? :many (s/* base-set)))

... fails for (test-mac "a" test-a)
(def test-a {:a :a})
(def test-b {:b :b})
(def test-set #{test-a test-b})
(s/fdef test-mac 
  :args (s/cat :one string? :many (s/* test-set)))

taylor19:11:26

the problem is spec can't see what the value of test-a is during expansion: https://clojure.org/guides/spec#_macros

taylor19:11:14

(test-mac "a" {:a :a}) works, for example

eoliphant15:11:16

ah.. crap.. right.. any ideas on how to perhaps work around this?

taylor15:11:50

could maybe move some functionality out of the macro into a function (and spec that), or reconsider whether you need a macro at all

eoliphant16:11:16

yeah it’s a bit of a chicken/egg problem. I’m working on a little DSL to create data to define a domain, so want to be able to assert stuff about say :user/name including it’s spec. so I need to pass in the spec, as well as other meta data so needed to use macros

eoliphant16:11:22

but going to play around with it