stuartsierra/component

Multiple dependencies of same "type"

jakepearson opened this issue · 1 comments

Hi! Is there a way to define multiple components that implement some kind of "interface" and then be able to query a system for all the components that meet those criteria? I want to find all the matching components without knowing the list in advance. In my case, I have a bunch of components that can handle messages of different types. I want to have one component that listens for the messages then routes them to the correct handler.

I wrote a sample below that appears to be a working solution, but seems kind of yucky with the atom. Ideas?

(ns teller.component-test
  (:require [clojure.test :refer [deftest is]]
            [com.stuartsierra.component :as component]))

(defrecord Accumulator []
  component/Lifecycle
  (start [this]
    (assoc this :atom (atom {})))
  (stop [this]
    (assoc this :atom nil)))

(defrecord Handler [text accumulator]
  component/Lifecycle
  (start [this]
    (let [lookup (:atom accumulator)]
      (swap! lookup assoc (-> (str "handler-" text) keyword) text)
      this))
  (stop [this]
    (update-in this [:handlers] dissoc :lookup)))

(defrecord Handlers [accumulator]
  component/Lifecycle
  (start [this]
    (let [lookup @(:atom accumulator)]
      (assoc this :lookup lookup)))
  (stop [this]
    (assoc this :lookup nil)))

(defrecord Mapper [handlers]
  component/Lifecycle
  (start [this]
    (println handlers)
    this)

  (stop [this]
    this))

(defn build-system []
  (component/system-map
   :accumulator (map->Accumulator {})
   :handler-a (component/using (map->Handler {:text "a"})
                               [:accumulator])
   :handler-b (component/using (map->Handler {:text "b"})
                               [:accumulator])
   :handlers (component/using (map->Handlers {})
                              [:accumulator])
   :mapper (component/using (map->Mapper {})
                            [:handlers])))

(defn start []
  (component/start (build-system)))

(deftest should-start
  (let [system (start)]
    (is (= {:handler-a "a"
            :handler-b "b"}
           (get-in system [:handlers :lookup])))))

Worked on it some more and came up with a solution we are pretty happy with ...

(ns teller.component-test
  (:require [clojure.test :refer [deftest is]]
            [com.stuartsierra.component :as component]
            [taoensso.timbre :as log]))

(defprotocol MappableHandler
  (get-label [this])
  (get-handler-fn [this]))

(defrecord Handler [db label handler]
  component/Lifecycle
  (start [this]
    this)
  (stop [this]
    this)

  MappableHandler
  (get-label [_this]
    label)
  (get-handler-fn [_this]
    handler))

(defrecord Mapper [db handler-labels]
  component/Lifecycle
  (start [this]
      ; get the injected handlers
    (let [handlers (into {}
                         (map (fn [label]
                                (let [component (get this label)
                                      handler-label (get-label component)
                                      handler-fn (get-handler-fn component)]
                                  [handler-label handler-fn])))
                         handler-labels)]
      (log/info "handlers" handlers)
      (assoc this :handlers handlers)))
  (stop [this]
    (assoc this :handlers nil)))

(defn create-system []
  (component/system-map
   :a (component/using (->Handler nil :handler-a (fn [_x] "a"))
                       [:db])
   :b (component/using (->Handler nil :handler-b (fn [_x] "b"))
                       [:db])
   :db {:conn "Database"}))

(defn add-mapper [system]
  (log/info "add-mapper")
  (let [handler-labels (into []
                             (comp (filter (fn [[_label component]]
                                             (log/info "component" component)
                                             (satisfies? MappableHandler component)))
                                   (map (fn [[label _component]]
                                          label)))
                             system)
        dep-comps (into [:db] handler-labels)]
    (log/info "after")
    (assoc system :mapper (component/using (->Mapper nil handler-labels)
                                           dep-comps))))

(defn start []
  (component/start (add-mapper (create-system))))

(deftest should-start
  (let [system (start)]
    (is (= [:handler-a :handler-b]
           (-> system
               (get-in [:mapper :handlers])
               keys)))))