├── .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 |
--------------------------------------------------------------------------------