Assuming I have some kind of router set up that maps some routes to handlers something like this...
(ns myapp.user.api
(:require [reitit.core :as r]))
; define handlers here...
(def router
(r/router
[["/user" {:get {:name ::user-get-all
:handler get-all-users}}]
["/user/:id"
{:post {:name ::user-post
:handler user-post}}
{:get {:name ::user-get
:handler user-get}}]]))
And those handlers then call services that want access to the routing information...
(ns myapp.user-service
(:require [myapp.user.api :as api]))
; how can I get access to the route properties inside here..?
(defn get-all-users [])
(println (r/route-names api/router)))
When I try to import the router from the api file, into the service, I get a problem with circular dependencies, because the api requires handler, which requires service, so service can not then require api.
What's the best way to avoid this circular dependency? Can I look up values and properties of the router from within services?
I use six general approaches to avoid circular dependencies in clojure. They all have different tradeoffs and some situations one will fit better than another. I list them in order from what I prefer most to what I prefer least.
I show one example for each below. There may be more ways I haven't thought of, but hopefully this gives you some ways of thinking about the issue.
Refactor the code to remove the commonly referenced vars into a new namespace and require that namespace from both original namespaces. Often this is the best and simplest way. But can't be done here because the root handler var is a literal containing a var from the other namespace.
Pass in the dependent value into the function at runtime so as to avoid having to require the namespace literally.
(ns circular.a)
(defn make-handler [routes]
(fn []
(println routes)))
(ns circular.b
(:require [circular.a :as a]))
(def routes
{:handler (a/make-handler routes)})
;; 'run' route to test
((:handler routes))
Use multimethods to provide the dispatch mechanism, and then defmethod your binding from the other namespace.
(ns circular.a
(:require [circular.b :as b]))
(defmethod b/handler :my-handler [_]
(println b/routes))
(ns circular.b)
(defmulti handler identity)
(def routes
{:handler #(handler :my-handler)})
(ns circular.core
(:require [circular.b :as b]
;; now we bring in our handlers so as to define our method implementations
[circular.a :as a]))
;; 'run' route to test
((:handler b/routes))
Use a var literal that is resolved at runtime
(ns circular.a)
(defn handler []
(println (var-get #'circular.b/routes)))
(ns circular.b
(:require [circular.a :as a]))
(def routes
{:handler a/handler})
;; 'run' route to test
((:handler routes))
Move the code into the same namespace.
(ns circular.a)
(declare routes)
(defn handler []
(println routes))
(def routes
{:handler handler})
;; 'run' route to test
((:handler routes))
Use state. Store one of the values in an atom at runtime.
(ns circular.a
(:require [circular.c :as c]))
(defn handler []
(println #c/routes))
(ns circular.b
(:require [circular.a :as a]
[circular.c :as c]))
(def routes
{:handler a/handler})
(reset! c/routes routes)
((:handler routes))
(ns circular.c)
(defonce routes (atom nil))
You are making a simple mistake somewhere. My example:
(ns demo.core
(:use tupelo.core)
(:require
[reitit.core :as r]
[schema.core :as s]
))
(defn get-all-users [& args] (println :get-all-users))
(defn user-post [& args] (println :user-post))
(defn user-get [& args] (println :user-get))
; define handlers here...
(def router
(r/router
[
["/dummy" :dummy]
["/user" {:get {:name ::user-get-all
:handler get-all-users}}]
["/user/:id"
{:post {:name ::user-post
:handler user-post}}
{:get {:name ::user-get
:handler user-get}}]
]))
and use here:
(ns tst.demo.core
(:use demo.core tupelo.core tupelo.test)
(:require
[clojure.string :as str]
[reitit.core :as r]
))
(dotest
(spyx-pretty (r/router-name router))
(spyx-pretty (r/route-names router))
(spyx-pretty (r/routes router))
)
with result:
*************** Running tests ***************
:reloading (demo.core tst.demo.core)
Testing _bootstrap
-----------------------------------
Clojure 1.10.3 Java 15.0.2
-----------------------------------
Testing tst.demo.core
(r/router-name router) =>
:lookup-router
(r/route-names router) =>
[:dummy]
(r/routes router) =>
[["/dummy" {:name :dummy}]
["/user"
{:get
{:name :demo.core/user-get-all,
:handler
#object[demo.core$get_all_users 0x235a3fc "demo.core$get_all_users#235a3fc"]}}]]
Ran 2 tests containing 0 assertions.
0 failures, 0 errors.
based on my favorite template project
Related
I have a (routes (route/not-found)) definition with value derived from an (atom). Though I've updated the atom, the routing retains the initial value. This is similiar to Dynamic handler update in Clojure Ring/Compojure REPL but I'm having a hard time understanding what needs to be de/referenced where.
(ns mveroute
(:require
[org.httpkit.server :as srv]
[compojure.core :as cmpj]
[compojure.route :as route]
[clj-http.client :as client])
(:gen-class))
(def my-atom (atom "foobar"))
(def app
(cmpj/routes
(route/not-found {:status 400 :body #my-atom})))
(defn -main [& args]
(reset! my-atom "hello world")
(srv/run-server #'app {:port 8005})
;; "hello world" as expected
(println #my-atom)
;; still "foobar" but wanted "hello world"
(-> "http://localhost:8005"
(client/get {:throw-exceptions? false})
:body
println))
I thought warp-routes might have come to the rescue. But not how I've used it.
(defn atom-body [] {:status 200 :body #my-atom})
(cmpj/defroutes wrap-found
(route/not-found (atom-body)))
(def app
(cmpj/wrap-routes #'wrap-found {}))
The ultimate goal is a simple cli application that can set the resource/html root directory with command line arguments.
Try something like the following:
(defn not-found-fn
[req]
{:status 400 :body "not found again!"})
(def app
(cmpj/routes
(route/not-found not-found-fn)))
So inside of not-found-fn, you can construct the :body string any way you like. You could also have the string stored in an atom which is dereferenced by not-found-fn.
Side note:
Please see the following to clarify when you should use a Var object instead of just a symbol in your code:
When to use a Var instead of a function?
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'm trying to split code in 2 files, each with it's own namespace. Following this tutorial.
But I get this error:
Exception in thread "main" java.lang.IllegalArgumentException: Don't know how to create ISeq from: clojure.lang.Keyword
I think it's because the namespace being included is not recognised properly.
Main file:
(ns mytest2.handler
(:use compojure.core)
(:require [compojure.handler :as handler]
[compojure.route :as route]
[mytest2.views :as foo] ;<-- line causing error
[hiccup.core :refer (html)])
)
(defn layout [title & content]
(html
[:head [:title title]]
[:body content]))
(defn main-page []
(layout "My Blog"
[:h1 "My Blog"]
[:p "Welcome to my page"]))
(defroutes app-routes
(GET "/" [] (main-page))
(route/resources "/")
(route/not-found "Not Found"))
(def app
(handler/site app-routes))
; (println (seq (.getURLs (java.lang.ClassLoader/getSystemClassLoader))))
Second file:
(ns mytest2.views
:require [hiccup.core :refer (html)]
)
(defn layout [title & content]
(html
[:head [:title title]]
[:body content]))
(defn main-page []
(layout "My Blog"
[:h1 "My Blog"]
[:p "Welcome to my page"]))
(note I copied the functions from mytest2.views in mytest2.handler for testing. They're not supposed to be in mytest2.handler).
Paths of files:
/mytest2/src/mytest2/handler.clj
/mytest2/src/mytest2/views.clj
(where first mytest2 is the name of the project, and the second is part of the path- automatically created by lein).
As you see in the first file I printed the class path to verify that /mytest2/src/mytest2/ is included, and yes it is.
Received the same error from trying to use :refer :all in Clojurescript, which apparently is against the rules.
You missed some brackets in your original code
;; wrong
(ns mytest2.views
:require [hiccup.core :refer [html]])
There is just one pair of brackets missing. Do it as in your Main file:
;; Done right!
(ns mytest2.views
(:require [hiccup.core :refer [html]]))
I am not familar with Compojure so I do not know what you have to require. But you need to add the bracket around :require.
I'm writing a Compojure application and am using clj-webdriver to graphically test it. I'm trying to use with-redefs to mock out the function that pulls out data from persistence to just return canned values, but it's ignoring my function overwrite. I know with-redefs works in terms of vars, but it's still not working:
project.clj relevant pieces:
(defproject run-hub "0.1.0-SNAPSHOT"
:main run-hub.handler/start-server)
handler.clj:
(ns run-hub.handler
(:require [compojure.core :refer :all]
[compojure.handler :as handler]
[compojure.route :as route]
[ring.adapter.jetty :refer :all]
[run-hub.controllers.log-controller :as log-controller]))
(defroutes app-routes
(GET "/MikeDrogalis/log" [] (log-controller/mikes-log))
(route/resources "/")
(route/not-found "Not Found"))
(def app (handler/site #'app-routes))
log-controller.clj:
(ns run-hub.controllers.log-controller
(:require [run-hub.views.log :as views]
[run-hub.persistence :as persistence]))
(defn mikes-log []
(views/mikes-log (persistence/mikes-log)))
persistence.clj
(ns run-hub.persistence
(require [clj-time.core :as time]
[run-hub.models.log :as log]))
(defn mikes-log [] [])
And finally, my graphical test - which tries to override mikes-log and fails:
(fact
"It has the first date of training as August 19, 2012"
(with-redefs [persistence/mikes-log (fn [] (one-week-snippet))]
(to (local "/MikeDrogalis/log"))
(.contains (text "#training-log") "August 19, 2012"))
=> true)
Where one-week-snippet is a function that returns some sample data.
(defn start-server []
(run-jetty (var app) {:port 3000 :join? false}))
I am able to use with-redefs in a clj-webdriver test doing the following:
(defn with-server
[f]
(let [server (run-jetty #'APP {:port 0 :join? false})
port (-> server .getConnectors first .getLocalPort)]
(binding [test-port port]
(try
(println "Started jetty on port " test-port)
(f)
(finally
(.stop server))))))
(use-fixtures :once with-server)
Then the whole bunch of tests gets its own jetty and this seems to run in
such a manner that with-redefs works.