I'm trying to build a simple rest api, using liberator and monger :
(ns tm.place.endpoint
(:require [liberator.core :refer [defresource]]
[monger [collection :as mc] [query :as mq]]
[clojure.data [json :as json]]
monger.json))
(defresource entity-place-resource [id]
:available-media-types ["application/json"]
:allowed-methods [:get :put :delete]
:exists? (if-let [place (mc/find-map-by-id "place" id)] {:place place})
:handle-ok :place
; :handle-created (fn [ctx]
; (let [body (-> ctx :request :body)]
; (assoc body :_id id)))
:handle-not-found []
:handle-not-acceptable "Should accept application/json"
:handle-method-not-allowed (json/write-str
{:message "Can only handle get, put and delete"})
; :handle-not-implemented
; :handle-no-content
:put! (fn [ctx]
(let [body (-> ctx :request :body)]
(mc/update-by-id "place" id (assoc body :_id id))))
:delete! (mc/remove-by-id "place" id)
:can-put-to-missing? false)
I'm using advanced rest client to see if it works. If the method is :get or :delete, it does perfectly what I want (It first check if a document is exists, and then do the appropriate action). However, if the method is :put, it simply spits out http 201 created, which I think the request is success, but the corresponding document is deleted, not updated.
If I comment out the :delete! line, the :put! is work as expected, so I'm guessing the culprit is the :delete! line, but I have no idea why because since I'm using :put method, I'm assuming :delete! should remain untouched. Any idea why?
As you have defined your delete! as a bare function it's going to get evaluated when you don't expect. The pattern you have followed for put! needs to be applied, where you return a fn that does the work rather than act directly.
http://clojure-liberator.github.io/liberator/faq.html
Due to the exact details of the defresource macro expansion, the form used as the value is evaluated at unexpected times.
Related
I made service endpoint api for getting single object by id and it works as expected. I tested it with Postman and in handler function. I use cljs-ajax library for asynchronous client. I cant change the state of Reagent atom when I get response. Here is the code:
(ns businesspartners.core
(:require [reagent.core :as r]
[ajax.core :refer [GET POST]]
[clojure.string :as string]))
(def business-partner (r/atom nil))
(defn get-partner-by-id [id]
(GET "/api/get-partner-by-id"
{:headers {"Accept" "application/transit+json"}
:params {:id id}
:handler #(reset! business-partner (:business-partner %))}))
When I tried to access business-partner atom I got nil value for that atom. I can't figure out why because another method is almost the same except it get's list of business partners and works fine.
When I change the get-partner-by-id function:
(defn get-partner-by-id [id]
(GET "/api/get-partner-by-id"
{:headers {"Accept" "application/transit+json"}
:params {:id id}
:handler (fn [arg]
(println :handler-arg arg)
(reset! business-partner (:business-partner arg))
(println "Business partner from handler: " #business-partner))}))
Output in the browser console:
:handler-arg {:_id 5e7ad2c84b5c2d44583e8ecd,
:address Main Street,
:email nenmit#gmail.com,
:phone 555888,
:name Nen Mit}
Business partner from handler: nil
So, as you can see, I have my object in handler as desired, but when I try to reset my atom nothing happens. That's the core of the problem I think. Thank you Alan.
When in doubt, use debug print statements. Make your handler look like this:
:handler (fn [arg]
(println :handler-arg arg)
(reset! business-partner (:business-partner arg)))
You may also want to use clojure.pprint/pprint to pretty-print the output, or also add (type arg) to the output.
You may also want to initialize the atom to a specific value like
:bp-default so you can see if the nil you observe is the original one or if it is being reset to nil.
Update
So it is clear the key :business-partner does not exist in the map you are receiveing. This is what you must debug.
Trying to pull a non-existent key out of a map always returns nil. You could also use the 3-arg version of get to make this explicit. Convert
(:business-partner arg) => (get arg :business-partner ::not-found)
and you'll see the keyword ::not-found appear in your atom, verifying what is occurring.
In order to catch these problems early, I nearly always use a simple function grab from the Tupelo library like so:
(:business-partner arg) => (grab :business-partner arg)
The grab function will throw an exception if the expected key is not found. This provides early-warning of problems so you can track them down faster.
Another hint: next time use prn instead of println and it will retain double-quotes on string output like:
"Main Street"
I have one defresource, that is supposed to take POST-requests, validate request body in :malformed-decision, save the body to database in :post!-decision and return the saved body in :handle-created.
(defn parse-project [context] (json/read-str
(slurp (get-in context [:request :body]))
:key-fn keyword))
(defresource add-new-project
:malformed? (fn[ctx] (not (project-is-valid (parse-project ctx))))
:handle-malformed (fn [_] (generate-string (str "Malformed json!")))
...
:post! (fn [ctx] (save-to-db (parse-project ctx))
:handle-created (fn [ctx] (... parse-project ...))
So my code reads three times the ByteArrayInputStream(that comes from :request :body) with slurp-function. First time works but second time when slurp is called, nil is passed as a param and comes java.io.EOFException: JSON error. I think the reader starts reading where it was left last time.
How could I read the body of the request three times? Or is there nice way to save the result of reading to variable and pass that to other liberator-decisions?
The context can be updated by the outcome of each decision and action functions. You can parse the project once in malformed? and return a map with the parsed project which will be merged into the context so it is available to the following decisions and actions. For example:
(defresource add-new-project
:malformed? (fn[ctx] (let [project (parse-project ctx)]
(when (project-is-valid project)
{:project project})))
:handle-malformed (fn [_] (generate-string (str "Malformed json!")))
:post! (fn [ctx] (save-to-db (:project ctx)))
:handle-created (fn [ctx] (do-something (:project ctx))))
If the project is valid, :malformed? returns the {:project project} map which will be merged into the context to be used in the next decisions and actions.
If the project is not valid, it will return nil so execution continues in :handle-malformed.
For more info on liberator's execution model, see https://clojure-liberator.github.io/liberator/doc/execution-model.html
I am struggling to return JSON from a put! request:
My code looks like this:
(defn body-as-string [ctx]
(if-let [body (get-in ctx [:request :body])]
(condp instance? body
java.lang.String body
(slurp (io/reader body)))))
(defn orbit-world [dimensions ctx]
(let [in (json/parse-string (body-as-string ctx))]
(json/generate-string in)))
(defn init-world [params]
(let [dimensions (Integer/parseInt params)
world (vec (repeat dimensions (vec (take dimensions (repeatedly #(rand-int 2))))))]
(json/generate-string world)))
(defresource world [dimensions]
:allowed-methods [:get :put]
:available-media-types ["application/json"]
:available-charsets ["utf-8"]
:handle-ok (fn [_] (init-world dimensions))
:put! (fn [ctx] (orbit-world dimensions ctx)))
I simply want to return anything that is passed to the put request back as JSON until I understand what is going on.
But if I make a put request, I get the following response:
HTTP/1.1 201 Created
Date: Sun, 18 May 2014 15:35:32 GMT
Content-Type: text/plain
Content-Length: 0
Server: Jetty(7.6.8.v20121106)
My GET request returns JSON so I don't understand why the PUT request is not/
That is because a successfull PUT request does not return a http 200 status code (at least according to liberator), it returns a http 201 status code, as you can see from the response. Liberator handle http status code each in a different handler. In order to achieve what you want, you have to do:
(defresource world [dimensions]
:allowed-methods [:get :put]
:available-media-types ["application/json"]
:available-charsets ["utf-8"]
:handle-ok (fn [_] (init-world dimensions))
:put! (fn [ctx] (orbit-world dimensions ctx))
:handle-created (fn [_] (init-world dimensions))) ; Basically just a handler like any other.
Since you declare none on :handle-created, it defaults to an empty string with a text/plain content-type.
Edit:
In order to understand more, you have to see the decision graph. In there, you can see that after handling put! it goes to decision handling new?, if it's true go to handle-created if false, go to respond-with-entity? and so on.
A while back, Chris Granger posted this middleware to get JSON hashes to appear in the defpage params under an umbrella "backbone" element.
(defn backbone [handler]
(fn [req]
(let [neue (if (= "application/json" (get-in req [:headers "content-type"]))
(update-in req [:params] assoc :backbone (json/parse-string (slurp (:body req)) true))
req)]
(handler neue))))
How could I modify this code to have the JSON elements appear as top-level params in defpage; i.e. get rid of the :backbone umbrella?
There are two things you can do. One option is to replace the value of :params with the map returned after parsing the JSON. In order to do that, just associate the new map to the :params key.
(assoc req [:params] (json/parse-string (slurp (:body req)) true))
The other option (as suggested by #dAni) is to merge the values of the parsed JSON into so that existing values in the :params map are not overridden. The reason why you need to use partial instead of just using merge here is because the final map is the merged result of the maps from left-to-right. Your solution works if you want the values from the JSON map to take precedence.
(update-in req [:params]
(partial merge (json/parse-string (slurp (:body req)) true)))
Got it. assoc just works for one element so you have to put everything under the :backbone umbrella. To push all the JSON elements into the params, you have to use merge. So change the 4th line to:
(update-in req [:params] merge (json/parse-string (slurp (:body req)) true))
If you don't mind pulling in another dependency, you can use the ring-middleware-format library.
Instructions:
Add [ring-middleware-format "0.1.1"] to your project.clj
and then in your server.clj, add the following code:
Code:
(:require [ring.middleware.format-params :as format-params])
(server/add-middleware format-params/wrap-json-params)
(defn -main [& m]
; Start the server...
)
Now any incoming JSON will be available to use just like form POSTdata.
If I have the request "size=3&mean=1&sd=3&type=pdf&distr=normal" what's the idiomatic way of writing the function (defn request->map [request] ...) that takes this request and
returns a map {:size 3, :mean 1, :sd 3, :type pdf, :distr normal}
Here is my attempt (using clojure.walk and clojure.string):
(defn request-to-map
[request]
(keywordize-keys
(apply hash-map
(split request #"(&|=)"))))
I am interested in how others would solve this problem.
Using form-decode and keywordize-keys:
(use 'ring.util.codec)
(use 'clojure.walk)
(keywordize-keys (form-decode "hello=world&foo=bar"))
{:foo "bar", :hello "world"}
Assuming you want to parse HTTP request query parameters, why not use ring? ring.middleware.params contains what you want.
The function for parameter extraction goes like this:
(defn- parse-params
"Parse parameters from a string into a map."
[^String param-string encoding]
(reduce
(fn [param-map encoded-param]
(if-let [[_ key val] (re-matches #"([^=]+)=(.*)" encoded-param)]
(assoc-param param-map
(codec/url-decode key encoding)
(codec/url-decode (or val "") encoding))
param-map))
{}
(string/split param-string #"&")))
You can do this easily with a number of Java libraries. I'd be hesitant to try to roll my own parser unless I read the URI specs carefully and made sure I wasn't missing any edge cases (e.g. params appearing in the query twice with different values). This uses jetty-util:
(import '[org.eclipse.jetty.util UrlEncoded MultiMap])
(defn parse-query-string [query]
(let [params (MultiMap.)]
(UrlEncoded/decodeTo query params "UTF-8")
(into {} params)))
user> (parse-query-string "size=3&mean=1&sd=3&type=pdf&distr=normal")
{"sd" "3", "mean" "1", "distr" "normal", "type" "pdf", "size" "3"}
Can also use this library for both clojure and clojurescript: https://github.com/cemerick/url
user=> (-> "a=1&b=2&c=3" cemerick.url/query->map clojure.walk/keywordize-keys)
{:a "1", :b "2", :c "3"}
Yours looks fine. I tend to overuse regexes, so I would have solved it as
(defn request-to-keywords [req]
(into {} (for [[_ k v] (re-seq #"([^&=]+)=([^&]+)" req)]
[(keyword k) v])))
(request-to-keywords "size=1&test=3NA=G")
{:size "1", :test "3NA=G"}
Edit: try to stay away from clojure.walk though. I don't think it's officially deprecated, but it's not very well maintained. (I use it plenty too, though, so don't feel too bad).
I came across this question when constructing my own site and the answer can be a bit different, and easier, if you are passing parameters internally.
Using Secretary to handle routing: https://github.com/gf3/secretary
Parameters are automatically extracted to a map in :query-params when a route match is found. The example given in the documentation:
(defroute "/users/:id" [id query-params]
(js/console.log (str "User: " id))
(js/console.log (pr-str query-params)))
(defroute #"/users/(\d+)" [id {:keys [query-params]}]
(js/console.log (str "User: " id))
(js/console.log (pr-str query-params)))
;; In both instances...
(secretary/dispach! "/users/10?action=delete")
;; ... will log
;; User: 10
;; "{:action \"delete\"}"
You can use ring.middleware.params. Here's an example with aleph:
user=> (require '[aleph.http :as http])
user=> (defn my-handler [req] (println "params:" (:params req)))
user=> (def server (http/start-server (wrap-params my-handler)))
wrap-params creates an entry in the request object called :params. If you want the query parameters as keywords, you can use ring.middleware.keyword-params. Be sure to wrap with wrap-params first:
user=> (require '[ring.middleware.params :refer [wrap-params]])
user=> (require '[ring.middleware.keyword-params :refer [wrap-keyword-params])
user=> (def server
(http/start-server (wrap-keyword-params (wrap-params my-handler))))
However, be mindful that this includes a dependency on ring.