├── .gitignore ├── README.markdown ├── examples └── awe │ ├── README.md │ ├── project.clj │ └── src │ ├── awesome_app.clj │ └── my_awesome_service.clj ├── project.clj ├── src └── ring │ └── middleware │ └── edn.clj └── test └── ring └── middleware └── edn_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /pom.xml 2 | .nrepl-port 3 | *jar 4 | /lib 5 | /classes 6 | /native 7 | /.lein-failures 8 | /checkouts 9 | /.lein-deps-sum 10 | target 11 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # ring-edn 2 | 3 | A [Ring](https://github.com/mmcgrana/ring) middleware that augments :params by parsing a request body as [Extensible Data Notation](https://github.com/edn-format/edn) (EDN). 4 | 5 | ## Where 6 | 7 | * [Source repository](https://github.com/fogus/ring-edn) *-- patches welcomed* 8 | 9 | ## Usage 10 | 11 | ### Leiningen 12 | 13 | In your `:dependencies` section add the following: 14 | 15 | [fogus/ring-edn "0.3.0"] 16 | 17 | ### Ring 18 | 19 | *the [examples directory of the ring-edn project](http://github.com/fogus/ring-edn/tree/master/examples/awe) contains the source for the following* 20 | 21 | To use this middleware using Ring and [Compojure](https://github.com/weavejester/compojure), create a new Leiningen project with a `project.clj` file of the form: 22 | 23 | ```clojure 24 | (defproject awesomeness "0.0.1" 25 | :description "true power awesomeness" 26 | :dependencies [[org.clojure/clojure "1.6.0"] 27 | [ring "1.0.2"] 28 | [compojure "1.0.1"] 29 | [fogus/ring-edn "0.3.0"]] 30 | :main awesome-app) 31 | ``` 32 | 33 | Next, create a file in `src` called `my_awesome_service.clj` with the following: 34 | 35 | ```clojure 36 | (ns my-awesome-service 37 | (:use compojure.core) 38 | (:use ring.middleware.edn)) 39 | 40 | (defn generate-response [data & [status]] 41 | {:status (or status 200) 42 | :headers {"Content-Type" "application/edn"} 43 | :body (pr-str data)}) 44 | 45 | (defroutes handler 46 | (GET "/" [] 47 | (generate-response {:hello :cleveland})) 48 | 49 | (PUT "/" [name] 50 | (generate-response {:hello name}))) 51 | 52 | (def app 53 | (-> handler 54 | wrap-edn-params)) 55 | ``` 56 | 57 | And finally, create another file in `src` named `awesome_app.clj` with the following: 58 | 59 | ```clojure 60 | (ns awesome-app 61 | (:use ring.adapter.jetty) 62 | (:require [my-awesome-service :as awe])) 63 | 64 | (defn -main 65 | [& args] 66 | (run-jetty #'awe/app {:port 8080})) 67 | ``` 68 | 69 | ### Using custom types 70 | 71 | EDN offers extensible types through 72 | [tagged literals](https://github.com/edn-format/edn#tagged-elements) 73 | and `ring-edn` can read those types from the incoming requests. 74 | As an example, let's add `uri` to EDN. In our Clojure program 75 | it will be represented by `java.net.URI` but in other platforms it 76 | might be represented differently, i.e `goog.Uri` in ClojureScript. To 77 | use a new type, we need to define a reader (takes a string and returns 78 | our representation) and a printer (takes our representation and writes 79 | it as a string). The printer determines the tagged literal and it is 80 | implemented as a multimethod of `clojure.core/print-method`. We might 81 | be tempted to use `#uri` for the tagged literal but it needs to be 82 | namespaced in case an application needs to deal with multiple `uri` 83 | representations. Therefore we will use `#my-app/uri`: 84 | 85 | ```clj 86 | (ns my-app.uri 87 | (:import (java.net URI))) 88 | 89 | (defn read-uri [s] 90 | (URI. s)) 91 | 92 | (defmethod print-method java.net.URI [this w] 93 | (.write w "#my-app/uri \"") 94 | (.write w (.toString this)) 95 | (.write w "\"")) 96 | ``` 97 | 98 | Now we indicate `wrap-edn-params` that whenever it finds `#my-app/uri` 99 | it should read the expression that follows with `read-uri`: 100 | 101 | ``` 102 | (def app 103 | (-> handler 104 | (wrap-edn-params {:readers {'my-app/uri #'my-app.uri/read-uri}}))) 105 | ``` 106 | 107 | Other options besides `:readers` can be passed to `wrap-edn-params` 108 | which are forwarded to `clojure.edn/read-string` as defined 109 | [here](https://clojure.github.io/clojure/clojure.edn-api.html). 110 | 111 | 112 | ### Testing 113 | 114 | Run this app in your console with `lein run` and test with `curl` using the following: 115 | 116 | ```sh 117 | $ curl -X GET http://localhost:8080/ 118 | 119 | #=> {:hello :cleveland} 120 | 121 | $ curl -X PUT -H "Content-Type: application/edn" \ 122 | -d '{:name :barnabas}' \ 123 | http://localhost:8080/ 124 | 125 | #=> {:hello :barnabas}% 126 | ``` 127 | 128 | You can also run the test suite with `lein test`. 129 | 130 | ## Acknowledgment(s) 131 | 132 | Thanks to [Mark McGranaghan](http://markmcgranaghan.com/) for his work on Ring and [ring-json-params](https://github.com/mmcgrana/ring-json-params) on which this project was based. An additional thanks to Sebastian Bensusan for his high-quality patches. 133 | 134 | ## License 135 | 136 | Copyright (C) 2012-2015 Fogus 137 | 138 | Distributed under the Eclipse Public License, the same as Clojure. 139 | -------------------------------------------------------------------------------- /examples/awe/README.md: -------------------------------------------------------------------------------- 1 | # awe 2 | 3 | Shows an example of how to create a web-service that handles EDN data via ring-edn. 4 | 5 | ## Running 6 | 7 | Type the following at the command line: 8 | 9 | lein run 10 | 11 | The sample will run on port 8080, so be aware if another app is hogging that port. 12 | 13 | At another command prompt type the following to test: 14 | 15 | ```sh 16 | $ curl -X GET http://localhost:8080/ 17 | 18 | #=> {:hello :cleveland} 19 | 20 | $ curl -X PUT -H "Content-Type: application/edn" \ 21 | -d '{:name :barnabas}' \ 22 | http://localhost:8080/ 23 | 24 | #=> {:hello :barnabas}% 25 | ``` 26 | 27 | ## License 28 | 29 | Copyright (C) 2012-2015 Fogus 30 | 31 | Distributed under the Eclipse Public License, the same as Clojure. 32 | 33 | -------------------------------------------------------------------------------- /examples/awe/project.clj: -------------------------------------------------------------------------------- 1 | (defproject awesomeness "0.0.1" 2 | :description "true power awesomeness" 3 | :dependencies [[org.clojure/clojure "1.6.0"] 4 | [ring "1.0.2"] 5 | [compojure "1.0.1"] 6 | [fogus/ring-edn "0.3.0"]] 7 | :main awesome-app) 8 | -------------------------------------------------------------------------------- /examples/awe/src/awesome_app.clj: -------------------------------------------------------------------------------- 1 | (ns awesome-app 2 | (:use ring.adapter.jetty) 3 | (:require [my-awesome-service :as awe])) 4 | 5 | (defn -main 6 | [& args] 7 | (run-jetty #'awe/app {:port 8080})) 8 | -------------------------------------------------------------------------------- /examples/awe/src/my_awesome_service.clj: -------------------------------------------------------------------------------- 1 | (ns my-awesome-service 2 | (:use compojure.core) 3 | (:use ring.middleware.edn)) 4 | 5 | (defn generate-response [data & [status]] 6 | {:status (or status 200) 7 | :headers {"Content-Type" "application/edn"} 8 | :body (pr-str data)}) 9 | 10 | (defroutes handler 11 | (GET "/" [] 12 | (generate-response {:hello :cleveland})) 13 | 14 | (PUT "/" [name] 15 | (generate-response {:hello name}))) 16 | 17 | (def app 18 | (-> handler 19 | wrap-edn-params)) 20 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject fogus/ring-edn "0.4.0-SNAPSHOT" 2 | :description "A Ring middleware that augments :params by parsing a request body as Extensible Data Notation (EDN)." 3 | :url "https://github.com/tailrecursion/ring-edn" 4 | :license {:name "Eclipse Public License - v 1.0" 5 | :url "http://www.eclipse.org/legal/epl-v10.html" 6 | :distribution :repo 7 | :comments "same as Clojure"} 8 | :dependencies [[org.clojure/clojure "1.6.0"]]) 9 | -------------------------------------------------------------------------------- /src/ring/middleware/edn.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.edn 2 | (:require clojure.edn)) 3 | 4 | (defn- edn-request? 5 | [req] 6 | (if-let [^String type (get-in req [:headers "content-type"] "")] 7 | (not (empty? (re-find #"^application/(vnd.+)?edn" type))))) 8 | 9 | (defprotocol EdnRead 10 | "Specifies that the object can be read and transformed to edn" 11 | (-read-edn [this] [this opts] 12 | "Transforms the serialized object into edn. 13 | May take an opts map to pass to clojure.edn/read-string")) 14 | 15 | (extend-type String 16 | EdnRead 17 | (-read-edn 18 | ([s] (-read-edn s {})) 19 | ([s opts] 20 | (clojure.edn/read-string opts s)))) 21 | 22 | (extend-type java.io.InputStream 23 | EdnRead 24 | (-read-edn 25 | ([is] (-read-edn is {})) 26 | ([is opts] 27 | (clojure.edn/read 28 | (merge {:eof nil} opts) 29 | (java.io.PushbackReader. 30 | (java.io.InputStreamReader. is "UTF-8")))))) 31 | 32 | (defn wrap-edn-params 33 | "If the request has the edn content-type, it will attempt to read 34 | the body as edn and then assoc it to the request under :edn-params 35 | and merged to :params. 36 | 37 | It may take an opts map to pass to clojure.edn/read-string" 38 | ([handler] (wrap-edn-params handler {})) 39 | ([handler opts] 40 | (fn [req] 41 | (if-let [body (and (edn-request? req) (:body req))] 42 | (let [edn-params (binding [*read-eval* false] (-read-edn body opts)) 43 | req* (assoc req 44 | :edn-params edn-params 45 | :params (merge (:params req) edn-params))] 46 | (handler req*)) 47 | (handler req))))) 48 | -------------------------------------------------------------------------------- /test/ring/middleware/edn_test.clj: -------------------------------------------------------------------------------- 1 | (ns ring.middleware.edn-test 2 | (:use [ring.middleware.edn]) 3 | (:use [clojure.test]) 4 | (:import java.io.ByteArrayInputStream) 5 | (:require [clojure.edn :as edn])) 6 | 7 | (def content-type "application/edn; charset=UTF-8") 8 | 9 | (defn stream [s] 10 | (ByteArrayInputStream. (.getBytes s "UTF-8"))) 11 | 12 | (def build-edn-params 13 | (wrap-edn-params identity)) 14 | 15 | (deftest noop-with-other-content-type 16 | (let [req {:content-type "application/xml" 17 | :body (stream "") 18 | :params {"id" 3}} 19 | resp (build-edn-params req)] 20 | (is (= "") (slurp (:body resp))) 21 | (is (= {"id" 3} (:params resp))) 22 | (is (nil? (:edn-params resp))))) 23 | 24 | (deftest augments-with-edn-content-type 25 | (let [req {:content-type content-type 26 | :body (stream "{:foo :bar}") 27 | :params {"id" 3}} 28 | resp (build-edn-params req)] 29 | (is (= {"id" 3 :foo :bar} (:params resp))) 30 | (is (= {:foo :bar} (:edn-params resp))))) 31 | 32 | (deftest augments-with-edn-content-type-no-eval 33 | (let [req {:content-type content-type 34 | :body (stream "{:expr (+ 1 2)}") 35 | :params {"id" 3}} 36 | resp (build-edn-params req)] 37 | (is (= {"id" 3 :expr '(+ 1 2)} (:params resp))) 38 | (is (= '{:expr (+ 1 2)} (:edn-params resp))))) 39 | 40 | (deftest augments-with-edn-content-type-no-read-eval 41 | (let [req {:content-type content-type 42 | :body (stream "{:expr #=(+ 1 2)}")}] 43 | (is (thrown? RuntimeException (build-edn-params req))))) 44 | 45 | (deftest augments-with-mixed-content-type 46 | (let [req {:content-type "application/vnd.foobar+edn; charset=UTF-8" 47 | :body (stream "{:foo :bar}") 48 | :params {"id" 3}} 49 | resp (build-edn-params req)] 50 | (is (= {"id" 3 :foo :bar} (:params resp))) 51 | (is (= {:foo :bar} (:edn-params resp))))) 52 | 53 | ;; Custom Tags 54 | 55 | (defrecord User [name]) 56 | 57 | (def build-custom-edn-params 58 | (wrap-edn-params identity {:readers {'ring-edn/user map->User}})) 59 | 60 | (deftest arguments-with-custom-tags 61 | (let [req {:content-type content-type 62 | :body (stream "{:user #ring-edn/user {:name \"Jane Doe\"}}") 63 | :params {"id" 3}}] 64 | (let [res (build-custom-edn-params req)] 65 | (is (= {:user (User. "Jane Doe")} (:edn-params res)))))) 66 | --------------------------------------------------------------------------------