ring-swagger & compojure-api
shark8me 2018-04-30T07:18:05.000066Z

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?

mgrbyte 2018-04-30T13:06:29.000020Z

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:

ikitommi 2018-04-30T13:10:43.000305Z

@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?).

ikitommi 2018-04-30T13:11:21.000261Z

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

mgrbyte 2018-04-30T13:11:26.000024Z

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

ikitommi 2018-04-30T13:11:51.000205Z

oh, that should work too.

mgrbyte 2018-04-30T13:12:13.000240Z

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

ikitommi 2018-04-30T13:26:10.000241Z

it should be ok. Can you paste an example?

ikitommi 2018-05-01T13:17:15.000245Z

(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
    {: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"}}
; :species/c-elegans
; => "\"species/c-elegans\""

ikitommi 2018-05-01T13:17:20.000280Z

ping @mgrbyte

ikitommi 2018-05-01T13:17:42.000119Z

It seems to be working correctly?

mgrbyte 2018-05-01T13:21:45.000323Z

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

mgrbyte 2018-05-01T13:22:39.000434Z

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

ikitommi 2018-05-01T13:23:24.000067Z

ok, just a sec.

mgrbyte 2018-05-01T13:23:53.000361Z

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

ikitommi 2018-05-01T13:27:09.000027Z

(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
    {: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"}}
  (->> (m/decode (m/create) "application/json"))
  (as-> value
        (st/conform :species/map value st/json-conforming)))
; => #:species{:id :species/c-elegans}

ikitommi 2018-05-01T13:29:39.000063Z


mgrbyte 2018-05-01T13:31:31.000377Z

am just trying out those samples now

mgrbyte 2018-05-01T13:39:16.000191Z

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

mgrbyte 2018-05-01T13:39:53.000472Z

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

mgrbyte 2018-05-01T13:40:33.000106Z

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.

mgrbyte 2018-05-01T13:51:20.000217Z

@ikitommi 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...

ikitommi 2018-05-01T14:14:23.000078Z

np. hope tou get it working

mgrbyte 2018-05-01T14:20:39.000296Z

@ikitommi 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)

mgrbyte 2018-05-01T14:22:03.000209Z

/me goes to grep the docs

ikitommi 2018-04-30T13:29:55.000587Z

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

mgrbyte 2018-04-30T13:42:56.000153Z

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

mgrbyte 2018-04-30T13:44:13.000280Z

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

mgrbyte 2018-04-30T13:45:03.000751Z

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

ikitommi 2018-04-30T14:21:46.000599Z

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. 🎈

mgrbyte 2018-04-30T14:28:28.000744Z

No worries, thank you 🙂

mgrbyte 2018-04-30T14:28:58.000066Z

enjoy the vappu! (had to google 🎈 ) 😁

mgrbyte 2018-04-30T15:10:58.000446Z

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)

mgrbyte 2018-04-30T15:11:33.000660Z

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

mgrbyte 2018-04-30T15:11:39.000558Z

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

mgrbyte 2018-04-30T15:12:06.000744Z

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

mgrbyte 2018-04-30T15:12:20.000569Z