diff --git a/examples/integrant-system/bases/system/deps.edn b/examples/integrant-system/bases/system/deps.edn new file mode 100644 index 000000000..9e929552e --- /dev/null +++ b/examples/integrant-system/bases/system/deps.edn @@ -0,0 +1,7 @@ +{:paths ["src" "resources"] + :deps {com.zaxxer/HikariCP {:mvn/version "5.1.0"} + integrant/integrant {:mvn/version "0.10.0"} + org.clojure/tools.logging {:mvn/version "1.2.4"} + org.clojure/tools.namespace {:mvn/version "1.3.0"}} + :aliases {:test {:extra-paths ["test"] + :extra-deps {}}}} diff --git a/examples/integrant-system/bases/system/src/integrant/system/config.clj b/examples/integrant-system/bases/system/src/integrant/system/config.clj new file mode 100644 index 000000000..e4385cffd --- /dev/null +++ b/examples/integrant-system/bases/system/src/integrant/system/config.clj @@ -0,0 +1,11 @@ +(ns + ^{:author "Mark Sto"} + integrant.system.config + (:require [clojure.tools.logging :as log] + [integrant.config.interface :as config] + [integrant.core :as ig])) + +(defmethod ig/init-key :integrant.system/config + [_ _] + (log/info "Loading configuration") + (config/load-config)) diff --git a/examples/integrant-system/bases/system/src/integrant/system/core.clj b/examples/integrant-system/bases/system/src/integrant/system/core.clj new file mode 100644 index 000000000..339bb8426 --- /dev/null +++ b/examples/integrant-system/bases/system/src/integrant/system/core.clj @@ -0,0 +1,52 @@ +(ns + ^{:author "Mark Sto"} + integrant.system.core + (:require [clojure.pprint :as pp] + [clojure.tools.logging :as log] + [integrant.core :as ig] + [integrant.system.state :as state])) + +;; Integrant System + +(def default-ig-config + {:integrant.system/config {} + :integrant.system/embedded-pg {:config (ig/ref :integrant.system/config) + #_#_:log-file "path/to/pg-logs-redirection"} + :integrant.system/data-source {:config (ig/ref :integrant.system/config) + :postgres (ig/ref :integrant.system/embedded-pg)}}) + +(defn halt-system! [] + (state/stop!) + ::stopped) + +(defn init-system! + ([] + (init-system! default-ig-config)) + ([ig-config] + (log/info "Starting system with config:\n" + (with-out-str (pp/pprint ig-config))) + (try + (halt-system!) + (state/start! ig-config) + ::started + (catch Exception ex + (log/error ex "Failed to start the system") + (halt-system!) + ::failed-to-start)))) + +;; Base API + +(defn shutdown! + [] + (let [halt-res (halt-system!)] + (shutdown-agents) + halt-res)) + +(defn launch! + [] + (.addShutdownHook (Runtime/getRuntime) (Thread. shutdown!)) + (init-system!)) + +(defn -main + [& _] + (launch!)) diff --git a/examples/integrant-system/bases/system/src/integrant/system/data_source.clj b/examples/integrant-system/bases/system/src/integrant/system/data_source.clj new file mode 100644 index 000000000..f3aed853d --- /dev/null +++ b/examples/integrant-system/bases/system/src/integrant/system/data_source.clj @@ -0,0 +1,39 @@ +(ns + ^{:author "Mark Sto"} + integrant.system.data_source + (:require [clojure.tools.logging :as log] + [integrant.core :as ig] + [integrant.pg-ops.interface :as pg-ops]) + (:import (com.zaxxer.hikari HikariDataSource))) + +(defn -conn-pool + ^HikariDataSource + [{:keys [classname + subprotocol + subname + user + password] + :as db-spec}] + (log/info "DB Spec:" db-spec) + (let [conn-pool (doto (HikariDataSource.) + (.setDriverClassName classname) + (.setJdbcUrl (str "jdbc:" subprotocol ":" subname)) + (.setUsername user) + (.setPassword password))] + ;; NB: Initializing the pool and performing a validation check. + (.close (.getConnection conn-pool)) + conn-pool)) + +(defmethod ig/init-key :integrant.system/data-source + [_ {:keys [config]}] + (log/info "Initializing JDBC DataSource") + (let [db-spec (-> (:db+creds config) + (assoc :port (get-in config [:postgres :port])) + (pg-ops/->db-spec))] + (-conn-pool db-spec))) + +(defmethod ig/halt-key! :integrant.system/data-source + [_ ^HikariDataSource data-source] + (log/info "Closing JDBC DataSource") + (when data-source + (.close data-source))) diff --git a/examples/integrant-system/bases/system/src/integrant/system/embedded_pg.clj b/examples/integrant-system/bases/system/src/integrant/system/embedded_pg.clj new file mode 100644 index 000000000..590393fb1 --- /dev/null +++ b/examples/integrant-system/bases/system/src/integrant/system/embedded_pg.clj @@ -0,0 +1,20 @@ +(ns + ^{:author "Mark Sto"} + integrant.system.embedded-pg + (:require [clojure.tools.logging :as log] + [integrant.core :as ig] + [integrant.embedded-pg.interface :as embedded-pg])) + +(defmethod ig/init-key :integrant.system/embedded-pg + [_ {:keys [config log-file]}] + (let [pg-config {:port (get-in config [:postgres :port]) + :log-file log-file}] + (log/info "Initializing Postgres with config:" pg-config) + (embedded-pg/start-postgres! pg-config))) + +(defmethod ig/halt-key! :integrant.system/embedded-pg + [_ embedded-pg] + (when embedded-pg + (log/info "Halting Postgres") + (embedded-pg/stop-postgres! embedded-pg) + nil)) diff --git a/examples/integrant-system/bases/system/src/integrant/system/state.clj b/examples/integrant-system/bases/system/src/integrant/system/state.clj new file mode 100644 index 000000000..3a6e1b721 --- /dev/null +++ b/examples/integrant-system/bases/system/src/integrant/system/state.clj @@ -0,0 +1,36 @@ +(ns + ^{:author "Mark Sto"} + integrant.system.state + (:require [clojure.tools.logging :as log] + [clojure.tools.namespace.repl :as repl] + [integrant.core :as ig])) + +(repl/disable-reload!) + +(def *state + "Stores the current Integrant system and its config." + (atom nil)) + +(defn start! + [ig-config] + (when ig-config + (log/info "Starting Integrant system") + (ig/load-namespaces ig-config) + (reset! *state {:system (ig/init ig-config) + :config ig-config}))) + +(defn stop! + [] + (log/info "Stopping Integrant system") + (-> (swap-vals! *state + (fn [{:keys [system]}] + (when system + (ig/halt! system)) + nil)) + (first) + :config)) + +(defn restart! + [] + (let [ig-config (stop!)] + (start! ig-config))) diff --git a/examples/integrant-system/bases/system/test/integrant/system/core_test.clj b/examples/integrant-system/bases/system/test/integrant/system/core_test.clj new file mode 100644 index 000000000..a7b4bcc3c --- /dev/null +++ b/examples/integrant-system/bases/system/test/integrant/system/core_test.clj @@ -0,0 +1,43 @@ +(ns + ^{:author "Mark Sto"} + integrant.system.core-test + (:require [clojure.string :as str] + [clojure.test :refer :all] + [integrant.pg-ops.interface :as pg-ops] + [integrant.system.core :as system] + [integrant.system.state :as state])) + +(defn- get-pg-version + [] + (-> @state/*state + (get-in [:system :integrant.system/data-source]) + (pg-ops/query-version))) + +(deftest integrant-system-lifecycle + (testing "System launch succeeds" + (is (= ::system/started (system/launch!))) + (let [system-state @state/*state] + (is (map? system-state)) + (is (contains? system-state :system)) + (is (contains? system-state :config)))) + + (testing "All system components are initialized and functional" + (is (str/starts-with? (get-pg-version) "PostgreSQL"))) + + (testing "System restart succeeds and config does not change" + (let [old-config (get @state/*state :config) + new-state (state/restart!)] + (is (map? new-state)) + (is (contains? new-state :system)) + (is (contains? new-state :config)) + (is (= old-config (:config new-state))))) + + (testing "System shutdown succeeds" + (is (= ::system/stopped (system/shutdown!))) + (let [system-state @state/*state] + (is (nil? system-state)) + (is (thrown? Exception (get-pg-version)))))) + +(comment + (run-tests) + .) diff --git a/examples/integrant-system/components/config/deps.edn b/examples/integrant-system/components/config/deps.edn new file mode 100644 index 000000000..d34e348e0 --- /dev/null +++ b/examples/integrant-system/components/config/deps.edn @@ -0,0 +1,4 @@ +{:paths ["src" "resources"] + :deps {} + :aliases {:test {:extra-paths ["test"] + :extra-deps {}}}} diff --git a/examples/integrant-system/components/config/resources/.keep b/examples/integrant-system/components/config/resources/.keep new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/examples/integrant-system/components/config/resources/.keep @@ -0,0 +1 @@ + diff --git a/examples/integrant-system/components/config/src/integrant/config/core.clj b/examples/integrant-system/components/config/src/integrant/config/core.clj new file mode 100644 index 000000000..7904f4885 --- /dev/null +++ b/examples/integrant-system/components/config/src/integrant/config/core.clj @@ -0,0 +1,11 @@ +(ns + ^{:author "Mark Sto"} + integrant.config.core) + +;; NB: This should normally use some library to load a system configuration map. +;; However, we keep things simple for demonstration purposes. +(defn load-config [] + {:postgres {:port 54321} + :db+creds {:dbname "postgres" + :user "postgres" + :password "postgres"}}) diff --git a/examples/integrant-system/components/config/src/integrant/config/interface.clj b/examples/integrant-system/components/config/src/integrant/config/interface.clj new file mode 100644 index 000000000..728860294 --- /dev/null +++ b/examples/integrant-system/components/config/src/integrant/config/interface.clj @@ -0,0 +1,9 @@ +(ns + ^{:doc "An abstract system configuration as a Polylith component" + :author "Mark Sto"} + integrant.config.interface + (:require [integrant.config.core :as core])) + +(defn load-config + [] + (core/load-config)) diff --git a/examples/integrant-system/components/config/test/integrant/config/interface_test.clj b/examples/integrant-system/components/config/test/integrant/config/interface_test.clj new file mode 100644 index 000000000..f8d87bd43 --- /dev/null +++ b/examples/integrant-system/components/config/test/integrant/config/interface_test.clj @@ -0,0 +1,6 @@ +(ns integrant.config.interface-test + (:require [clojure.test :as test :refer :all] + [integrant.config.interface :as config])) + +(deftest dummy-test + (is (= 1 1))) diff --git a/examples/integrant-system/components/embedded-pg/deps.edn b/examples/integrant-system/components/embedded-pg/deps.edn new file mode 100644 index 000000000..f065d4ede --- /dev/null +++ b/examples/integrant-system/components/embedded-pg/deps.edn @@ -0,0 +1,4 @@ +{:paths ["src"] + :deps {io.zonky.test/embedded-postgres {:mvn/version "2.0.7"}} + :aliases {:test {:extra-paths ["test"] + :extra-deps {}}}} diff --git a/examples/integrant-system/components/embedded-pg/src/integrant/embedded_pg/core.clj b/examples/integrant-system/components/embedded-pg/src/integrant/embedded_pg/core.clj new file mode 100644 index 000000000..931f54720 --- /dev/null +++ b/examples/integrant-system/components/embedded-pg/src/integrant/embedded_pg/core.clj @@ -0,0 +1,29 @@ +(ns + ^{:author "Mark Sto"} + integrant.embedded-pg.core + (:require [clojure.java.io :as io]) + (:import + (io.zonky.test.db.postgres.embedded EmbeddedPostgres EmbeddedPostgres$Builder) + (java.lang ProcessBuilder$Redirect))) + +(defn ->embedded-pg-builder + ^EmbeddedPostgres$Builder + [{:keys [port log-file] :as _pg-config}] + {:pre [(some? port)]} + (let [pg-builder (-> (EmbeddedPostgres/builder) + (.setPort port))] + (when log-file + (let [log-redirector (ProcessBuilder$Redirect/appendTo (io/file log-file))] + (-> pg-builder + (.setOutputRedirector log-redirector) + (.setErrorRedirector log-redirector)))) + pg-builder)) + +(defn start-postgres! + [pg-config] + (.start (->embedded-pg-builder pg-config))) + +(defn stop-postgres! + [^EmbeddedPostgres embedded-pg] + (when embedded-pg + (.close embedded-pg))) diff --git a/examples/integrant-system/components/embedded-pg/src/integrant/embedded_pg/interface.clj b/examples/integrant-system/components/embedded-pg/src/integrant/embedded_pg/interface.clj new file mode 100644 index 000000000..670b4f7d8 --- /dev/null +++ b/examples/integrant-system/components/embedded-pg/src/integrant/embedded_pg/interface.clj @@ -0,0 +1,15 @@ +(ns + ^{:doc "An embedded PostgreSQL DBMS as a Polylith component" + :author "Mark Sto"} + integrant.embedded-pg.interface + (:require [integrant.embedded-pg.core :as core]) + (:import + (io.zonky.test.db.postgres.embedded EmbeddedPostgres))) + +(defn start-postgres! + [pg-config] + (core/start-postgres! pg-config)) + +(defn stop-postgres! + [^EmbeddedPostgres embedded-pg] + (core/stop-postgres! embedded-pg)) diff --git a/examples/integrant-system/components/embedded-pg/test/integrant/embedded_pg/interface_test.clj b/examples/integrant-system/components/embedded-pg/test/integrant/embedded_pg/interface_test.clj new file mode 100644 index 000000000..f09a2b55b --- /dev/null +++ b/examples/integrant-system/components/embedded-pg/test/integrant/embedded_pg/interface_test.clj @@ -0,0 +1,6 @@ +(ns integrant.embedded-pg.interface-test + (:require [clojure.test :as test :refer :all] + [integrant.embedded-pg.interface :as postgres])) + +(deftest dummy-test + (is (= 1 1))) diff --git a/examples/integrant-system/components/pg-ops/deps.edn b/examples/integrant-system/components/pg-ops/deps.edn new file mode 100644 index 000000000..154ddbc0f --- /dev/null +++ b/examples/integrant-system/components/pg-ops/deps.edn @@ -0,0 +1,4 @@ +{:paths ["src"] + :deps {org.clojure/java.jdbc {:mvn/version "0.7.12"}} + :aliases {:test {:extra-paths ["test"] + :extra-deps {}}}} diff --git a/examples/integrant-system/components/pg-ops/src/integrant/pg_ops/core.clj b/examples/integrant-system/components/pg-ops/src/integrant/pg_ops/core.clj new file mode 100644 index 000000000..e790bc104 --- /dev/null +++ b/examples/integrant-system/components/pg-ops/src/integrant/pg_ops/core.clj @@ -0,0 +1,20 @@ +(ns + ^{:author "Mark Sto"} + integrant.pg-ops.core + (:require [clojure.java.jdbc :as jdbc])) + +(defn ->db-spec + [{:keys [port dbname user password] :as _db+creds}] + {:classname "org.postgresql.Driver" + :subprotocol "postgresql" + :subname (format "//localhost:%s/%s" port dbname) + :user (or user "postgres") + :password (or password "postgres")}) + +(defn query-version + [data-source] + (jdbc/with-db-connection + [conn {:datasource data-source}] + (some-> (jdbc/query conn ["SELECT version()"]) + (first) + :version))) diff --git a/examples/integrant-system/components/pg-ops/src/integrant/pg_ops/interface.clj b/examples/integrant-system/components/pg-ops/src/integrant/pg_ops/interface.clj new file mode 100644 index 000000000..58cc489d1 --- /dev/null +++ b/examples/integrant-system/components/pg-ops/src/integrant/pg_ops/interface.clj @@ -0,0 +1,13 @@ +(ns + ^{:doc "A set of PostgreSQL-specific operations as a Polylith component" + :author "Mark Sto"} + integrant.pg-ops.interface + (:require [integrant.pg-ops.core :as core])) + +(defn ->db-spec + [db+creds] + (core/->db-spec db+creds)) + +(defn query-version + [data-source] + (core/query-version data-source)) diff --git a/examples/integrant-system/components/pg-ops/test/integrant/pg_ops/interface_test.clj b/examples/integrant-system/components/pg-ops/test/integrant/pg_ops/interface_test.clj new file mode 100644 index 000000000..c8d1695a3 --- /dev/null +++ b/examples/integrant-system/components/pg-ops/test/integrant/pg_ops/interface_test.clj @@ -0,0 +1,6 @@ +(ns integrant.pg-ops.interface-test + (:require [clojure.test :as test :refer :all] + [integrant.pg-ops.interface :as pg-ops])) + +(deftest dummy-test + (is (= 1 1))) diff --git a/examples/integrant-system/deps.edn b/examples/integrant-system/deps.edn new file mode 100644 index 000000000..7f4cd473d --- /dev/null +++ b/examples/integrant-system/deps.edn @@ -0,0 +1,21 @@ +{:aliases {:dev {:extra-paths ["development/src" + "development/resources"] + + :extra-deps {poly+integrant/system {:local/root "bases/system"} + poly+integrant/config {:local/root "components/config"} + poly+integrant/embedded-pg {:local/root "components/embedded-pg"} + poly+integrant/pg-ops {:local/root "components/pg-ops"} + + integrant/repl {:mvn/version "0.3.3"} + + org.clojure/clojure {:mvn/version "1.11.1"} + org.clojure/tools.logging {:mvn/version "1.2.4"} + + org.apache.logging.log4j/log4j-api {:mvn/version "2.23.1"} + org.apache.logging.log4j/log4j-core {:mvn/version "2.23.1"} + org.apache.logging.log4j/log4j-slf4j-impl {:mvn/version "2.23.1"}}} + + :test {:extra-paths ["bases/system/test"]} + + :poly {:main-opts ["-m" "polylith.clj.core.poly-cli.core"] + :extra-deps {polylith/clj-poly {:mvn/version "0.2.18"}}}}} diff --git a/examples/integrant-system/development/resources/log4j2.xml b/examples/integrant-system/development/resources/log4j2.xml new file mode 100644 index 000000000..321df9836 --- /dev/null +++ b/examples/integrant-system/development/resources/log4j2.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/integrant-system/development/src/user.clj b/examples/integrant-system/development/src/user.clj new file mode 100644 index 000000000..9ba9937b4 --- /dev/null +++ b/examples/integrant-system/development/src/user.clj @@ -0,0 +1,25 @@ +(ns + ^{:author "Mark Sto"} + user + (:require [clojure.tools.logging :as log] + [integrant.pg-ops.interface :as pg-ops] + [integrant.system.core :as system] + [integrant.system.state :as state])) + +(defn- get-data-source + [] + (get-in @state/*state [:system :integrant.system/data-source])) + +(comment + ;; 1. Launch a new system + (system/launch!) + + ;; 2. Check that it works + (log/info (pg-ops/query-version (get-data-source))) + + ;; 3. Check restart works + (state/restart!) + + ;; 4. Shutdown the system + (system/shutdown!) + .) diff --git a/examples/integrant-system/projects/.keep b/examples/integrant-system/projects/.keep new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/examples/integrant-system/projects/.keep @@ -0,0 +1 @@ + diff --git a/examples/integrant-system/readme.md b/examples/integrant-system/readme.md new file mode 100644 index 000000000..0539bb9b9 --- /dev/null +++ b/examples/integrant-system/readme.md @@ -0,0 +1,27 @@ +# Polylith Systems + +## Polylith + Integrant + +This example demonstrates a basic setup of a stateful system (a Polylith `base`) +handled by the [Integrant](https://github.com/weavejester/integrant) library. + +### System Components + +The most frequently asked system was taken as an illustrative example. It uses +both "stateful" and "stateless" components to work with a traditional database, +in this case PostgreSQL. By "stateful" we mean components that are part of the +Integrant system (used at runtime) and that may also have Polylith counterparts +(used at build time). By "stateless" we mean regular Polylith components. + +The minimal set of system components: + +| Component | Polylith | Integrant | Description | +|---------------|---------------|---------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Config | `config` | `:integrant.system/config` | A "stateful" component encapsulating the system runtime configuration. Every system should start with this one. Here we have it for completeness and keep its implementation dead simple. | +| Embedded DB | `embedded-pg` | `:integrant.system/embedded-pg` | A "stateful" component which should be divided into two parts along the boundary between the component and the Integrant system that merely prepares arguments and calls its methods. | +| DataSource | n/a | `:integrant.system/data-source` | A "stateful" component which is only required at runtime (to be started and stopped properly), i.e. lacks a Polylith counterpart. | +| DB Operations | `pg-ops` | n/a | A regular "stateless" component whose methods are parametrized by the required system state (e.g. `data-source`) or its derivatives. | + +### Example Author + +Kudos to [Mark Sto](https://github.com/marksto). diff --git a/examples/integrant-system/workspace.edn b/examples/integrant-system/workspace.edn new file mode 100644 index 000000000..c3a766dd5 --- /dev/null +++ b/examples/integrant-system/workspace.edn @@ -0,0 +1,9 @@ +{:top-namespace "integrant" + :interface-ns "interface" + :default-profile-name "default" + :compact-views #{} + :vcs {:name "git" + :auto-add false} + :tag-patterns {:stable "stable-*" + :release "v[0-9]*"} + :projects {"development" {:alias "dev"}}}