You see a tray full of interchangeable parts. You reach in and grab some and start putting things together. Something starts to form. You mostly like what you see but you need to swap out a few things. No problem, it's all super easy. You tinker a bit more and the next thing you know you have a working system.
This is partsbin - a set of reusable components and a philosophy for building simple systems.
The goal of this project is to:
ig
is used I am referring to [integrant.core :as ig]
.As such, it can be used in a few ways:
partsbin.system
, partsbin.middleware
, etc. but define your own ig/init-key
methods.ig/init-key
as well.Add the following dependency to your project.clj:
Since there are multiple frameworks out there for reloadable systems (e.g. Integrant, Component, Mount, I need to clarify the following for this discussion:
integrant.core/init
.To illustrate, here is a sample system with the various aspects labelled:
;The pre-initialized system configuration
(def config
{::jdbc/connection {:connection-uri "jdbc:h2:mem:mem_only"}
::datomic/database {:db-uri "datomic:mem://example"
:delete? true}
::datomic/connection {:database (ig/ref ::datomic/database)
:db-uri "datomic:mem://example"}
::web/server {:host "0.0.0.0"
:port 3000
:sql-conn (ig/ref ::jdbc/connection)
:datomic-conn (ig/ref ::datomic/connection)
:handler #'app}})
;The initialized configuration is a system
(def system (ig/init config))
;Component or part refers to the same concept
(def web-component (system ::web/server))
(def web-part (system ::web/server))
The overarching aim of partsbin is to provide a conceptual framework that facilitates functional, data-driven architectures. As such, here are some guiding principles and practices that will help to achieve this goal:
(myfunction datomic-conn arg)
- Just takes a connection and arguments with no regard to partsbin or the system map.(myfunction {:keys[datomic-conn]} arg)
- If it makes sense for the component to be a map, ensure that the keys are generic to what is being done in the calling function. datomic-conn
implies that this is a generic Datomic connection and is not system or config specific.(myfunction {:keys[project-x/conn]} arg)
- relies on knowing the configuration/system keys of your system.(myfunction datomic-conn args)
- Only the datomic connection is used here.(myfunction datomic-conn sql-conn args)
- Two different parts are used in two positions.(myfunction {:keys[datomic-conn sql-conn]} args)
- If multiple system components are needed for a single function, combine them using ig/ref to create a single, logical component with all required component.(myfunction {:keys[project-x/datomic-connection project-x/hornetq]} arg1 arg2)
- You are conflating your system with your function. This is making your code system-specific and non-generic.Suppose you want to test the following function. Note that it is not system-aware, but what you have is a system.
(defn doit [datomic-conn arg]
(let[db (d/db datomic-conn)]
(d/q query db arg)))
The right way to test this could be one of the following:
;Option 1: Doing this repeatedly in the REPL might be tedious.
(let[{conn ::datomic/connection} system]
(doit conn arg))
:Option 2: Do this once
(def datomic-conn (::datomic/connection system))
;Do this as much as you want.
(doit conn arg)
Currently, there are two small nses that I use pervasively when building systems that I've captured here. The first is partsbin.core
, which declares a simple protocol along the lines of what Component does along with some helper methods for modifying the default system configuration. For a similar effort see integrant-repl. The main difference I take in my approach is that I use an atom along with a simple protocol to manage the system rather than a dynamic var so that it becomes easier to localize the system (or have many) versus a single (start), (stop), etc. set of functions. If you like those other systems better, feel free to use them.
Should you want to try out my approach, it is as simple as what you see here:
(ns partsbin.example.example
(:require [partsbin.core :refer [create start stop restart system]]
[partsbin.datomic.api.core :as datomic]
[partsbin.immutant.web.core :as web]
[partsbin.clojure.java.jdbc.core :as jdbc]
[clojure.java.jdbc :as j]
[integrant.core :as ig]))
(defn app [{:keys [sql-conn] :as request}]
(let [res (j/query sql-conn "SELECT 1")]
{:status 200 :body (str "OK - " (into [] res))}))
(def config
{::jdbc/connection {:connection-uri "jdbc:h2:mem:mem_only"}
::datomic/database {:db-uri "datomic:mem://example"
:delete? true}
::datomic/connection {:database (ig/ref ::datomic/database)
:db-uri "datomic:mem://example"}
::web/server {:host "0.0.0.0"
:port 3000
:sql-conn (ig/ref ::jdbc/connection)
:datomic-conn (ig/ref ::datomic/connection)
:handler #'app}})
;Note that this is a different approach than what most reloadable systems do.
(defonce sys (create config))
(start sys)
;(stop sys)
The second ns I provide, partsbin.middleware
, provides an elegant and simple solution to the problem of making components available to your web handlers.
The typical web handler always boils down to something like this (be it hand-rolled, compojure, or reitit):
;Simple hand-rolled handler
(defn handler [request]
{:status 200 :body "OK"})
;Compojure fanciness
(defroutes app
(GET "/" [] (ok (html5 [:h1 "Hello World"])))
(GET "/time" [] (str "The time is: " (time/now)))
(route/not-found "<h1>Page not found</h1>"))
There may be fancy routes and so on, but ultimately it is a function that takes a request and returns a response. This is challenging to composable apps because you may need a db or other part of your application made available to you. I've seen solutions along the lines of:
(defn system-handler[component]
(routes
(GET "/" [] ...some sort of logic that uses the component...)))
This solution is pretty ugly. It still isn't clear how to inject the component into the system and your wrapped function is going to be created at every invocation.
Instead, consider this simple middleware and implementation (in this case using immutant):
(defn wrap-component [handler component]
(fn [request] (handler (into component request))))
(defmethod ig/init-key ::server [_ {:keys [handler host port] :as m}]
(immutant/run (wrap-component handler m) {:host host :port port}))
This solution pours the request into your configured component (the map used to create the server). I pour the request into the component so that if there are key collisions the request wins.
The result of this middleware is that when you declare keys like :datomic-conn (ig/ref ::datomic/connection)
the referenced component is now injected into your request and is available to your handler. Awesome, right!
Here's what it looks like:
(defn handler [{:keys [datomic-conn] :as request}]
(let [db (d/db datomic-conn)]
{:status 200 :body (d/q some-query db))}))
Notice that this follows the guiding principles outlined above. This is just a simple function in which I expect a request with a connection that I specified. There's no particular knowledge of partsbin or anything else. I just write the function and let some other aspect of the system worry about creating the right request.
An aim of partsbin is to provide an ever-accreting set of implementations of ig/init-key
and ig/halt-key!
for a variety of libraries that you might use in your systems. To ensure that this is done thoughtfully the following strategy has been taken:
clojure.java.jdbc
, the function clojure.java.jdbc/get-connection
is called from the clojure.java.jdbc
ns. In this case, I would create the package partsbin.clojure.java.jdbc
.clojure.java.jdbc.v0
corresponds to the latest released major version of this library (0.7.10 as of this writing). This version will not be produced until it is deemed stable. If a stable API version is determined to be wanting, a new ns should be created (vX_0 or whatever). If it corresponds to the latest version of the API, the core derivation should also be updated.Copyright © 2019 Mark Bastian
Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close