Fork me on GitHub
#clojure-spec
<
2019-04-20
>
drone00:04:31

so, we use records and have a macro that generates a spec which includes checking fields as well as asserting the object is an instance of the backing record class (the latter part can also be used in isolation as a fast check using instance?). because of this, I didn’t realize keys generates open specs

drone00:04:02

ah, partial nm. forgot about :req and :req-un, which we use behind the scenes

drone00:04:26

what’s the use case for a spec that describes keys but permits maps that have none of those keys?

drone00:04:31

and agreed with above that mutating closedness seems like a bad idea

seancorfield00:04:21

@mrevelle One observation about records is that they are open too -- you can assoc in any additional keys you want and it remains an instance that record type.

drone00:04:18

yeah, and that makes sense. but my expectation is that a record (or keys spec) provides a base set of keys that should be present

seancorfield00:04:33

My view of :opt in s/keys is that it mostly exists for generative testing (and partly for documentation).

drone00:04:23

(s/def ::f string?)
(s/def ::l string?)
(s/def ::s (s/schema [::f ::l]))
(s/valid? ::s {::x 10})  ;; "extra" keys are ok
;;=> true

drone00:04:42

that seems wrong. but maybe I’m missing something

seancorfield00:04:16

select is how you specify required-ness in spec2.

drone00:04:54

so the default, when no select is provided, is to not include anything?

seancorfield01:04:13

"Schemas do not define "required" or "optional" - it's up to selections to declare this in a context."

seancorfield01:04:50

user=> (s/valid? (s/select ::s [::f ::l]) {::x 10})
false
user=>

drone01:04:05

thanks, sorry for the easily researched question. and ugh…

seancorfield01:04:41

user=> (s/valid? (s/select ::s [::f ::l]) {::x 10 ::f "F" ::l "L"})
true
user=> 

seancorfield01:04:15

The schema/`select` stuff is going to be very helpful for us -- it will simplify our API specs quite a bit. Instead of needing to have different specs for otherwise very similar API calls, we can have a schema for the "family" of parameters that can be passed in and then use select to specify what a particular API requires.

seancorfield01:04:10

I'm still thinking about how it can help with the specs around our database, but I think it can help with the required-for-insert vs required-for-update scenarios.

drone01:04:35

yeah, sub-spec selection seems like a good idea. I think it’s just that the undefined default behavior is to not include any of the keys/fields

drone01:04:04

spec2-scratch.core> (s/valid? ::s {::x 10 ::f 10})
false

seancorfield03:04:56

@mrevelle This is not valid because ::f's value is not string? per your spec for ::f. That makes perfect sense to me.

seancorfield03:04:32

s/keys works that way today.

drone04:04:02

Oh, I don’t disagree. Was pointing out that a unselected spec field becomes active when it matches a key even though it’s not selected.

seancorfield04:04:56

Yes, which makes sense in the context of the way Spec has always worked.

seancorfield04:04:38

If you added a spec for ::x as string? then (s/valid? ::s {::x 10}) would fail -- because 10 is not string?.

seancorfield04:04:29

That's how s/keys works today. That's how s/schema and s/select work in Spec2: if you provide additional keys, they're still checked if there's a spec for them.

seancorfield04:04:29

user=> (s/def ::y string?)
:user/y
user=> (s/valid? (s/select ::s []) {::x 10})
true
user=> (s/valid? (s/select ::s []) {::x 10 ::y 11})
false
user=> (s/valid? (s/select ::s [::f]) {::x 10 ::y 11 ::f "s"})
false
user=> (s/valid? (s/select ::s [::f]) {::x 10 ::y "a" ::f "s"})
true

drone04:04:24

again, I understand and am not disagreeing with you. But you pointed out that the docs for spec2 say use of a schema without select is undefined and I’m providing examples of how saying something is undefined doesn’t mean there isn’t a default

drone04:04:14

and for my use case, it’s a poor default

seancorfield04:04:45

I didn't say it was undefined (as in "undefined behavior"). I quoted the part that says schema says nothing about required-ness -- that's what select is for -- and that decoupling is exactly what Rich was talking about in Maybe Not.

seancorfield04:04:22

What I do think is odd, right now, is that you can select keys that are not in the schema and that makes them required.

drone04:04:17

from the docs: > Schemas do not define “required” or “optional”

drone04:04:11

and that is weird

seancorfield04:04:24

I disagree. It's exactly what Rich talked about.

seancorfield04:04:40

This, on the other hand, seems wrong:

user=> (s/def ::s (s/schema [::f ::l]))
:user/s
user=> (s/valid? (s/select ::s [::f ::x]) {::f "s"})
false
user=> (s/valid? (s/select ::s [::f ::x]) {::f "s" ::x 10})
true

seancorfield04:04:13

(there's no spec for ::x but the s/select call makes it required, even tho' it isn't part of ::s)

drone04:04:52

I guess my point is, based on Rich’s talk and the current spec2 docs, (s/valid? ::s x) shouldn’t be permitted since the schema is being used without a select

seancorfield04:04:20

In the first cut of schema/`select` that landed in the repo, schemas were not specs, so that was not allowed.

seancorfield04:04:53

I'm not sure why the change was made to turn schemas into specs.

drone04:04:51

ah, I see

drone04:04:29

I’m mostly just bummed that spec seems to be going away from being a more-flexible structural typing substitute and into something that I’m not sure fits at all with how I use Clojure

drone04:04:00

thanks for the link

seancorfield04:04:33

I'm not sure I follow you... select is much more powerful than s/keys -- and now you can define the overall shape of your data once, and then validate against the relevant parts of it as needed, without having to write multiple s/keys specs that all overlap.

seancorfield04:04:59

Reading over the wiki page again in more detail, I wonder if the drive for that change (the commit above) came from wanting to treat schemas like specs because of data generation?

seancorfield04:04:55

There's an example of generating against a schema, which produces random selections of keys from the set in the schema. Which would all be "valid" in any context that accepted that schema without placing any required-ness constraints on the contents. Seems like a slim driver to me tho'...

drone04:04:25

that’s true, it’s not that something can’t be done. but it’s unclear how compatible it is with other use cases. don’t want to end up working against the language

drone04:04:17

and I think you’re probably right about generation as motivation

seancorfield17:04:53

Alex responded to my question on that commit: "The big thing that tipped it is the unqualified key support which does enforce the specs of unqualified keys (in addition to checking the values of any qualified keys against the registry)." -- because the unqualified key schemas include the spec and don't exist outside the schema (so the schema is a useful spec mostly for unqualified keys which would not otherwise be checkable).

drone01:04:25

but I didn’t select ::f from ::s

ikitommi01:04:06

@alexmiller About the closed specs. I agree on @dominicm on the open/closed on the borders. We want to drop out extra keys at the borders (recursively) and also coerce the values. Inside they can be open. Spec-tools (and by so, reitit) does both stripping extra keys & coercion already, but using the s/form parsing, which feels wrong. There is CLJ-2251 I wrote about the different ways to walk the Specs from inside, not outside. Also the fast path of just validating.

drone01:04:54

i.e., based on what Rich presented and my lurking on here, I’d expect select to be used to choose a subset of keys when the full record isn’t expected or needed. having it default to none seems silly when it’s much easier to write (s/select ::s []) when none are required than enumerate all the fields in a spec when you want all to be required. I also think it’s less surprising, but that’s subjective

seancorfield07:04:47

> when it’s much easier to write (s/select ::s []) when none are required than enumerate all the fields in a spec when you want all to be required FYI (s/select ::s [*]) is the "all fields required" case

drone15:04:32

cool, that’s a bit better

ikitommi01:04:15

I would put the closed-checking into select: Schemas would be open by default, but one can define select default to open or closed maps, which can be explicitly defined to different (sub)selects too.

ikitommi01:04:54

(s/select ::user [::id ::name ::address {::address [::street s/closed]} s/closed])

ikitommi01:04:38

(s/select ::user [::id ::name ::address {::address [::street]}] [s/closed]) ;;3rd arg will be merged into all submaps

wilkerlucio01:04:42

maybe moving to the select get back on the closed problem again, it will always be closed, I agree with @borkdude in this one, maybe the point to make this decision is s/valid?, this way you keep the possibility to stay open, but can do closed checks when it makes sense

ikitommi01:04:47

with s/valid? is would be all or nothing. In s/select, one could close it just partially. Not sure if there is a real life case for that thou.

didibus05:04:24

It seems you register which spec are to be validated as closed and open. So the s/valid could be partially closed as well, if you broke it up into named specs.

eggsyntax03:04:39

Independently wrote a response to the open/closed approach which pretty much echoes the same things other folks are saying above — global-stateful-ness bothers me, what about threads, etc. The need for closedness seems to me like it’s usually context-specific and scope-limited, so I was expecting a limited-scope approach. My first inclination would be to be able to do something like

(s/valid? (s/closed-variant ::bar) my-bar)
but I understand you’re trying to make sure it doesn’t become part of the spec language. If it’s about limiting it to the API, what about a separate s/closedly-valid? (or strictly-valid? or strictly-correct? or whatever)? And maybe an equivalent for s/explain. Or I like what I think @wilkerlucio is suggesting, making it available as an extra-arity arg to s/valid? (off by default so it doesn’t break existing code).

seancorfield03:04:21

@eggsyntax My first thought was s/valid-closed? but then you need a variant of each of the explain* functions and conform and probably several others... which gets ugly fast...

👍 4
eggsyntax20:04:52

What about maybe a separate s/closed? function that just verifies whether the data structure contains only the keys in the spec? It doesn’t seem like a big burden to do

(and (s/closed? ::spec x) (s/valid? ::spec x))

seancorfield03:04:56

@mrevelle This is not valid because ::f's value is not string? per your spec for ::f. That makes perfect sense to me.

metametadata07:04:36

If closed/open status becomes an option to some s/valid* fn then I don't see how the closed spec can be specified to validate args in defn-spec or something like that. So "closedness" should be attached to the spec instance. And I guess I'd also intuitively expect (s/close-specs ::s) to return a new instance of the spec. Even though I don't yet have cases where I need to open or close the already created spec, I'd expect it to return new values instead of "mutating" existing ones. Otherwise, there's a risk to have all the old problems with mutable data like threading, reasoning about code, etc.

👍 4
adam17:04:18

Should I be using spec for server side form validation or http://funcool.github.io/struct/latest/ or something else?

seancorfield17:04:49

@somedude314 We use Spec for that. We use it for API parameter validation, server-side form validation, and several other places in our production code.

adam17:04:58

@seancorfield do you use any helper libraries built on top the spec or just vanilla spec?

seancorfield17:04:00

Just vanilla spec. We wrote some code on top of explain-data to turn Spec failures into domain-specific error messages but that's it.

seancorfield17:04:31

And we mostly do it like this:

(let [params (s/conform ::my-spec (:params req))]
  (if (s/invalid? params)
    (report-error (s/explain-data ::myspec (:params req)))
    (process-form-data params)))
(pseudo-code)

adam17:04:10

Cool thanks. I will give it a try. This is the first time I am trying to validate stuff in Clojure so still unsure what is practical and what is not.

eggsyntax20:04:52

What about maybe a separate s/closed? function that just verifies whether the data structure contains only the keys in the spec? It doesn’t seem like a big burden to do

(and (s/closed? ::spec x) (s/valid? ::spec x))

ikitommi22:04:01

Did a demo in Clojure/North about closed specs, with spec.alpha, spec-tools, spell-spec and expound, using the new reitit error formatter: https://vimeo.com/331602826. The code calls spec-tools.spell/closed-spec function on a spec and gets a closed (spell-)spec back.

👍 20
💯 8