Fork me on GitHub
#ring-swagger
<
2018-04-30
>
shark8me07:04:05

Hi everyone! I send a JSON-encoded request, with no content-type headers. That gives an error

"errors":"(not (map? nil))","type":"compojure.api.exception/request-validation","coercion":"schema","value":null,"in":["request","body-params"]} 
. However, adding a content-type "application/json" (using Curl) returns a valid response. How can I have ring-swagger encode a request body to JSON, if no content-type is specified?

mgrbyte13:04:29

does anyone have an example of a compojure-api app that uses json as API format, spec coercion, and a spec that uses a keyword? Trying to grok how to translate between clojure and json when I have for example (s/def :my/thing keyword?). Current attempt uses (muuntaja/encode m "application/json data) which produces "my/thing", but obviously the coercion doesn't pick that up... do I need an extra transformation mw? :thinking_face:

ikitommi13:04:43

@mgrbyte Rich didn't mean specs to be extended, so you need to wrap specs into spec-tools.core/spec to it to work. e.g. (s/def :my/thing (st/spec keyword?).

ikitommi13:04:21

there is also spec-tools.spec with most predicates wrapped.

mgrbyte13:04:26

ah, i tried spec-tools.spec/keyword?

ikitommi13:04:51

oh, that should work too.

mgrbyte13:04:13

:thinking_face: perhaps because it's nested inside another (non-spec-tools-wrapped spec)

ikitommi13:04:10

it should be ok. Can you paste an example?

mgrbyte13:04:56

Have just done a WIP commit.. https://github.com/WormBase/names/tree/feature/json-api-format Currently working on one case: lein test :only integration.test-new-gene Data being sent via the test client api here: https://github.com/WormBase/names/blob/feature/json-api-format/test/wormbase/api_test_client.clj#L21 Specs are defined in src/wormbase/specs/{species,gene}.clj endpoints defined in src/wormbase/names/gene.clj Top level handler in src/wormbase/names/service.clj

mgrbyte13:04:13

Basically I had an EDN only api, but being asked to provide JSON too by my peers, as they don't want to use an EDN client library/are concerned about api adoption/interop

mgrbyte13:04:03

am sure I'm doing something dumb and it's not capi/spec-tools

ikitommi14:04:46

thanks for the detailed info, should be easy to find the cause from that. There is a Vappu just about to begin here in Finland, so can poke it later. 🎈

mgrbyte14:04:28

No worries, thank you 🙂

mgrbyte14:04:58

enjoy the vappu! (had to google 🎈 ) 😁

mgrbyte15:04:58

FWIW, here's a simpler reproduction:

(require '[clojure.spec.alpha :as s])
(require '[spec-tools.core :as stc])
(require '[spec-tools.spec :as sts])
(require '[compojure.api.coercion :as cac])
(require '[compojure.api.request :as car)
(s/def :species/id stc/keyword?)
(def c! #(cac/coerce-request! :species/id :body-params :body false false %))
(def m {:body-params {:species/id "species/c-elegans"}
             ::car/coercion :spec})
(c! m)
"fails" with:
ExceptionInfo Request validation failed: #compojure.api.coercion.core.CoercionError{:spec #Spec{:form (clojure.spec.alpha/keys :req [:species/id]), :type :map, :keys #{:species/id}, :keys/req #{:species/id}}, :problems ({:path [:species/id], :pred clojure.core/keyword?, :val "species/c-elegans", :via [:species/id], :in [:species/id]})}  clojure.core/ex-info (core.clj:4739)

mgrbyte15:04:33

which leads me to think that that keywords cannot be "round-tripped":

mgrbyte15:04:39

(m/decode mformats "application/json"
                                  "{\"species/id\": \"test/this\"}")

mgrbyte15:04:06

gives -> #:species{:id "test/this"}

mgrbyte15:04:20

:thinking_face:

ikitommi13:05:15

(ns user)

(require '[compojure.api.sweet :refer :all])
(require '[ring.util.http-response :refer :all])
(require '[clojure.spec.alpha :as s])
(require '[spec-tools.spec :as sts])

(s/def :species/id sts/keyword?)

(def app
  (api
    {:coercion :spec}
    (POST "/body" []
      :body-params [species/id :- :species/id]
      :return :species/id
      (prn id)
      (ok id))))

(->
  {:request-method :post
   :uri "/body"
   :body-params {:species/id "species/c-elegans"}
   :headers {"content-type" "application/json"}}
  (app)
  :body
  slurp)
; :species/c-elegans
; => "\"species/c-elegans\""

ikitommi13:05:42

It seems to be working correctly?

mgrbyte13:05:45

It's fine for decoding, not sure how to encode

mgrbyte13:05:39

i.e given JSON: {"species/id": "species/c-elegans"} want to turn it back into the clojure form {:species/id :species/c-elegans}

ikitommi13:05:24

ok, just a sec.

mgrbyte13:05:53

or else am looking for a pattern of how data is sent to the app in e.g a post request

ikitommi13:05:09

(ns user)

(require '[muuntaja.core :as m])
(require '[compojure.api.sweet :refer :all])
(require '[ring.util.http-response :refer :all])
(require '[clojure.spec.alpha :as s])
(require '[spec-tools.core :as st])
(require '[spec-tools.spec :as sts])

(s/def :species/id sts/keyword?)
(s/def :species/map (s/keys :req [:species/id]))

(def app
  (api
    {:coercion :spec}
    (POST "/body" []
      :body-params [species/id :- keyword?]
      :return :species/map
      (ok {:species/id id}))))

(->
  {:request-method :post
   :uri "/body"
   :body-params {:species/id "species/c-elegans"}
   :headers {"content-type" "application/json"}}
  (app)
  :body
  slurp
  (->> (m/decode (m/create) "application/json"))
  (as-> value
        (st/conform :species/map value st/json-conforming)))
; => #:species{:id :species/c-elegans}

ikitommi13:05:46

(ns user)

(require '[muuntaja.core :as m])
(require '[compojure.api.sweet :refer :all])
(require '[ring.util.http-response :refer :all])
(require '[clojure.spec.alpha :as s])
(require '[spec-tools.core :as st])
(require '[spec-tools.spec :as sts])

(s/def :species/id sts/keyword?)
(s/def :species/map (s/keys :req [:species/id]))

(def app
  (api
    {:coercion :spec}
    (POST "/body" []
      :body [body :species/map]
      :return :species/map
      (ok body))))

(->
  {:request-method :post
   :uri "/body"
   :body-params {:species/id "species/c-elegans"}
   :headers {"content-type" "application/json"}}
  (app)
  :body
  (as-> value
        (m/decode (m/create) "application/json" value)
        (st/conform :species/map value st/json-conforming)))
; => #:species{:id :species/c-elegans}

mgrbyte13:05:31

am just trying out those samples now

mgrbyte13:05:16

@U055NJ5CC ah, so I was missing the conforming step: (st/conform :species/map value st/json-conforming)))

mgrbyte13:05:53

also, I've not been using spec-tools for conforming thus far (using clojure.spec directly)

mgrbyte13:05:33

thank you v. much; so I should probably change all my handlers to manually conform using st/json-conforming thinking_face will think about how to incorporate this.

mgrbyte13:05:20

@U055NJ5CC Sorry, I'm quite confused now. Running the last sample you posted, I can see it works as I'd expect. My app must be doing something different/have a bug somewhere that I can't see. thanks again for your help, I'll keep looking...

ikitommi14:05:23

np. hope tou get it working

mgrbyte14:05:39

@U055NJ5CC found the error. Thought json-conforming was the default (and it is) My mistake was trivial; was using :body instead of :body-params in my resource definition. (I'll admit to be not entirely clear on the diference between using the two)

mgrbyte14:05:03

/me goes to grep the docs

ikitommi13:04:55

metosin/c2 on github could be turned into a sample with keywords.

ikitommi13:05:46

(ns user)

(require '[muuntaja.core :as m])
(require '[compojure.api.sweet :refer :all])
(require '[ring.util.http-response :refer :all])
(require '[clojure.spec.alpha :as s])
(require '[spec-tools.core :as st])
(require '[spec-tools.spec :as sts])

(s/def :species/id sts/keyword?)
(s/def :species/map (s/keys :req [:species/id]))

(def app
  (api
    {:coercion :spec}
    (POST "/body" []
      :body [body :species/map]
      :return :species/map
      (ok body))))

(->
  {:request-method :post
   :uri "/body"
   :body-params {:species/id "species/c-elegans"}
   :headers {"content-type" "application/json"}}
  (app)
  :body
  (as-> value
        (m/decode (m/create) "application/json" value)
        (st/conform :species/map value st/json-conforming)))
; => #:species{:id :species/c-elegans}