I am learning clojure, and I've hit a snag while trying to refactor my web app to make it more functional and less reliant on global state.
The way you make a simple auto-reloading server with ring framework is like this:
(defn handler [req]
{:status 200
:body "<h1>Hello World</h1>"
:headers {}})
(defn -main []
(jetty/run-jetty (wrap-reload #'handler)
{:port 12345}))
(source: https://practicalli.github.io/clojure-webapps/middleware-in-ring/wrap-reload.html)
So handler is a global function. It is given as a var reference to wrap-reload. On each request, wrap-reload will reload the entire namespace, and then re-resolve handler reference to a potentially different function. Or at least this is my understanding.
This is all good in this simple example. The trouble starts when I replace this hello world handler with my actual handler, which has all sorts of state bound into it (eg. database connection). This state is initialized once inside -main, and then injected into the routing stack. Something like this:
(defn init-state [args dev]
(let
[db (nth args 1 "jdbc:postgresql://localhost/mydb")
routes (make-routes dev)
app (make-app-stack routes db)
{:port (Integer. (nth args 0 3000))
:route routes
:dev dev
:db db
:app app})
(defn start [state]
(println "Running in " (if (:dev state) "DEVELOPMENT" "PRODUCTION") " mode")
(model/create-tables (:db state))
(jetty/run-jetty (:app state) {:port (:port state)}))
(defn -main [& args]
(start (init-state args false)))
make-routes generates the router based on compojure library, and make-app-stack then wraps this router into a bunch of middlewares, which will inject global state (like DB string) for the use by handlers.
So how do I add wrap-reload to this setup? #'app macro won't work on local let "variable" (or whatever it's called). Do I need to expose my app as a global variable? Or can I re-generate the entire closure on each request.
My instincts tell me to avoid having global initialization code in the module body and keep all code in the main, but maybe I am overthinking. Should I just type my init-state code as globals and call it a day, or is there a better way?
I came up with a solution I can live with.
(ns webdev.core
(:require [ring.middleware.reload :as ring-reload]))
; Defeat private defn
(def reloader #'ring-reload/reloader)
; Other stuff...
(defn load-settings [args dev]
{:port (Integer. (nth args 0 3000))
:db (nth args 1 "jdbc:postgresql://localhost/webdev")
:dev dev})
(defn make-reloading-app [settings]
(let [reload! (reloader ["src"] true)]
(fn [request]
(reload!)
((make-app settings) request))))
(defn start [settings]
(let
[app
(if (:dev settings)
(make-reloading-app settings)
(make-app settings))]
(println "Running in " (if (:dev settings) "DEVELOPMENT" "PRODUCTION") " mode")
(model/create-tables (:db settings))
(jetty/run-jetty app {:port (:port settings)})))
(defn -main [& args]
(start (load-settings args false)))
Full code is available here: https://github.com/panta82/clojure-webdev/blob/master/src/webdev/core.clj
Instead of using wrap-reload directly, I use the underlying private reload function. I have to recreate the routing stack in every request, but it seems to work fine in dev. No global state needed :)
Related
I keep getting "Invalid anti-forgery token" when wrapping specific routes created with metosin/reitit reitit.ring/ring-router. I've also tried reitit's middleware registry, but it didn't work too. Although I could just wrap the entire handler with wrap-session and wrap-anti-forgery, that defeats the reitit's advantage on allowing route-specific middleware.
(ns t.core
(:require [immutant.web :as web]
[reitit.ring :as ring]
[ring.middleware.anti-forgery :refer [wrap-anti-forgery]]
[ring.middleware.content-type :refer [wrap-content-type]]
[ring.middleware.params :refer [wrap-params]]
[ring.middleware.keyword-params :refer [wrap-keyword-params]]
[ring.middleware.session :refer [wrap-session]]
[ring.util.anti-forgery :refer [anti-forgery-field]]
[ring.util.response :as res]))
(defn render-index [_req]
(res/response (str "<form action='/sign-in' method='post'>"
(anti-forgery-field)
"<button>Sign In</button></form>")))
(defn sign-in [{:keys [params session]}]
(println "params: " params
"session:" session)
(res/redirect "/index.html"))
(defn wrap-af [handler]
(-> handler
wrap-anti-forgery
wrap-session
wrap-keyword-params
wrap-params))
(def app
(ring/ring-handler
(ring/router [["/index.html" {:get render-index
:middleware [[wrap-content-type]
[wrap-af]]}]
["/sign-in" {:post sign-in
:middleware [wrap-af]}]])))
(defn -main [& args]
(web/run app {:host "localhost" :port 7777}))
It turns out that metosin/reitit creates one session store for each route (refer to issue 205 for more information); in other words, ring-anti-forgery is not working because reitit does not use the same session store for each route.
As of the time of this answer, the maintainer suggests the following (copied from the issue for ease of reference within Stack Overflow):
mount the wrap-session outside of the router so there is only one instance of the mw for the whole app. There is :middleware option in ring-handler for this:
(require '[reitit.ring :as ring])
(require '[ring.middleware.session :as session])
(defn handler [{session :session}]
(let [counter (inc (:counter session 0))]
{:status 200
:body {:counter counter}
:session {:counter counter}}))
(def app
(ring/ring-handler
(ring/router
["/api"
["/ping" handler]
["/pong" handler]])
(ring/create-default-handler)
;; the middleware on ring-handler runs before routing
{:middleware [session/wrap-session]}))
create a single session store and use it within the routing table (all instances of the session middleware will share the single store).
(require '[ring.middleware.session.memory :as memory])
;; single instance
(def store (memory/memory-store))
;; inside, with shared store
(def app
(ring/ring-handler
(ring/router
["/api"
{:middleware [[session/wrap-session {:store store}]]}
["/ping" handler]
["/pong" handler]])))
Not shown in this answer is the third option that the maintainer calls for PR.
I am having a little trouble starting my app.
Here is my core.clj
(ns myapp.core
(:require [yada.yada :as yada :refer [resource as-resource]]
[yada.resources.file-resource :refer [new-directory-resource]]
[aero.core :refer [read-config]]
[web.view :as view]
[web.routes :as routes]
[clojure.java.io :as io]
[aero.core :refer [read-config]]
[com.stuartsierra.component :as component]
[clojure.java.jdbc :as jdbc]
[clojure.tools.namespace.repl :refer (refresh)]
[ring.adapter.jetty :as jetty]
[environ.core :refer [env]]))
(defrecord Listener [listener]
component/Lifecycle
(start [component]
(assoc component :listener (yada/listener
["/"
[(view/view-route)
routes/route-handler
["public/" (new-directory-resource (io/file "target/cljsbuild/public") {})]
[true (as-resource nil)]]] )))
(stop [component]
(when-let [close (-> component :listener :close)]
(close))
(assoc component :listener nil)))
(defn new-system []
(component/system-map
:listener (map->Listener {})
))
(def system nil)
(defn init []
(alter-var-root #'system
(constantly (new-system))))
(defn start []
(alter-var-root #'system component/start))
(defn stop []
(alter-var-root #'system
(fn [s] (when s (component/stop s)))))
(defn go []
(init)
(start))
(defn reset []
(stop)
(refresh :after 'web.core/go))
(defn -main
[& [port]]
(let [port (Integer. (or port (env :port) 3300))]
(jetty/run-jetty (component/start (new-system)) {:port port :join? false})))
I am testing out Stuart Sierra's library, component.
I can start the app if I do lein repl and (go) but I am trying to start my app by running lein run (to see what the app is like if I deployed it in production). When I do lein run in the browser I get the error
HTTP ERROR: 500
Problem accessing /view. Reason:
com.stuartsierra.component.SystemMap cannot be cast to clojure.lang.IFn
I am confused because I don't know why the system-map (in new-system) is the error. I'm also not sure what the error means so I don't know how to fix it
Could someone please help. Thanks
Your -main function calls jetty/run-jetty function first argument of which must be a Ring handler - function which accepts request map and produces response map. You're passing a system instead which leads to the exception. Exception means that jetty adapter tries to call passed system as a function, but can't, because system is actually a record and doesn't implement function interface IFn.
I'm not that familiar with yada, but it looks like yada/listener starts the (Aleph) server, so there's no need to explicitly call the jetty adapter. Your main should look something like this:
(defn -main [& [port]]
(component/start (new-system)))
Port (or any other config) could be passed as an argument to the new-system and then forwarded to components requiring it (in your case port should be passed down to the Listener and then to yada/listener call in start implementation).
I have a function that I would like to test.
(defn -main
[& args]
(let [port (Integer/parseInt (or (first args) "8080"))]
(run-jetty
app
{:port port})))
I need to mock away run-jetty for obvious reasons, so I use with-redefs to verify that the correct args are passed. This works great.
(it "should overide the port with first arg"
(with-redefs [run-jetty (fn [handler opts] [handler opts])]
(should= 8081 (jetty-port (underTest/-main "8081")))))
I'd like to extract the with-redefs call since I use it in a few more tests, so I tried this:
(defn mock-jetty
[test]
(with-redefs [run-jetty (fn [handler args] [handler args])] test))
(describe "The app"
(it "should default to port 8080"
(mock-jetty
(should= 8080 (jetty-port (underTest/-main))))))
This test calls the real run-jetty function and starts a server. Why does this happen?
EDIT
Changing extraction to a macro:
(defmacro mock-jetty
[someTest]
`(with-redefs [run-jetty (fn [handler args] [handler args])] ~someTest))
(describe "The app"
(it "should default to port 8080"
(mock-jetty
(should= 8080 (jetty-port (underTest/-main))))))
throws this exception:
Can't use qualified name as parameter: boost-bin.handler-test/handler
I can guess, that it is connected with the fact that with-redefs is a macro. So when you call it straight in code, it executes test body in context of redefs, while when you move it out to the function, the body (should= 8080 (jetty-port (underTest/-main))) is being executed before being passed to macro (as it always happens to functions' params)
To make it work, you could rewrite mock-jetty as a macro:
(defmacro mock-jetty
[test]
`(with-redefs [run-jetty (fn [handler# args#] [handler# args#])] ~test))
I'm pretty sure it would help
I have this code to get data from sumo logic and other services.
core.clj has this, which parses the arguments and routes it to the right function in route.clj
(def cli-options
[
["-a" "--app APPNAME" "set app. app can be:
sumologic or jira"]
["-?" "--help"]
])
(defn -main
[& args]
(let [{:keys [options summary errors arguments]} (parse-opts args cli-options)]
(cond
(:app options) (route/to (:app options) options arguments)
:else (print_usage summary))))
route.clj has this:
(defn to
[app options arguments]
(case app
"jira" (jira/respond options arguments)
"sumologic" (sumo/respond)))
And then sumo.clj has this. there are other functions, of course, but showing just the relevant parts.
(defn get-env-var
[var]
(let [result (System/getenv var)]
(if (nil? result)
(throw (Exception. (str "Environment variable: " var " not set. Aborting")))
result)))
(def access_key
(let [user (get-env-var "SUMO_ID")
pass (get-env-var "SUMO_KEY")]
[user pass]))
(defn respond
[]
(let [{:keys [status body error] :as response} (http/get endpoint rest-options)]
(if error
(println error)
(print-response body))))
When I run the program using leiningen as lein run -- -? or even just lein run, I get this error, even though I haven't explicitly called the sumologic function. What am I doing wrong and what are things that I can do differently?
Caused by: java.lang.Exception: Environment variable: SUMO_KEY not set. Aborting
at clarion.sumo$get_env_var.invoke(sumo.clj:14)
at clarion.sumo$fn__3765.invoke(sumo.clj:19)
at clojure.lang.AFn.applyToHelper(AFn.java:152)
at clojure.lang.AFn.applyTo(AFn.java:144)
at clojure.lang.Compiler$InvokeExpr.eval(Compiler.java:3553)
You have def'd access_key so it is being evaluated when you load the application. You probably want to make it a function instead.
I have the following file that acts as the access point to my DB from my API endpoints. What is the proper way to maintain a single connection (or a connection pool?) for the life of the server?
(ns my-api.repository
(:require [clojure.java.jdbc :as sql]))
(defn some-query
(sql/with-connection (System/getenv "DATABASE_URL")
(sql/with-query-results results
;; You get the picture
)))
the DATABASE_URL is stored, if you use with-connection macro for single connection. you can use c3p0 library for connection pool:
(defn pooled-spec
"return pooled conn spec.
Usage:
(def pooled-db (pooled-spec db-spec))
(with-connection pooled-db ...)"
[{:keys [classname subprotocol subname user password] :as other-spec}]
(let [cpds (doto (ComboPooledDataSource.)
(.setDriverClass classname)
(.setJdbcUrl (str "jdbc:" subprotocol ":" subname))
(.setUser user)
(.setPassword password))]
{:datasource cpds}))
I also recommend c3p0.
Clojure connection pooling
provides a succinct introduction to how to configure clojure.java.jdbc with c3p0.
jdbc-pool simplifies the process of using C3P0 with clojure.java.jdbc. I created the library specifically (and only) for this purpose.
I do not think with-connection holds anything open. Doc nor source suggest it, and in 2.3 I was able to confirm it by inspecting db after running queries. The source looks like this:
(defn with-connection*
"Evaluates func in the context of a new connection to a database then
closes the connection."
[db-spec func]
(with-open [^java.sql.Connection con (get-connection db-spec)]
(binding [*db* (assoc *db* :connection con :level 0 :rollback (atom false))]
(func))))
Connection pooling may be useful in lazily creating them, but that doesn't hold them open. Making the connection settable seems to be necessary. However the latest API emphasizes just creating the connection and passing it into each call. While still ALPHA, this looks like the future for this library (and is still compatible with connection pooling). From the library's wiki:
(require '[clojure.java.jdbc :as j]
'[clojure.java.jdbc.sql :as s])
(def mysql-db {:subprotocol "mysql"
:subname "//127.0.0.1:3306/clojure_test"
:user "clojure_test"
:password "clojure_test"})
(j/insert! mysql-db :fruit
{:name "Apple" :appearance "rosy" :cost 24}
{:name "Orange" :appearance "round" :cost 49})