Matching the whole path - clojure

What's the proper way to match the entire path?
(defroutes app
(ANY "*" [*]
{:status 200
:headers {"Content-Type" "text/plain"}
:body (str "path = " *)}))
works but it also gives me the compiler warning WARNING: * should not be used as a route binding.
(defroutes app
(ANY "*" [path]
{:status 200
:headers {"Content-Type" "text/plain"}
:body (str "path = " path)}))
compiles without a warning but doesn't bind path to the path.

It looks like
(defroutes app
(ANY [":path" :path #".*"] [path]
{:status 200
:headers {"Content-Type" "text/plain"}
:body (str "path = " path)}))
works.

Related

How do I simplify Compojure routes?

I have the following code to define my routes in Compojure:
(ns my-project.my-test
(:gen-class)
(:require
[my-test.template-views :refer :all]
[compojure.core :refer [defroutes GET POST context]]
[compojure.route :as route]
[org.httpkit.server :refer [run-server]]))
(defn wrap-request
[handler]
(fn [request]
(let [{remote-addr :remote-addr uri :uri scheme :scheme request-method :request-method} request]
(println (str "REQUEST: " request)))
(handler request)))
(defroutes app
(wrap-request
(GET "/" request
{:status 200
:headers {"Content-Type" "text/html"}
:body (template-body (:uri request))}))
(wrap-request
(GET "/page1" request
{:status 200
:headers {"Content-Type" "text/html"}
:body (template-body (:uri request))}))
(wrap-request
(GET "/page2" request
{:status 200
:headers {"Content-Type" "text/html"}
:body (template-body (:uri request))}))
(wrap-request
(GET "/page3" request
{:status 200
:headers {"Content-Type" "text/html"}
:body (template-body (:uri request))}))
(route/resources "/")
(route/not-found {:status 404
:headers {"Content-Type" "text/html"}
:body "<h1>Not Found</h1>"}))
That works but it seems like I should be able to simplify it like this:
(ns my-project.my-test
(:gen-class)
(:require
[my-test.template-views :refer :all]
[compojure.core :refer [defroutes GET POST context]]
[compojure.route :as route]
[org.httpkit.server :refer [run-server]]))
(defn wrap-request
[handler]
(fn [request]
(let [{remote-addr :remote-addr uri :uri scheme :scheme request-method :request-method} request]
(println (str "REQUEST: " request)))
(handler request)))
(defn wrap-template
[route]
(wrap-request
(GET route request
{:status 200
:headers {"Content-Type" "text/html"}
:body (template-body (:uri request))})))
(defroutes app
(map wrap-template ["/" "/page1" "/page2" "/page3"])
(route/resources "/")
(route/not-found {:status 404
:headers {"Content-Type" "text/html"}
:body "<h1>Not Found</h1>"}))
However, when I do, I get this error backtrace:
Sat Apr 24 22:38:33 MDT 2021 [worker-2] ERROR - GET /page2
java.lang.ClassCastException: class clojure.lang.LazySeq cannot be cast to class clojure.lang.IFn (clojure.lang.LazySeq and clojure.lang.IFn are in unnamed module of loader 'app')
at compojure.core$routing$fn__368163.invoke(core.clj:185)
at clojure.core$some.invokeStatic(core.clj:2705)
at clojure.core$some.invoke(core.clj:2696)
at compojure.core$routing.invokeStatic(core.clj:185)
at compojure.core$routing.doInvoke(core.clj:182)
at clojure.lang.RestFn.applyTo(RestFn.java:139)
at clojure.core$apply.invokeStatic(core.clj:669)
at clojure.core$apply.invoke(core.clj:662)
at compojure.core$routes$fn__368167.invoke(core.clj:192)
at org.httpkit.server.HttpHandler.run(RingHandler.java:117)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:834)
Is there something about using (map) that is wrong here?
routes (and thus defroutes) expects each argument to be a request handler function. A list of handlers is not a handler function; hence the error. Happily, there is a function to convert a list of handlers to a single handler: routes! Since it wants N separate arguments, rather than a single list, you will need apply as well. So:
(defroutes app
(apply routes (map wrap-template ["/" "/page1" "/page2" "/page3"]))
(route/resources "/")
(route/not-found {:status 404
:headers {"Content-Type" "text/html"}
:body "<h1>Not Found</h1>"}))
As an aside, I generally suggest not using defroutes, simply because it does not compose as easily as separate def + routes, and for beginners it leads to forgetting that anything but defroutes exists, when in fact most interesting servers will want to apply a function to some of their routes.

Unable to route properly with bidi

I am sending an index-handler on "/this-route":
(defn index-handler [req]
(assoc (resource-response "index.html" {:root "public"})
:headers {"Content-Type" "text/html; charset=UTF-8"}))
(def routes ["" {"/this-route" {:get index-handler}}]) ;; works
Which works fine.
But when I append anything to this-route, I'm unable to send index-handler even though I can still send a basic res/response:
(def routes ["" {"/this-route" {"" {:get index-handler} ;; doesn't work
"/something" {:get index-handler} ;; doesn't work
"/something-else" (res/response "some response") ;; works
}}])
I get the errors in the client side console showing an error in the index.html line, where I start my app:
<script type="text/javascript">myapp.system.go();</script>
and the error is "myapp is not defined."
Why might this be, and what am I doing wrong?
--- EDIT ---
This is the complete error in console:
Uncaught ReferenceError: myapp is not defined
at something-else:15
DevTools failed to load SourceMap: Could not load content for chrome-extension://gighmmpiobklfepjocnamgkkbiglidom/include.preload.js.map: HTTP error: status code 404, net::ERR_UNKNOWN_URL_SCHEME
Your routes work fine:
(let [routes ["" {"/this-route" {:get :some-handler}}]]
(is= (bidi/match-route routes "/this-route" :request-method :get)
{:handler :some-handler, :request-method :get}))
(let [routes ["" {"/this-route" {"" {:get :handler-1}
"/something" {:get :handler-2}
"/something-else" {:get :handler-3}}}]]
(is= (bidi/match-route routes "/this-route" :request-method :get)
{:handler :handler-1, :request-method :get})
(is= (bidi/match-route routes "/this-route/something" :request-method :get)
{:handler :handler-2, :request-method :get})
(is= (bidi/match-route routes "/this-route/something-else" :request-method :get)
{:handler :handler-3, :request-method :get}))
As cfrick said, you have somehow created a problem compiling and/or loading your code. That is where you need to look.

compojure wrap-json-body not working

I'm using the below code to try and access some json input in a PUT request however what I get returned has :body {}, I'm not sure what I'm doing wrong?
(ns compliant-rest.handler
(:use compojure.core ring.middleware.json)
(:require [compojure.handler :as handler]
[compojure.route :as route]
[ring.util.response :refer [response]]
[clojure.data.json :refer [json-str]]))
(defroutes app-routes
(PUT "/searches" {body :params} (response body))
(route/resources "/")
(route/not-found "Not Found"))
(def app
(-> (handler/site app-routes)
(wrap-json-body)
(wrap-json-response)))
(app {
:request-method :put
:uri "/searches"
:content-type "application/json"
:body (with-in-str (json-str {:field "value"}))
})
;; {:status 200, :headers {"Content-Type" "application/json; charset=utf-8"}, :body "{}"}
Also, I'm new to Clojure/Lisp, any comments about my syntax and style would be appreciated.
Two things stand out:
The unparsed request body is not supposed to be a string, but an InputStream. This means your test expression won't work as is.
wrap-json-body replaces (:body request) with a clojure data structure. It does not put anything in (:params request) or (:body (:params request)). You want wrap-json-params for that.
Thanks to Joost and the comments I found there is a ring function ring.util.io.string-input-stream that does what I mistakenly thought with-in-str did. Finally I had the following working:
(ns compliant-rest.handler
(:use compojure.core ring.middleware.json)
(:require [compojure.handler :as handler]
[compojure.route :as route]
[ring.util.response :refer [response]]
[ring.util.io :refer [string-input-stream]]
[clojure.data.json :refer [json-str]]))
(defroutes app-routes
(PUT "/searches/:id" {params :params body :body}
(response body))
(route/resources "/")
(route/not-found "Not Found"))
(def app
(-> (handler/site app-routes)
(wrap-json-body)
(wrap-json-response)))
;; Example request
(app {
:request-method :put
:uri "/searches/1"
:content-type "application/json"
:body (string-input-stream (json-str {:key1 "val1"}))
})
;; {:status 200, :headers {"Content-Type" "application/json; charset=utf-8"}, :body "{\"key1\":\"val1\"}"}
It's so awesome that I can just create a simple map and call my api's entry point without needing any sort of server or mocking. I'm totally being pulled into this whole dynamic languages thing with Clojure, the repl and light table!

How to write a simple error interceptor?

Here's my first attempt of a catcher interceptor on Pedestal:
(definterceptorfn catcher []
(interceptor
:error (fn [context error]
{:status 500
:body (->> error .toString (hash-map :error) json/write-str)
:headers {"Content-type" "application/json"}})))
As I could test, by adding (/ 1 0) to my code, the function does get called, but the client gets an empty response with status 200, instead of the response in the map. I wonder why it is so.
There is nothing fancy in my routes variable:
(defroutes routes
[[["/api"
^:interceptors [(body-params/body-params) (catcher) bootstrap/html-body]
...
As Tim Ewald explained, I was returning a response map when a context was needed.
Fixed with
(definterceptorfn catcher []
(interceptor
:error (fn [context error]
(assoc context :response
{:status 500
:body (->> error .toString (hash-map :error) json/write-str)
:headers {"Content-type" "application/json"}}))))

Compojure Routes Issues

I have a small compojure site, with the routes defined as such:
(defroutes example
(GET "/" [] {:status 200
:headers {"Content-Type" "text/html"}
:body (home)})
(GET "/*" (or (serve-file (params :*)) :next))
(GET "/execute/" [] {:status 200
:headers {"Content-Type" "text/html"}
:body (execute-changes)})
(GET "/status/" [] {:status 200
:headers {"Content-Type" "text/html"}
:body (status)})
(route/not-found "Page not found"))
When I try to load the project, I get this error:
java.lang.Exception: Unsupported binding form: (or (serve-file (params :*)) :next)
What am I doing wrong? I took most of this from scattered examples on the internet.
After adding the empty vector, I get this error:
java.lang.Exception: Unable to resolve symbol: serve-file in this context
I think you're missing a binding form:
(GET "/*" {params :params} (or (serve-file (params :*)) :next))
; ^- note the binding form