Fork me on GitHub
#clojure-spec
<
2019-04-01
>
tangrammer07:04:17

hi spec people!! I’m trying to temporarily redefine the spec global registry in the same way as with-redefsdoes … My intention is to reduce possible valid values, eg: instead of only checking that value is a string, validate that is exactly foo value so far, this is the code that i’ve reached…

(s/def ::simple string?)

(s/def ::composed
  (s/keys :req [::simple]))

(with-redefs [s/registry (constantly (assoc (s/registry) ::simple (#'s/res #{"foo"})))]
    [(s/valid? ::composed {::simple "other"})
     (s/valid? ::composed {::simple "foo"})] 
    ) ;; [false true]

(s/valid? ::composed {::simple "other"}) ;; true

:thinking_face: what do you think about using with-redefs to redefine specs?

tangrammer12:04:16

the final idea is to apply authorisation

tangrammer12:04:58

basically if certain path values of the speced data are “allowed”, the full data could be viewed by a user or not

tangrammer12:04:33

i’s it more clear now?

Alex Miller (Clojure team)12:04:05

Sounds like a nightmare for such a critical role

tangrammer12:04:49

could you expand a bit why you think it could be a nightmare?

Alex Miller (Clojure team)13:04:14

why would you base something as critical as auth on something as fragile as with-redefs?

tangrammer13:04:38

sorry i didn’t realise about “These temporary changes will be visible in all threads” 😬 … I was trying for something thread isolated … but with-redefs was my quick first attempt

djtango15:04:26

if you want thread-local rebinding - use binding

djtango15:04:54

though so far the only things I've found with-redef useful for is orthogonal instrumentation of functions (e.g. timing functions) or stubbing in testing

tangrammer15:04:18

(def ^:dynamic my-fun string?)

(s/def ::simple #'my-fun)

(s/def ::composed (s/keys :req [::simple]))

(s/def ::extra-composed (s/keys :req [::composed]))

(assert (s/valid? ::extra-composed {::composed {::simple "one"}}))

(assert (= (binding [my-fun #{"foo"} ]
             [(s/valid? ::extra-composed {::composed {::simple "other"}})
              (s/valid? ::extra-composed {::composed {::simple "foo"}})])
           [false true]))

(assert (s/valid? ::extra-composed {::composed {::simple "other"}}))


djtango15:04:19

(though since spec I've not really redef'd fns for testing)

tangrammer15:04:43

example using binding ^^ 🙂

djtango15:04:19

ya - still hard to see how / when you might apply that but at least it's thread-safe now

tangrammer15:04:18

@U064X3EF3 maybe less nightmare now with binding ?

Alex Miller (Clojure team)15:04:04

Less but the idea of changing specs still seems like an idea likely to cause problems later

4
tangrammer15:04:01

I'll take into account, thanks both for your help!

djtango16:04:50

If it's a top level spec you can do:

(let [the-spec (if p? ::a ::b)]
  (s/valid? the-spec data))
If it's a nested spec, you could try using :req-un to have (s/keys :req-un [:very-specific/a]) and (s/keys :req-un [:general/a]) alternatively, you could try experimenting (s/or ...) If you're using namespaced keys on your data, it's hard to understand why the spec for a namespaced entity would change

4
tangrammer21:04:48

more than change, the spec should be more specific. so instead of having a predicate string? would be #{"value1" "value2" "value3"}

tangrammer21:04:59

being these values the returned of a sql execution

tangrammer12:04:09

the thing (at this level) is getting spec validation messages

djtango13:04:41

given that specs are typically named with just data can't you dispatch at real-time the spec you might want to validate against

👍 4
djtango13:04:55

(with no other knowledge of what you're trying to achieve)

mathpunk16:04:03

I've got a question about spec design. The situation: I have an entity in mind, which I call a run (for test run). It has two variants: it might be a test run from CI, or it might be a test run from my local machine.

mathpunk16:04:36

Some data is present in runs of any variant, like results and a revision (the SHA of the codebase during the run). Only CI runs have a pipeline. Both variants have logs but only the local variant has api logs under that key.

mathpunk16:04:04

My design problem is, I'm trying to use mostly namespaced keywords, so that I can nest specs. For instance,

(s/def ::revision (fn [s] (boolean (re-find #"[a-f0-9]+" s))))
(s/def ::meta-v1
  (s/keys :req [::date ::job ::pipeline ::revision]
          :opt [::suites]))

mathpunk16:04:40

But then I realized, ooooh when I start modeling runs of the local variant, project.ci.runs/revision is overly specific

mathpunk16:04:29

Do you have any thoughts on what I should keep in mind while spec'ing these variants out? Thanks!

mathpunk16:04:04

I guess I should add, I'm trying to be careful about not over-spec'ing things --- this could be an example of, Don't spec this part

mathpunk16:04:14

(The value-add that I see of spec'ing this data is, I've been doing these runs for months, and iterating on how they're shaped, so I'm interested in validating that I understand what 'type' of run I'm looking at)

Alex Miller (Clojure team)16:04:37

a good guiding principle is to just try to spec the truth - what are all the values an attribute can actually take on?

Alex Miller (Clojure team)16:04:01

if it's not constrained to the regex, then that's not the truth, it's just one variant

Alex Miller (Clojure team)16:04:20

it's always possible to spec generally, then s/and an additional constraint for a particular context

mathpunk16:04:01

:thinking_face: So maybe,

:project.run/revision
    :

mathpunk16:04:54

oh wait, you said 's/and', not 'and'... I'll have to think about that a minute

kenny17:04:21

How do you guys use clojure.test.check.generators/let with Spec? It doesn't appear to be exposed in clojure.spec.gen.alpha.

taylor01:04:53

I just add the dependency explicitly to the test check namespace

kenny01:04:21

We don't allow test.check as a runtime dependency.

taylor01:04:29

Instead of using the aliased stuff from spec

taylor01:04:43

You can add it as a dev only dependency

kenny01:04:02

Right but then you can't define your generators next to your specs.

taylor01:04:18

Also, you’re depending on it already if you’re actually using any of its functionality

kenny01:04:04

We don't use the functionality in production but want to define the generators next to our specs.

taylor01:04:19

The stuff in spec are merely aliases to the test check definitions

kenny01:04:54

True but it does it lazily -- it only requires test.check if you actually invoke a function in the gen namespace.

taylor01:04:02

I supposed you could use the same strategy

kenny01:04:08

Yes but that begs the question of why it wasn't included to begin with. It may be because it is a macro.

gfredericks10:04:29

yes, a macro has to run at compile time

gfredericks10:04:03

so the normal trick that allows you to not have a runtime dependency on test.check doesn't work

kenny17:04:42

So the solution is to copy and paste the let macro elsewhere?

gfredericks18:04:25

You can do that, you could relax your requirement to not have test.check in prod, you could wrap in a simple proxying macro that expands to an exception throwing generator when tc isn't on the classpath

kenny19:04:52

The problem with including test.check on the cp in prod is certain code paths check input structures via s/valid?. If, by accident, a function is passed in a map of things, it would get generatively checked. That would have a significant impact on performance.

gfredericks19:04:12

They skip that based on the namespaces being requirable or not?

kenny19:04:40

Not sure what you mean.

kenny19:04:12

If test.check isn't available on the cp and a fn is passed to the s/valid? call, an exception will get thrown.

kenny19:04:00

If it is, no exception is thrown and the performance will slow down silently.

gfredericks19:04:44

Ah, I see, I didn't notice the "by accident" part In any case, the other two approaches would work though

kenny19:04:49

The latter seems like a good approach. Seems like a worthy addition to the Spec gen namespace as well.

gfredericks01:04:51

I can see that