metosin / compojure-api

Sweet web apis with Compojure & Swagger
http://metosin.github.io/compojure-api/doc/
Eclipse Public License 1.0
1.11k stars 149 forks source link

Body coercion and validation with spec #397

Closed piotr-yuxuan closed 5 years ago

piotr-yuxuan commented 5 years ago

Library Version(s)

[cheshire "5.7.1"]
[org.clojure/clojure "1.9.0"]
[metosin/compojure-api "2.0.0-alpha26"]
[ring/ring-mock "0.3.2"]
[ring "1.6.1"]
[metosin/spec-tools "0.7.2"]

Observation

I'm trying to coerce and validate request following these examples:

https://github.com/metosin/c2/blob/master/src/c2/spec.clj https://www.metosin.fi/blog/clojure-spec-with-ring-and-swagger/

However I haven't found so far how to get the request body in the handler.

Complete code is available in this minimum working example repository.

(ns minimum-example-code.core
  (:require [cheshire.core :as cheshire]
            [clojure.spec.alpha :as s]
            [compojure.api.sweet :refer [context GET POST resource api]]
            [ring.mock.request :as mock]
            [ring.util.http-response :refer [ok]]
            [spec-tools.spec :as spec])
  (:gen-class))

(defn -main [& args] nil)

(s/def ::a spec/int?)

(def app
  (api {:coercion :spec}
       (context "/application/path" []
                (POST "/endpoint" []
                      :body [numbers (s/keys :req-un [::a])]
                      :return spec/map?
                      (ok {:other-numbers numbers})))))

(-> (assoc (mock/request :post "/application/path/endpoint" {:a 7})
      :content-type "application/json")
    (app)
    :body
    (slurp)
    (cheshire/parse-string true))

(comment
  => {:spec "(spec-tools.core/spec {:spec (clojure.spec.alpha/keys :req-un [:minimum-example-code.core/a]), :type :map, :keys #{:a}, :keys/req #{:a}})",
      :problems [{:path [], :pred "map?", :val nil, :via [], :in []}],
      :type "compojure.api.exception/request-validation",
      :coercion "spec",
      :value nil,
      :in ["request" "body-params"]})

(def app2
  (api {:coercion :spec}
       (context "/application/path/endpoint" []
                (resource
                  {:coercion :spec
                   :post {:parameters {;; :body isn't called, only :body-params
                                       :body-params (fn always-true [body]
                                                      ;; here body is nil
                                                      true)}
                          :responses {200 {:schema spec/map?}}
                          :handler (fn [{{:keys [a]} :body}]
                                     ;; Here, a is nil
                                     (ok {:other-numbers ((fnil inc 0) a)}))}}))))

(-> (assoc (mock/request :post "/application/path/endpoint" {:a 7})
      :content-type "application/json")
    (app2)
    :body
    slurp
    (cheshire/parse-string true))

(comment
  => {:other-numbers 1})

(def app3
  (api {:coercion :spec}
       (context "/application/path/endpoint" []
                (resource
                  {:coercion :spec
                   :post {:parameters {:body-params spec/any?}
                          :responses {200 {:schema spec/map?}}
                          :handler (fn [{{:keys [a]} :body}]
                                     (ok {:other-numbers ((fnil inc 0) a)}))}}))))

(-> (assoc (mock/request :post "/application/path/endpoint" {:a 7})
      :content-type "application/json")
    (app3)
    :body
    slurp
    (cheshire/parse-string true))

(comment
  => {:other-numbers 1})

Problem?

It may be a bug, or another example would be much appreciated.

ikitommi commented 5 years ago

As responded in Slack, you need to write json strings manually.

ikitommi commented 5 years ago
(mock/request :post "/application/path/endpoint" {:a 7} ...)

=>

(mock/request :post "/application/path/endpoint" (cheshire/generate-string {:a 7}) ...)