Repository: oliyh/martian
Pull request #211 (2024-10-25 18:07:41)

Form encoder


Patch:
diff --git a/core/src/martian/encoders.cljc b/core/src/martian/encoders.cljc
index 6baa3c7..b055e85 100644
--- a/core/src/martian/encoders.cljc
+++ b/core/src/martian/encoders.cljc
@@ -2,10 +2,13 @@
   (:require [clojure.string :as string]
             [flatland.ordered.map :as linked]
             [cognitect.transit :as transit]
+            [clojure.walk :refer [keywordize-keys]]
             #?(:clj [clojure.edn :as edn]
                :cljs [cljs.reader :as edn])
             #?(:clj [cheshire.core :as json])
-            #?(:clj [clojure.java.io :as io]))
+            #?(:clj [clojure.java.io :as io])
+            #?@(:bb []
+                :clj [[ring.util.codec :as codec]]))
   #?(:clj (:import [java.io ByteArrayInputStream ByteArrayOutputStream])))
 
 (defn transit-decode [body type]
@@ -32,25 +35,49 @@
      (if-let [v (if-not (string/blank? body) (js/JSON.parse body))]
        (js->clj v :keywordize-keys key-fn))))
 
+#?(:cljs
+   (defn- form-encode [body]
+     (str (js/URLSearchParams. (clj->js body)))))
+
+#?(:cljs
+   (defn- form-decode [body]
+     (let [params (js/URLSearchParams. body)]
+       (reduce (fn [acc k]
+                 (let [v (.getAll params k)]
+                   (assoc acc (keyword k) (if (= 1 (count v))
+                                            (first v)
+                                            (vec v)))))
+               {}
+               (.keys params)))))
+
 (defn default-encoders
   ([] (default-encoders keyword))
   ([key-fn]
    (merge
     #?(:bb
-       {"application/transit+json"    {:encode #(transit-encode % :json)
-                                       :decode #(transit-decode % :json)}})
+       {"application/transit+json" {:encode #(transit-encode % :json)
+                                    :decode #(transit-decode % :json)}})
     #?(:clj
        {"application/transit+msgpack" {:encode #(transit-encode % :msgpack)
                                        :decode #(transit-decode % :msgpack)
-                                       :as :byte-array}
+                                       :as     :byte-array}
         "application/transit+json"    {:encode #(transit-encode % :json)
                                        :decode #(transit-decode (.getBytes ^String %) :json)}})
     #?(:cljs
-       {"application/transit+json"    {:encode #(transit-encode % :json)
-                                       :decode #(transit-decode % :json)}})
+       {"application/transit+json" {:encode #(transit-encode % :json)
+                                    :decode #(transit-decode % :json)}})
     (linked/ordered-map
 
      "application/edn"             {:encode pr-str
                                     :decode edn/read-string}
      "application/json"            {:encode json-encode
-                                    :decode #(json-decode % key-fn)}))))
+                                    :decode #(json-decode % key-fn)})
+
+    #?(:bb nil
+
+       :clj
+       {"application/x-www-form-urlencoded" {:encode codec/form-encode
+                                             :decode (comp keywordize-keys codec/form-decode)}}
+       :cljs
+       {"application/x-www-form-urlencoded" {:encode form-encode
+                                             :decode form-decode}}))))


Test patch:
diff --git a/core/test/martian/interceptors_test.cljc b/core/test/martian/interceptors_test.cljc
index 1b4f0aa..188f6dc 100644
--- a/core/test/martian/interceptors_test.cljc
+++ b/core/test/martian/interceptors_test.cljc
@@ -3,7 +3,6 @@
             [martian.encoders :as encoders]
             [tripod.context :as tc]
             [schema.core :as s]
-            [schema-tools.core :as st]
             #?(:clj [clojure.test :refer [deftest is testing]]
                :cljs [cljs.test :refer-macros [deftest testing is]])
             #?(:clj [martian.test-utils :as tu])))
@@ -12,42 +11,57 @@
    (def Throwable js/Error))
 
 (deftest encode-body-test
-  (let [i i/default-encode-body
-        body {:the {:wheels ["on" "the" "bus"]}
-              :go {:round {:and "round"}}}]
-    (testing "json"
-      (is (= {:body (encoders/json-encode body)
-              :headers {"Content-Type" "application/json"}}
-             (:request ((:enter i) {:request {:body body}
-                                    :handler {:consumes ["application/json"]}})))))
-
-    (testing "edn"
-      (is (= {:body (pr-str body)
-              :headers {"Content-Type" "application/edn"}}
-             (:request ((:enter i) {:request {:body body}
-                                    :handler {:consumes ["application/edn"]}})))))
-
-    (testing "transit"
-      (testing "+json"
-        (is (= {:body body
-                :headers {"Content-Type" "application/transit+json"}}
-               (-> ((:enter i) {:request {:body body}
-                                :handler {:consumes ["application/transit+json"]}})
-                   :request
-                   (update :body #?(:clj (comp #(encoders/transit-decode % :json)
-                                               tu/input-stream->byte-array)
-                                    :cljs #(encoders/transit-decode % :json)))))))
-
-      #?(:bb nil
-         :clj
-         (testing "+msgpack"
-           (is (= {:body body
-                   :headers {"Content-Type" "application/transit+msgpack"}}
-                  (-> ((:enter i) {:request {:body body}
-                                   :handler {:consumes ["application/transit+msgpack"]}})
-                      :request
-                      (update :body (comp #(encoders/transit-decode % :msgpack)
-                                          tu/input-stream->byte-array))))))))))
+  (let [i i/default-encode-body]
+
+    #?(:bb nil
+       :default
+       (testing "simple encoders"
+	 (let [body {:the "wheels"
+                     :on "the"
+                     :bus ["go" "round" "and" "round"]}]
+           (testing "form"
+             (is (= {:body #?(:clj "the=wheels&on=the&bus=go&bus=round&bus=and&bus=round"
+                              :cljs "the=wheels&on=the&bus=go%2Cround%2Cand%2Cround")
+                     :headers {"Content-Type" "application/x-www-form-urlencoded"}}
+                    (:request ((:enter i) {:request {:body body}
+                                           :handler {:consumes ["application/x-www-form-urlencoded"]}}))))))))
+
+    (testing "complex encoders"
+      (let [body {:the {:wheels ["on" "the" "bus"]}
+                  :go {:round {:and "round"}}}]
+        (testing "json"
+          (is (= {:body (encoders/json-encode body)
+                  :headers {"Content-Type" "application/json"}}
+                 (:request ((:enter i) {:request {:body body}
+                                        :handler {:consumes ["application/json"]}})))))
+
+        (testing "edn"
+          (is (= {:body (pr-str body)
+                  :headers {"Content-Type" "application/edn"}}
+                 (:request ((:enter i) {:request {:body body}
+                                        :handler {:consumes ["application/edn"]}})))))
+
+        (testing "transit"
+          (testing "+json"
+            (is (= {:body body
+                    :headers {"Content-Type" "application/transit+json"}}
+                   (-> ((:enter i) {:request {:body body}
+                                    :handler {:consumes ["application/transit+json"]}})
+                       :request
+                       (update :body #?(:clj (comp #(encoders/transit-decode % :json)
+                                                   tu/input-stream->byte-array)
+                                        :cljs #(encoders/transit-decode % :json)))))))
+
+          #?(:bb nil
+             :clj
+             (testing "+msgpack"
+               (is (= {:body body
+                       :headers {"Content-Type" "application/transit+msgpack"}}
+                      (-> ((:enter i) {:request {:body body}
+                                       :handler {:consumes ["application/transit+msgpack"]}})
+                          :request
+                          (update :body (comp #(encoders/transit-decode % :msgpack)
+                                              tu/input-stream->byte-array))))))))))))
 
 (defn- stub-response [content-type body]
   {:name ::stub-response
@@ -56,35 +70,47 @@
                                   :headers {:content-type content-type}}))})
 
 (deftest coerce-response-test
-  (let [i i/default-coerce-response
-        body {:the {:wheels ["on" "the" "bus"]}
-              :go {:round {:and "round"}}}]
-
-    (testing "json"
-      (let [ctx (tc/enqueue* {} [i (stub-response "application/json" (encoders/json-encode body))])]
-        (is (= body
-               (-> (tc/execute ctx) :response :body)))))
-
-    (testing "edn"
-      (let [ctx (tc/enqueue* {} [i (stub-response "application/edn" (pr-str body))])]
-        (is (= body
-               (-> (tc/execute ctx) :response :body)))))
-
-    (testing "transit"
-      (testing "+json"
-        (let [ctx (tc/enqueue* {} [i (stub-response "application/transit+json"
-                                                    #?(:clj (slurp (encoders/transit-encode body :json))
-                                                       :cljs (encoders/transit-encode body :json)))])]
-          (is (= body
-                 (-> (tc/execute ctx) :response :body)))))
-
-      #?(:bb nil
-         :clj
-         (testing "+msgpack"
-           (let [ctx (tc/enqueue* {} [i (stub-response "application/transit+msgpack"
-                                                       (tu/input-stream->byte-array (encoders/transit-encode body :msgpack)))])]
-             (is (= body
-                    (-> (tc/execute ctx) :response :body)))))))))
+  (let [i i/default-coerce-response]
+
+    #?(:bb nil
+       :default
+       (testing "simple decoders"
+         (let [body {:the "wheels"
+                     :on "the"
+                     :bus ["go" "round" "and" "round"]}]
+           (testing "form"
+             (let [ctx (tc/enqueue* {} [i (stub-response "application/x-www-form-urlencoded" "the=wheels&on=the&bus=go&bus=round&bus=and&bus=round")])]
+               (is (= body
+                      (-> (tc/execute ctx) :response :body))))))))
+
+    (testing "complex decoders"
+      (let [body {:the {:wheels ["on" "the" "bus"]}
+                  :go {:round {:and "round"}}}]
+        (testing "json"
+          (let [ctx (tc/enqueue* {} [i (stub-response "application/json" (encoders/json-encode body))])]
+            (is (= body
+                   (-> (tc/execute ctx) :response :body)))))
+
+        (testing "edn"
+          (let [ctx (tc/enqueue* {} [i (stub-response "application/edn" (pr-str body))])]
+            (is (= body
+                   (-> (tc/execute ctx) :response :body)))))
+
+        (testing "transit"
+          (testing "+json"
+            (let [ctx (tc/enqueue* {} [i (stub-response "application/transit+json"
+                                                        #?(:clj (slurp (encoders/transit-encode body :json))
+                                                           :cljs (encoders/transit-encode body :json)))])]
+              (is (= body
+                     (-> (tc/execute ctx) :response :body)))))
+
+          #?(:bb nil
+             :clj
+             (testing "+msgpack"
+               (let [ctx (tc/enqueue* {} [i (stub-response "application/transit+msgpack"
+                                                           (tu/input-stream->byte-array (encoders/transit-encode body :msgpack)))])]
+                 (is (= body
+                        (-> (tc/execute ctx) :response :body)))))))))))
 
 (deftest custom-encoding-test
   (testing "a user can support an encoding that martian doesn't know about by default"
@@ -148,10 +174,12 @@
                                                                          :decode #(encoders/json-decode % keyword)
                                                                          :as :magic}))]
 
-      (is (= {:encodes #?(:clj #{"application/json" "application/transit+msgpack" "application/transit+json" "application/edn"}
-                          :cljs #{"application/json" "application/transit+json" "application/edn"})
-              :decodes #?(:clj #{"application/json" "text/magical+json" "application/transit+msgpack" "application/transit+json" "application/edn"}
-                          :cljs #{"application/json" "text/magical+json" "application/transit+json" "application/edn"})}
+      (is (= {:encodes #?(:bb #{"application/json" "application/transit+msgpack" "application/transit+json" "application/edn"}
+                          :clj #{"application/json" "application/transit+msgpack" "application/transit+json" "application/edn" "application/x-www-form-urlencoded"}
+                          :cljs #{"application/json" "application/transit+json" "application/edn" "application/x-www-form-urlencoded"})
+              :decodes #?(:bb #{"application/json" "text/magical+json" "application/transit+msgpack" "application/transit+json" "application/edn"}
+                          :clj #{"application/json" "text/magical+json" "application/transit+msgpack" "application/transit+json" "application/edn" "application/x-www-form-urlencoded"}
+                          :cljs #{"application/json" "text/magical+json" "application/transit+json" "application/edn" "application/x-www-form-urlencoded"})}
              (i/supported-content-types [encode-body coerce-response]))))))
 
 (deftest validate-response-test
diff --git a/core/test/martian/openapi_test.cljc b/core/test/martian/openapi_test.cljc
index a9a3187..50cc50c 100644
--- a/core/test/martian/openapi_test.cljc
+++ b/core/test/martian/openapi_test.cljc
@@ -199,3 +199,21 @@
                                                      :decodes ["application/json"]}))]
 
            (:body-schema handler)))))
+
+(deftest form-encoded-schemas-test
+  (let [openapi-json
+        {:paths {(keyword "/models")
+                 {:post {:operationId "create-thing"
+                         :summary "Creates things"
+                         :requestBody {:required true,
+                                       :content
+                                       {:application/x-www-form-urlencoded
+                                        {:schema
+                                         {:type "object"
+                                          :properties {:foo {:type "string"} :bar {:type "number"}}
+                                          :required ["foo" "bar"]}}}}}}}}
+        [handler] (openapi->handlers openapi-json {:encodes ["application/x-www-form-urlencoded"]
+                                                   :decodes ["application/json"]})]
+    (testing "parses parameters"
+      (is (= {:body {:foo s/Str :bar s/Num}}
+             (:body-schema handler))))))