Billy Moon
Billy Moon

Reputation: 58619

avoid circular dependency when I access reitit route info from handler

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?

Upvotes: 1

Views: 416

Answers (2)

Crispin Wellington
Crispin Wellington

Reputation: 96

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.

  1. 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.

  2. 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))
  1. 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))
  1. 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))
  1. 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))
  1. 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))

Upvotes: 5

Alan Thompson
Alan Thompson

Reputation: 29984

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

Upvotes: 0

Related Questions