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
nmkip 2020-06-04T02:20:17.098700Z

I have a map that has this shape {"account" {"active" true "balance" 30}} , what's the best way to spec the "account" key? I've tried these and I'm using the first one.

(s/def ::account-key (s/with-gen (s/and string?
                                        #(= "account" %))
                       #(gen/return "account"))) 
(s/def ::account-key (s/with-gen (s/and string?
                                        #{"account"})
                       #(gen/return "account"))) 

seancorfield 2020-06-04T02:20:59.099400Z

You can use #{"account"} to spec a known set of string values.

seancorfield 2020-06-04T02:21:16.099800Z

(and that will also generate correctly)

nmkip 2020-06-04T02:21:35.100300Z

just #{"account"} that makes sense

seancorfield 2020-06-04T02:21:54.100900Z

(s/def ::account-key #{"account"})

nmkip 2020-06-04T02:21:57.101100Z

yes

nmkip 2020-06-04T02:22:30.101700Z

great, thanks! ๐Ÿ™‚ I'll use that instead, much shorter

nmkip 2020-06-04T02:23:25.102200Z

I was being redundant here:

(s/def ::account-key (s/with-gen (s/and string?
                                        #{"account"})
                       #(gen/return "account"))) 

1
seancorfield 2020-06-04T02:24:00.102700Z

Inventive... but certainly "overkill" ๐Ÿ™‚

nmkip 2020-06-04T02:24:24.103100Z

indeed

dev.4openID 2020-06-04T13:05:48.110Z

Learner. Maybe a dumb question. I have built up a detailed spec using s/def s/valid? s/conform. It works fine. Following best practices all my keywords use :: (e.g. ::timestamp). However, my data covered JSON to map, all keywords are single colon (e.g. :timestamps) When I construct the data with two colons the valid? and conform has no problem whereas the mapped data with single colon fails. I was understanding the :: indicates the keyword in this namesapace so having :: in my spec vs. the single : in may data should not matter. Any clarifications on this? :thinking_face: <script src="https://gist.github.com/dev4openid/1becc5be61b764e330edc82d16f99eb6.js"></script>

frankitox 2020-06-04T13:12:38.114Z

The difference is that :: expands to the current namespace. So if you're working in a ns called my.test then ::timestamp expands to :my.test/timestamp. The spec fails with :timestamp because it's also expecting the namespace.

aisamu 2020-06-04T13:12:40.114100Z

The conversion from JSON will probably give you unqualified keywords, since no such concept exists there. You have to either qualify your keywords on a separate post-processing step, or make a spec for the payload that takes unqualified keys (e.g. :req-un instead of :req)

dev.4openID 2020-06-04T13:23:06.114500Z

@franquito Thanks for the explanation. But..... why is the :timestamp not found in the namespace or is it global? If global than how do I relate it to my incoming data - as it were. Because the spec is not of use right now. Obviously I do not see the connection

dev.4openID 2020-06-04T13:24:05.114800Z

So that is the purpose of req-un then? Does that make the :keyword global?

frankitox 2020-06-04T13:30:56.115100Z

Maybe you are mixing concepts. Keywords are just keywords, they can be qualified (`:test/timestamp`) or unqualified (`:timestamp`). :: It's a shortcut to write a qualified keyword with the same namespace as the current file.

aisamu 2020-06-04T13:31:30.115300Z

Keywords can be qualified or unqualified. Unqualified can be thought of as "global", but it's better to stick with "lacking a namespace". When you type in ::timestamp, Clojure uses the current namespace and creates a qualified keyword. req-un means that the spec will match unqualified keys to the qualified keys, comparing just the name (i.e. discarding the namespace)

dev.4openID 2020-06-04T13:32:47.115600Z

So in a more complex map structure, where I have several collections do I have to req-un for all definitions or only at the top level and it is inferred?

dev.4openID 2020-06-04T13:33:17.115800Z

'Cos I req-un all my defs and now it does not work

aisamu 2020-06-04T13:42:05.116Z

Yes, you'd have to req-un all of them. Try breaking it into smaller pieces to find where it's failing

dev.4openID 2020-06-04T13:42:33.116300Z

here is the gist <script src="https://gist.github.com/dev4openid/1becc5be61b764e330edc82d16f99eb6.js"></script>

dev.4openID 2020-06-04T13:42:37.116500Z

It is detailled

aisamu 2020-06-04T13:44:12.116700Z

The spec for ::loca seems a bit off

dev.4openID 2020-06-04T13:45:08.116900Z

seems to work.... what do you mean?

dev.4openID 2020-06-04T13:46:08.117100Z

If it is all :req for keywords exampl2 works fine and conforms. So I am confused here?

aisamu 2020-06-04T13:46:10.117300Z

Unless this is spec2, I don't think this is a valid definition: (s/def ::loca (s/keys :req-un {::addr [::country]}))

aisamu 2020-06-04T13:46:53.117500Z

IIRC, req & req-un take a vector of keys, not a map

aisamu 2020-06-04T13:47:42.117700Z

Might have worked by accident, or because you had a correct definition earlier that was stored in the registry

dev.4openID 2020-06-04T13:48:00.117900Z

would (s/def ::loca :req {::addr [::country]}) do it? no it does not

dev.4openID 2020-06-04T13:49:55.118200Z

Surely the country spec forces a s/keys?

aisamu 2020-06-04T13:52:06.118400Z

I've never seen that syntax (feeding a map to :req). Does it work on a simple case?

dev.4openID 2020-06-04T13:53:40.118800Z

2 rows above and it works -> (s/def ::addr (s/keys :req [::country])) (s/conform ::addr {::country "USA"}) ;; => #:myproj.trial.spec{:country "USA"}

dev.4openID 2020-06-04T13:53:59.119Z

It is effectively a map of map, no?

dev.4openID 2020-06-04T13:55:33.119300Z

and :loca is the map of the above. But if I do not have s/keys it seems now not to work

aisamu 2020-06-04T13:56:29.119500Z

These two are not the same:

(s/def ::addr (s/keys :req-un [::country]))
(s/def ::loca (s/keys :req-un {::addr [::country]}))
You're feeding a vector to the first req, but a map to the second

dev.4openID 2020-06-04T13:57:42.119700Z

Agreed but I tried ::loca {:: addr {::country and it will not accept it

dev.4openID 2020-06-04T14:06:02.120500Z

@aisamu BTW what do you mean as spec2? I am using clojure.spec.alpha latest version

dev.4openID 2020-06-04T15:00:45.120900Z

@franquito I agree with you, how does one relate the one to the other? I changed my spec to have :reg-un at the highest level and it fails spec is at <script src="https://gist.github.com/dev4openid/1becc5be61b764e330edc82d16f99eb6.js"></script>

frankitox 2020-06-04T15:06:46.121200Z

Try first working with simpler examples, try to create a spec for a map with unqualified keys

dev.4openID 2020-06-04T15:07:35.121400Z

If you look at the gist that Is what I am doing? No?

dev.4openID 2020-06-04T15:10:11.121600Z

When all the keys are NOT reg-un then all my test cases pass

frankitox 2020-06-04T15:10:31.121800Z

Well, https://gist.github.com/dev4openid/1becc5be61b764e330edc82d16f99eb6#file-myproj-trial-spc-clj-L19 you need to change it to (s/def ::loca (s/keys :req-un [::addr]))

dev.4openID 2020-06-04T15:10:41.122Z

and so my spec seems OK ??

dev.4openID 2020-06-04T15:11:24.122200Z

OK how? I know it is indicating a map but if I do not use s/keys it fails

dev.4openID 2020-06-04T15:11:55.122400Z

https://app.slack.com/team/UEGT2541Jย ย [2:57 PM] Agreed but I tried ::loca {:: addr {::countryย ย  and it will not accept

dev.4openID 2020-06-04T15:12:18.122600Z

Even though I initially figured it wasa map of map of map

dev.4openID 2020-06-04T15:14:51.122800Z

Oh oh!!!! I see it now!

frankitox 2020-06-04T15:15:25.123Z

I see several places you use :req-un instead of :req, which is the correct way for conforming/validating these examples

frankitox 2020-06-04T15:16:43.123200Z

As I said, try creating a simpler spec for a non nested map and then try validating maps

dev.4openID 2020-06-04T15:17:52.123400Z

Ok. But how do I use the spec against the data map (ex JSON) which is :keyword and not ::timeline (IYKWIM)

frankitox 2020-06-04T15:20:26.123600Z

Here's an example for a qualified map

(ns myproj.trial.spec)

(s/def ::country string?)
(s/def ::street string?)

(s/def ::addr (s/keys :req [::country ::street]))

(s/valid? ::addr {::country "USA"
                  ::street "A street"})
;; =&gt; true

(s/valid? ::addr {:country "USA"
                  :street "A street"})
;; =&gt; false

(s/valid? ::addr {:myproj.trial.spec/country "USA"
                  :myproj.trial.spec/street "A street"})
;; =&gt; true

dev.4openID 2020-06-04T15:23:30.123800Z

yes, without changing the above, how do you apply it to a map ex JSON) {:addr {:country "USA :street "A street"}}

frankitox 2020-06-04T15:23:42.124Z

As aisamu said before, if you are working with JSON you loose the namespace of the keywords, so you probably want to use :req-un in your specs.

(ns myproj.trial.spec)

(s/def ::country string?)
(s/def ::street string?)

(s/def ::addr (s/keys :req-un [::country ::street]))

(s/valid? ::addr {:country "USA"
                  :street "A street"})
;; =&gt; true

dev.4openID 2020-06-04T15:24:02.124200Z

I was advise to use :req-un to allow for unqualif namesapces

frankitox 2020-06-04T15:25:18.124400Z

Right, but then when you use valid? or conform you also have to use unqualified keywords for the map https://gist.github.com/dev4openid/1becc5be61b764e330edc82d16f99eb6#file-myproj-trial-spc-clj-L69-L81

dev.4openID 2020-06-04T15:40:08.125Z

(s/def ::country string?) (s/def ::addr (s/keys :req-un [::country])) (s/def ::loca (s/keys :req-un [::addr ::country ])) (s/conform ::country "USA") ;; => "USA" (s/conform ::addr {:country "USA"}) ;; => {:country "USA"} (s/conform ::loca {:addr {:country "USA"}}) ;; => #:myproj.trial.spec{:addr #:myproj.trial.spec{:country "USA"}} (s/explain-str ::loca {:addr {:country "USA"}}) ;; => "{:addr {:country \"USA\"}} - failed: (contains? % trial.spec/addr) spec: :trial.spec/loca\n{:addr {:country \"USA\"}} - failed: (contains? % trail.spec/country) spec: :parceltrax.tracking.spec/loca\n" ??why this??

dev.4openID 2020-06-04T15:47:32.125300Z

I updated the gist <script src="https://gist.github.com/dev4openid/1becc5be61b764e330edc82d16f99eb6.js"></script> If you look at line 16 When I look sat it it is a map of map

frankitox 2020-06-04T15:53:53.125800Z

Try conforming this

(s/conform ::loca {:addr {:country "USA"}
                   :country "OTHER"})

dev.4openID 2020-06-04T15:55:52.126Z

Yes, in theory that will work, however the data looks like this :pieceID ["ABC1" "DEF2" "GHI3"]}]}) (def exampl2 {::abc [{::loca {::addr {::country "USA"}} ::dets {::some{::timestamp "2020-04-09T15:27:00" ::Url "https://mywebpage.com/answer"

dev.4openID 2020-06-04T15:57:02.126300Z

Look at line 1 of the gist

dev.4openID 2020-06-04T15:57:29.126500Z

so loca is a map of map of map

dev.4openID 2020-06-04T15:58:54.126700Z

The trouble I a,mm having is the compounded map that does not seems to play nicely

dev.4openID 2020-06-04T16:06:37.126900Z

OK - it does conform to the definition Thx.  However it is not as per line 1 - which is how the data is meant to be.   So extracting from line 1:  :loca {:addr {:country "USA"}}

frankitox 2020-06-04T16:09:19.127100Z

Yes, you need to modify ::loca so it looks like this (s/def ::loca (s/keys :req-un [::addr]))

dev.4openID 2020-06-04T16:09:32.127300Z

@franquito OK !!!! I think I have it ! I must take out the ::country in the s/def as the ::country is already defined (s/def ::loca (s/keys :req-un [::addr]))

dev.4openID 2020-06-04T16:09:54.127500Z

I think that is what you were trying to get me to see

frankitox 2020-06-04T16:10:19.127700Z

Yes, exactly! ๐Ÿ™‚

dev.4openID 2020-06-04T16:10:38.127900Z

Appreciated

dev.4openID 2020-06-04T16:26:50.128100Z

@franquito Thanks for your patience and help. I got it now!! my complete script now works! Kudos for you ๐Ÿ™‚

๐ŸŽ‰ 1
2020-06-04T19:30:09.130400Z

Hi fellow clojurians, is anyone aware of a way to parse a swagger json to valid specs that can be used to generate test data?

dev.4openID 2020-06-09T11:04:01.217Z

This is what I did to learn spec. From this point on one can gen data. What you see is a map converted from a JSON structure. Now this is not a "take a swagger spec and convert" but is the basis of how to clojure spec based on a swagger that I did.

(def exampl {:abc [{:loca {:addr {:country "USA"}}
                    :dets {:some{:timestamp "2020-04-09T15:27:00"
                                 :Url "<https://mywebpage.com/answer>"
                                 :prod {:name "this product"}}}
                    :totalNumber 399
                    :pieceID ["ABC1" "DEF2" "GHI3"]}]})


(s/def ::country string?)
(s/def ::addr (s/keys :req-un [::country]))
(s/def ::loca (s/keys :req-un [::addr]))

(s/conform ::country "USA") ;; =&gt; "USA"
(s/conform ::addr {:country "USA"}) ;; =&gt; {:country "USA"}
(s/conform ::loca  {:addr {:country "USA"}}) ;; =&gt; {:addr {:country "USA"}}
(s/valid? ::loca  {:addr {:country "USA"}})  ;; =&gt; true
(s/check-asserts?)
(s/check-asserts true)                       ;; by default false!!
(s/assert ::loca  {:addr {:country "USA"}})  ;; =&gt; {:addr {:country "USA"}}
(s/assert ::loca  {:addr {:countryx "SA"}})  ;; note the exception error BUT look at the repl

(s/def ::timestamp (and not nil?
                        string?))     ;; this needs work for T-time issue
(s/def ::Url string?)                 ;; need to validate URLs - technique
(s/def ::name string?)
(s/def ::prod (s/keys :req-un [::name]))
(s/def ::some (s/keys :req-un [::timestamp ::Url ::prod]))
(s/def ::dets (s/keys :req-un [::some]))

(s/conform ::timestamp "2020-04-09T15:27:00")
(s/conform ::Url "<https://mywebpage.com/answer>")
(s/conform ::name "this product")
(s/conform ::prod {:name "this product"})
(s/conform ::some {:timestamp "2020-04-09T15:27:00"
                   :Url "<https://mywebpage.com/answer>"
                   :prod {:name "this product"}})
(s/conform ::dets {:some {:timestamp "2020-04-09T15:27:00"
                           :Url "<https://mywebpage.com/answer>"
                           :prod {:name "this product"}}})

(s/def ::totalNumber (and pos? int?))
(s/def ::pieceID (and (s/coll-of string?)
                      vector?))
(s/conform ::totalNumber 399) ;; =&gt; 399
(s/conform ::pieceID ["ABC1" "DEF2"]) ;; =&gt; ["ABC1" "DEF2"]

(s/def ::abc (s/coll-of (s/keys :req-un [::loca ::dets ::totalNumber ::pieceID])))
(s/conform ::abc [{:loca {:addr {:country "USA"}}
                   :dets {:some{:timestamp "2020-04-09T15:27:00"
                                :Url "<https://mywebpage.com/answer>"
                                :prod {:name "this product"}}}
                   :totalNumber 399
                   :pieceID ["ABC1" "DEF2" "GHI3"]}])

;; now ex is an map of a single instance)
ex
;; =&gt; {:abc [{:loca {:addr {:country "USA"}}, :dets {:some {:timestamp "2020-04-09T15:27:00", :Url "<https://mywebpage.com/answer>", :prod {:name "this product"}}}, :totalNumber 399, :pieceID ["ABC1" "DEF2" "GHI3"]}]}

(s/def ::an_instance (s/keys :req-un [::abc]))
(s/conform ::an_instance {:abc [{:loca {:addr {:country "USA"}}
                                 :dets {:some{:timestamp "2020-04-09T15:27:00"
                                              :Url "<https://mywebpage.com/answer>"
                                              :prod {:name "this product"}}}
                                 :totalNumber 399
                                 :pieceID ["ABC1" "DEF2" "GHI3"]}]})

(s/conform ::an_instance exampl)
(s/valid? ::an_instance exampl)
(s/explain-str ::an_instance exampl)
Also look at https://www.youtube.com/watch?v=f2hNQdS2VxQ to watch a conversion - the nearest I have found. Good luck with this ๐Ÿ˜‰

2020-06-09T14:19:52.217500Z

hey @dev.4openid thanks for joining in. actually the challenge is not to create a clojure map from the json and then writing the spec. but parsing the json into specs . the difference being that I don't want to write them manually. but you are absolutely right, the chain is: json -> clojure-map -> transform clojure map entries to spec entries and compose them to upper level spec. then we can use the gen functions to create test data

2020-06-09T14:21:53.217700Z

if we have a working parser for that, we can just call the endpoint on webservices, slurp the swagger json and create specs for data models

2020-06-09T14:22:11.217900Z

which is a handy little tool ๐Ÿ˜„

2020-06-09T14:26:24.218100Z

I'm not an expert for specs, but searching a swagger yaml/ json for the definitions

2020-06-09T14:27:17.218300Z

and then using the type information there and maping those and the field names to specs sounds doable ๐Ÿ˜„

2020-06-09T14:27:21.218500Z

definitions:
  Order:
    type: "object"
    properties:
      id:
        type: "integer"
        format: "int64"
      petId:
        type: "integer"
        format: "int64"
      quantity:
        type: "integer"
        format: "int32"
      shipDate:
        type: "string"
        format: "date-time"
      status:
        type: "string"
        description: "Order Status"
        enum:
        - "placed"
        - "approved"
        - "delivered"
      complete:
        type: "boolean"
        default: false
    xml:
      name: "Order"

2020-06-09T14:27:51.218700Z

i like structured data ๐Ÿ˜„

2020-06-09T14:28:40.218900Z

I'll work on that a bit an will put a link to a gist here if I can create something useful

flyboarder 2020-06-09T17:44:15.222600Z

@dev-hartmann im really interested in this, we just rolled out swagger to our APIโ€™s both internally and externally, I would love to be able to have our apps handle specs for us

2020-06-09T17:46:50.222800Z

will keep you posted too.

2020-06-09T17:47:03.223Z

I think it's easier than I thought.

2020-06-09T17:47:17.223200Z

.. I mean until I hit the brick wall ๐Ÿ˜„

2020-06-09T17:55:18.223400Z

at least the basics. translating the conditions on properties can take some time ๐Ÿ˜„

dev.4openID 2020-06-09T20:29:34.226100Z

OK, similar to what you are stsing then: look st the library serene - It autoigenerates the specs for REST and graphql APIs.

dev.4openID 2020-06-09T20:30:43.226300Z

I'll shut up now ๐Ÿ™‚

2020-06-09T22:05:27.229200Z

no, please don't. never heard of that library and it looks like they are doing the same thing, but for a different format. :thumbsup: