Compojure - map query parameters with string keys - clojure

I know I can map the query string as a keyworkd map.
(defroutes my-routes
(GET "/" {params :query-params} params))
But is there a way do the same with a string keyed map?
(Using Compojure or Ring)
The point here is not to iterate over the map or use a function, but create it with string keys by default.
{ :a "b" } -> {"a" "b"}

Compojure 1.5.1 does not parse any query strings by default (by not using any middleware). However, this might have been different in earlier versions.
(require '[compojure.core :refer :all])
(require '[clojure.pprint :refer [pprint]])
(defroutes handler
(GET "/" x
(with-out-str (pprint x)))) ;; just a way to receive a pretty printed string response
$ curl localhost:3000/?a=b
{:ssl-client-cert nil,
:protocol "HTTP/1.1",
:remote-addr "127.0.0.1",
:params {}, ;; EMPTY!
:route-params {},
:headers
{"user-agent" "curl/7.47.1", "accept" "*/*", "host" "localhost:3000"},
:server-port 3000,
:content-length nil,
:compojure/route [:get "/"],
:content-type nil,
:character-encoding nil,
:uri "/",
:server-name "localhost",
:query-string "a=b", ;; UNPARSED QUERY STRING
:body
#object[org.eclipse.jetty.server.HttpInputOverHTTP 0x6756d3a3 "HttpInputOverHTTP#6756d3a3"],
:scheme :http,
:request-method :get}
Ring offers the ring.params.wrap-params middleware, which parses the query string and creates a hashmap of it under the params-key:
(defroutes handler
(wrap-params (GET "/" x
(prn-str (:params x)))))
$ curl localhost:3000/?a=55
{"a" "55"}
Additionaly ring.params.wrap-params can be used:
(defroutes handler
(wrap-params (wrap-keyword-params (GET "/" x
(prn-str (:params x))))))
$ curl localhost:3000/?a=55
{:a "55"}

Not sure about compojure, but you can undo it yourself:
(use 'clojure.walk)
(stringify-keys {:a 1 :b {:c {:d 2}}})
;=> {"a" 1, "b" {"c" {"d" 2}}}
https://clojuredocs.org/clojure.walk/stringify-keys

Related

How to save the file in the server?

I'm receiving the following request in the server that I'm extracting using (-> req :params):
{"_parts":[["video",{"_data":{"size":2971246,"blobId":"D002459C-47C5-4403-ABC6-A2DE6A46230A","offset":0,"type":"video/quicktime","name":"DCDE604A-954F-4B49-A1F9-1BCC2C2F37BC.mov","__collector":null}}],["key","VAL"]]}
It contains a file "video" with a name and blobId. However, I want to access the file's data and save it to a file. So far, I've tried the following:
(defn upload-shot-video [req]
(prn "uploading video")
(prn "video is! " (-> req :multipart-params))
(prn "video is " (-> req :params))
(prn "video before is " (slurp (-> req :body)))
(.reset (-> req :body))
(prn "req full" (-> req))
(prn "video after is " (-> req :body))
(prn "video is! " (-> req :multipart-params))
(clojure.java.io/copy (-> req :body) (clojure.java.io/file "./resources/public/video.mov"))
(let [filename (str (rand-str 100) ".mov")]
(s3/put-object
:bucket-name "humboi-videos"
:key filename
:file "./resources/public/video.mov"
:access-control-list {:grant-permission ["AllUsers" "Read"]})
(db/add-video {:name (-> req :params :name)
:uri (str "https://humboi-videos.s3-us-west-1.amazonaws.com/" filename)}))
(r/response {:res "okay!"}))
In which I'm trying to save the (-> req :body) into the file (which is an inputstream). This must be incorrect. What's the correct way to save this file that the server has received into a file, by saving the data into a file on the server? How to extract this data from the request?
If you are using Ring, you need to use wrap-multipart-params middleware.
(ns controller
(:require [ring.middleware.params :refer [wrap-params]]
[ring.middleware.multipart-params :refer [wrap-multipart-params]])
(defn upload-shot-video [req]
(let [uploaded-file (-> req :params "file" :tempfile) ;; here is a java.io.File instance of your file
(save-file uploaded-file)
{:status 201 :body "Upload complete"}))
(def app
(-> upload-shot-video
wrap-params
wrap-multipart-params))

How to extract the form data from a request in liberator?

I have the following client-side request:
(let [form-data (doto
(js/FormData.)
(.append "filename" file))]
(ajax-request
{:uri "/some/uri"
:method :post
:body form-data
:format (raw-request-format)
:response-format (raw-response-format)})
)
And on the server (with liberator):
(defresource some-resource [_]
:allowed-methods [:get :post]
:available-media-types ["application/json"]
:exists? (fn [ctx]
(prn "form-data " (-> ctx :request :body slurp)) ;prints
(prn "form-data " (-> ctx :request :body slurp cheshire/decode)) ;doesn't print
))
At the server, I do get the print of the form-data like so:
"form-data " "------WebKitFormBoundaryI9CA2zKhFT0Stmx7\r\nContent-Disposition: form-data; name=\"new-name\"\r\n\r\n{:uploaded-file #object[File [object File]], :name \"somename\", :another \"1234\"}\r\n------WebKitFormBoundaryI9CA2zKhFT0Stmx7--\r\n"
But the next prn doesn't print anything where my goal is to extract the :uploaded-file. What am I doing wrong?

How do you set up middleware in Clojure using Reitit to enable coercion of body params?

I am trying to set a a Reitit router that performs coercion. I can get the response section working but I can't seem to get the body coercion to work properly. The following is the code I am using:
(ns example
(:require
[ring.middleware.json :refer [wrap-json-body wrap-json-response]]
[ring.middleware.reload :refer [wrap-reload]]
[ring.util.response :refer [response]]
[reitit.ring.coercion :as rrc]
[reitit.coercion.malli]
[reitit.ring :as ring]))
(def router
(ring/ring-handler
(ring/router
[["/healthz" {:get (fn [_] {:status 200 :body "healthy"})}]
["/api" {:coercion reitit.coercion.malli/coercion
:middleware [rrc/coerce-exceptions-middleware
rrc/coerce-request-middleware
rrc/coerce-response-middleware]}
["/messages" {:post {:summary "Add a new message"
:parameters {:body [:map [:name string?]]}
:responses {200 {:body [:map
[:message string?]]}}
:handler (fn [req]
{:status 200 :body {:message (:name (:body req))}})}}]]]
{:data {:middleware []}})
(ring/create-default-handler)))
(def app (-> #'router
(wrap-reload)
(wrap-json-response)
(wrap-json-body {:keywords? true :bigdecimals? true})))
The coercion is failing even when I send the correct body parameters with the following error:
{
"schema": "[:map {:closed true} [:name string?]]",
"errors": [
{
"path": [],
"in": [],
"schema": "[:map {:closed true} [:name string?]]",
"value": null,
"type": "malli.core/invalid-type",
"message": "invalid type"
}
],
"value": null,
"type": "reitit.coercion/request-coercion",
"coercion": "malli",
"in": [
"request",
"body-params"
],
"humanized": [
"invalid type"
]
}
This seems to be indicating that the coercion can't find any body parameters. I assume this has to do with the way I am setting up my middleware but I can't seem to fix this. How should I set the middleware up so the Reitit coercion works properly in this case?
I was able to get coercion to work by removing the wrap-json-response and wrap-json-body middleware and replacing it with the muuntaja middleware as follows:
(def router
(ring/ring-handler
(ring/router
[["/healthz" {:get (fn [_] {:status 200 :body "healthy"})}]
["/api" {:coercion reitit.coercion.malli/coercion
:middleware []}
["/messages" {:name ::message
:post {:summary "Add a new message"
:parameters {:body [:map [:name string?]]}
:responses {200 {:body [:map
[:message string?]]}}
:handler (fn [{:keys [parameters]}]
{:status 200 :body {:message (:name (:body parameters))}})}}]]]
{:data {:muuntaja m/instance
:middleware [params/wrap-params
muuntaja/format-middleware
rrc/coerce-exceptions-middleware
rrc/coerce-request-middleware
rrc/coerce-response-middleware]}})
(ring/create-default-handler)))

How do I mock a PUT request using ring.mock.request

How do I make this test pass:
(ns imp-rest.parser-test-rest
(:require [clojure.test :refer :all])
(:require [ring.mock.request :as mock] )
(:require [imp-rest.web :as w]))
(deftest test-parser-rest
(testing "put settings"
(w/app
(mock/request :put "/settings/coordinateName" "FOO" ))
(let [response (w/app (mock/request :get "/settings"))]
(println response )
(is (= (get (:body response) :coordinateName) "FOO")))))
it fails with:
FAIL in (test-parser-rest) (parser_test_rest.clj:30)
put settings
expected: (= (get (:body response) :coordinateName) "FOO")
actual: (not (= nil "FOO"))
Here's my handler:
(ns imp-rest.web
(:use compojure.core)
(:use ring.middleware.json-params)
(:require [clj-json.core :as json])
(:require [ring.util.response :as response])
(:require [compojure.route :as route])
(:require [imp-rest.settings :as s]))
(defn json-response [data & [status]]
{:status (or status 200)
:headers {"Content-Type" "application/json"}
:body (json/generate-string data)})
(defroutes handler
(GET "/settings" []
(json-response (s/get-settings)))
(GET "/settings/:id" [id]
(json-response (s/get-setting id)))
(PUT "/settings" [id value]
(json-response (s/put-setting id value)))
(route/not-found "Page not found") )
(def app
(-> handler
wrap-json-params))
which exposes this map (of settings):
(ns imp-rest.settings)
(def settings
(atom
{:coordinateName nil
:burnin nil
:nslices nil
:mrsd nil
}))
(defn get-settings []
#settings)
(defn get-setting [id]
(#settings (keyword id)))
(defn put-setting [id value]
(swap! settings assoc (keyword id) value)
value)
and the entry point:
(ns imp-rest.core
(:use ring.adapter.jetty)
(:require [imp-rest.web :as web]))
(defn -main
"Entry point"
[& args]
(do
(run-jetty #'web/app {:port 8080})
);END;do
);END: main
Now when I 'lein run' I can make a (working) request like this:
curl -X PUT -H "Content-Type: application/json" \
-d '{"id" : "coordinateName", "value" : "FOO"}' \
http://localhost:8080/settings
which is what I try to mock with the test. Any help appreciated.
If you want to have :id in your PUT /settings/:id route accepting body in format {"value": "..."}, you need to change your routes definition:
(defroutes handler
(GET "/settings" []
(json-response (s/get-settings)))
(GET "/settings/:id" [id]
(json-response (s/get-setting id)))
(PUT "/settings/:id" [id value]
(json-response (s/put-setting id value)))
(route/not-found "Page not found"))
And change how you call your PUT endpoint in the test:
(w/app
(-> (mock/request
:put
"/settings/coordinateName"
(json/generate-string {:value "FOO"}))
(mock/content-type "application/json")))
What was changed?
:id in your PUT URL route definition (/settings -> /settings/:id)
Your PUT request didn't send a correct request and content type.
If you want to have a PUT /settings route expecting {"id": "...", "value": "..."} request body, then you need to change how you create a mock request:
(w/app
(-> (mock/request
:put
"/settings"
(json/generate-string {:id "coordinateName" :value "FOO"}))
(mock/content-type "application/json"))
Your curl request specifies the parameters as JSON in the body of the PUT request, but your mock request tries to use URL parameters.
There are two options to resolve this:
compojure can automatically translate parameters, but only when the relevant middleware is present -- you have wrap-json-params added to your handler, but you're missing wrap-params. The answer from Piotrek Bzdyl amounts to making these params explicit in the compojure routes.
Alternatively, you can add the ID/value pair as JSON in the body of the mock request using request.mock.body.

Clojure - use a core.async channel with Yada/Aleph

I am trying to use Clojure manifold library, and in order to understand it, I need wanted to convert a core.async channel into a manifold stream.
I would like to create the equivalent the following using a core.async channel :
(require '[manifold.stream :as s])
(s/periodically 100 #(str " ok "))
;; Here is what I tried, it fails with an error 500
(let [ch (chan)]
(go-loop []
(>! ch " ok ")
(<! (timeout 100))
(recur))
(s/->source ch))
I am trying to feed a core.async channel into yada. The first code sample, using manifold.stream/periodic works, not the others using core.async. I tried on yada 1.0.0 and 1.1.0-SNAPSHOT.
Using manifold.stream/periodic works :
(def get-stream
(yada (fn [ctx]
(-> (:response ctx)
(assoc :status 202)
(assoc :body (s/periodically 1000 #(str (System/currentTimeMillis) " ")))))
{:representations [{:media-type "application/json"
:charset "UTF-8"}
{:media-type "application/edn"
:charset "UTF-8"}]}))
Using manifold.stream/->source returns an error 500 :
(def get-stream
(yada (fn [ctx]
(-> (:response ctx)
(assoc :status 202)
;; Similar to this : https://github.com/juxt/yada/blob/94f3ee93de155a8513b27e0508608691ed556a55/dev/src/yada/dev/async.clj
(assoc :body (let [ch (chan)]
(go-loop []
(>! ch " ok ")
(<! (timeout 100))
(recur))
(s/->source ch)))))
{:representations [{:media-type "application/json"
:charset "UTF-8"}
{:media-type "application/edn"
:charset "UTF-8"}]}))
;; Error on the page :
;; 500: Unknown
;; Error on GET
;; #error {
;; :cause "No implementation of method: :to-body of protocol: #'yada.body/MessageBody found for class: manifold.stream.async.CoreAsyncSource"
;; :via
;; [{:type clojure.lang.ExceptionInfo
;; :message "Error on GET"
;; :data {:response #yada.response.Response{:representation {:media-type #yada.media-type[application/json;q=1.0], :charset #yada.charset.CharsetMap{:alias "UTF-8", :quality 1.0}}, :vary #{:media-type}}, :resource #function[backend.routes.examples.ts/fn--57734]}
;; :at [clojure.core$ex_info invoke "core.clj" 4593]}
;; {:type java.lang.IllegalArgumentException
;; :message "No implementation of method: :to-body of protocol: #'yada.body/MessageBody found for class: manifold.stream.async.CoreAsyncSource"
;; :at [clojure.core$_cache_protocol_fn invoke "core_deftype.clj" 554]}]
;; :trace
Third attempt, with a core.async channel (different error 500) :
(def get-stream
(yada (fn [ctx]
(-> (:response ctx)
(assoc :status 202)
(assoc :body (chan)))
{:representations [{:media-type "application/json"
:charset "UTF-8"}
{:media-type "application/edn"
:charset "UTF-8"}]}))
;; Error on the page :
;; 500: Unknown
;; Error on GET
;; #error {
;; :cause "No implementation of method: :to-body of protocol: #'yada.body/MessageBody found for class: clojure.core.async.impl.channels.ManyToManyChannel"
;; :via
;; [{:type clojure.lang.ExceptionInfo
;; :message "Error on GET"
;; :data {:response #yada.response.Response{:representation {:media-type #yada.media-type[application/json;q=1.0], :charset #yada.charset.CharsetMap{:alias "UTF-8", :quality 1.0}}, :vary #{:media-type}}, :resource #function[backend.routes.api.subscribe/subscribe$fn--64130]}
;; :at [clojure.core$ex_info invoke "core.clj" 4593]}
;; {:type java.lang.IllegalArgumentException
;; :message "No implementation of method: :to-body of protocol: #'yada.body/MessageBody found for class: clojure.core.async.impl.channels.ManyToManyChannel"
;; :at [clojure.core$_cache_protocol_fn invoke "core_deftype.clj" 554]}]
;; :trace
The key error is this:
"No implementation of method: :to-body of protocol:
#'yada.body/MessageBody
found for class: manifold.stream.async.CoreAsyncSource"
It reveals that the yada version you are using is trying to coerce a body type it doesn't understand. More recent versions of yada are more permissive about what you can send through to the web-server, and allow anything through that it doesn't know how to transform.