From 85ceda7e8b8be0037b1fd65a36e63c0437efb5d5 Mon Sep 17 00:00:00 2001 From: John Practicalli <250870+practicalli-john@users.noreply.github.com> Date: Sun, 2 Jul 2023 00:11:35 +0100 Subject: [PATCH] template: service with donut & integrant options Add donut and integrant options based on value of `:component` passed as a command line argument when generating a new template When `:component` is not passed, an atom-based REPL reset of the HTTP server is provided --- .cljstyle | 2 +- CHANGELOG.md | 3 +- .../service/build/deps.edn.template | 2 - .../service/build/deps_donut.edn.template | 52 +++++ .../service/build/deps_integrant.edn.template | 52 +++++ .../practicalli/service/dev/mulog_events.clj | 55 +++++ .../service/dev/mulog_publisher.clj | 26 --- resources/practicalli/service/dev/portal.clj | 24 ++ .../service/dev/system_repl.clj.template | 51 +++++ .../dev/system_repl_donut.clj.template | 36 +++ ...ate => system_repl_integrant.clj.template} | 2 +- .../practicalli/service/dev/user.clj.template | 115 ++++++++++ .../practicalli/service/dev/user_donut.clj | 116 ++++++++++ .../dev/{user.clj => user_integrant.clj} | 56 ++--- .../resources/config_donut_env.edn.template | 79 +++++++ ...template => config_integrant.edn.template} | 2 +- .../service/src/parse_system.clj.template | 28 --- .../src/parse_system_integrant.clj.template | 54 +++++ .../service/src/service.clj.template | 121 +++------- .../service/src/service_donut.clj.template | 76 +++++++ .../src/service_integrant.clj.template | 145 ++++++++++++ .../service/src/system_donut.clj.template | 97 ++++++++ .../src/system_donut_aero.clj.template | 119 ++++++++++ .../service/src/system_integrant.clj.template | 91 ++++++++ resources/practicalli/service/template.edn | 13 +- src/practicalli/rules.clj | 209 ++++++++++++++++++ src/practicalli/service.clj | 16 +- 27 files changed, 1437 insertions(+), 205 deletions(-) create mode 100644 resources/practicalli/service/build/deps_donut.edn.template create mode 100644 resources/practicalli/service/build/deps_integrant.edn.template create mode 100644 resources/practicalli/service/dev/mulog_events.clj delete mode 100644 resources/practicalli/service/dev/mulog_publisher.clj create mode 100644 resources/practicalli/service/dev/portal.clj create mode 100644 resources/practicalli/service/dev/system_repl.clj.template create mode 100644 resources/practicalli/service/dev/system_repl_donut.clj.template rename resources/practicalli/service/dev/{system.clj.template => system_repl_integrant.clj.template} (99%) create mode 100644 resources/practicalli/service/dev/user.clj.template create mode 100644 resources/practicalli/service/dev/user_donut.clj rename resources/practicalli/service/dev/{user.clj => user_integrant.clj} (72%) create mode 100644 resources/practicalli/service/resources/config_donut_env.edn.template rename resources/practicalli/service/resources/{config.edn.template => config_integrant.edn.template} (98%) create mode 100644 resources/practicalli/service/src/parse_system_integrant.clj.template create mode 100644 resources/practicalli/service/src/service_donut.clj.template create mode 100644 resources/practicalli/service/src/service_integrant.clj.template create mode 100644 resources/practicalli/service/src/system_donut.clj.template create mode 100644 resources/practicalli/service/src/system_donut_aero.clj.template create mode 100644 resources/practicalli/service/src/system_integrant.clj.template create mode 100644 src/practicalli/rules.clj diff --git a/.cljstyle b/.cljstyle index 8dbd146..96ef9b1 100644 --- a/.cljstyle +++ b/.cljstyle @@ -1,7 +1,7 @@ ;; cljstyle configuration {:files {:extensions #{"cljc" "cljs" "clj" "cljx" "edn"}, - :ignore #{"checkouts" "dev" ".hg" "target" ".git" "mulog_publisher.clj"}}, + :ignore #{"checkouts" "dev" ".hg" "target" ".git" "mulog_events.clj"}}, :rules {:namespaces {:enabled? false, diff --git a/CHANGELOG.md b/CHANGELOG.md index 90225de..9080adf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,11 @@ All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/). ## [Unreleased] - ## Added - [#19](https://github.com/practicalli/project-templates/issues/19) `practicalli/minimal` template - templates: link to Practicalli Clojure Project templates section for help after new project is created +- [#22](https://github.com/practicalli/project-templates/pull/22) Service template with `:component` option for `:donut` and `:integrant` + ### Changed - ci: update MegaLinter GitHub Action v7 in MegaLnter workflow - ci: set monthly schedule and cron examples for alternative schedules diff --git a/resources/practicalli/service/build/deps.edn.template b/resources/practicalli/service/build/deps.edn.template index 79fe9c2..2a72365 100644 --- a/resources/practicalli/service/build/deps.edn.template +++ b/resources/practicalli/service/build/deps.edn.template @@ -16,8 +16,6 @@ ;; org.slf4j/slf4j-nop {:mvn/version "1.7.32"} ;; System - aero/aero {:mvn/version "1.1.6"} - integrant/integrant {:mvn/version "0.8.0"} org.clojure/clojure {:mvn/version "{{clojure-version}}"}} :aliases diff --git a/resources/practicalli/service/build/deps_donut.edn.template b/resources/practicalli/service/build/deps_donut.edn.template new file mode 100644 index 0000000..3436322 --- /dev/null +++ b/resources/practicalli/service/build/deps_donut.edn.template @@ -0,0 +1,52 @@ +{:paths + ["src" "resources"] + + :deps + {;; Service + http-kit/http-kit {:mvn/version "2.6.0"} ; latest "2.7.0-alpha1" + metosin/reitit {:mvn/version "0.5.13"} + metosin/reitit-dev {:mvn/version "0.5.18"} ; human readable exceptions + + ;; Logging + ;; create events and send to publisher + com.brunobonacci/mulog {:mvn/version "0.9.0"} + ;; JSON Console out support + com.brunobonacci/mulog-adv-console {:mvn/version "0.9.0"} + ;; Optional: suppress slf4j warning + ;; org.slf4j/slf4j-nop {:mvn/version "1.7.32"} + + ;; System + aero/aero {:mvn/version "1.1.6"} + party.donut/system {:mvn/version "0.0.202"} + org.clojure/clojure {:mvn/version "{{clojure-version}}"}} + + :aliases + {;; Clojure.main execution of application + :run/service + {:main-opts ["-m" "{{top/ns}}.{{main/ns}}.service"]} + + ;; Clojure.exec execution of specified function + :run/greet + {:exec-fn {{top/ns}}.{{main/ns}}.service/greet + :exec-args {:name "Clojure"}} + + ;; Add libraries and paths to support additional test tools + :test/env + {} + + ;; Test runner - local and CI + ;; call with :watch? true to start file watcher and re-run tests on saved changes + :test/run + {:extra-paths ["test"] + :extra-deps {lambdaisland/kaocha {:mvn/version "1.85.1342"}} + :main-opts ["-m" "kaocha.runner"] + :exec-fn kaocha.runner/exec-fn + :exec-args {:randomize? false + :fail-fast? true}} + + ;; tools.build `build.clj` built script + :build + {:replace-paths ["."] + :replace-deps {io.github.clojure/tools.build + {:git/tag "v0.9.4" :git/sha "76b78fe"}} + :ns-default build}}} diff --git a/resources/practicalli/service/build/deps_integrant.edn.template b/resources/practicalli/service/build/deps_integrant.edn.template new file mode 100644 index 0000000..79fe9c2 --- /dev/null +++ b/resources/practicalli/service/build/deps_integrant.edn.template @@ -0,0 +1,52 @@ +{:paths + ["src" "resources"] + + :deps + {;; Service + http-kit/http-kit {:mvn/version "2.6.0"} ; latest "2.7.0-alpha1" + metosin/reitit {:mvn/version "0.5.13"} + metosin/reitit-dev {:mvn/version "0.5.18"} ; human readable exceptions + + ;; Logging + ;; create events and send to publisher + com.brunobonacci/mulog {:mvn/version "0.9.0"} + ;; JSON Console out support + com.brunobonacci/mulog-adv-console {:mvn/version "0.9.0"} + ;; Optional: suppress slf4j warning + ;; org.slf4j/slf4j-nop {:mvn/version "1.7.32"} + + ;; System + aero/aero {:mvn/version "1.1.6"} + integrant/integrant {:mvn/version "0.8.0"} + org.clojure/clojure {:mvn/version "{{clojure-version}}"}} + + :aliases + {;; Clojure.main execution of application + :run/service + {:main-opts ["-m" "{{top/ns}}.{{main/ns}}.service"]} + + ;; Clojure.exec execution of specified function + :run/greet + {:exec-fn {{top/ns}}.{{main/ns}}.service/greet + :exec-args {:name "Clojure"}} + + ;; Add libraries and paths to support additional test tools + :test/env + {} + + ;; Test runner - local and CI + ;; call with :watch? true to start file watcher and re-run tests on saved changes + :test/run + {:extra-paths ["test"] + :extra-deps {lambdaisland/kaocha {:mvn/version "1.85.1342"}} + :main-opts ["-m" "kaocha.runner"] + :exec-fn kaocha.runner/exec-fn + :exec-args {:randomize? false + :fail-fast? true}} + + ;; tools.build `build.clj` built script + :build + {:replace-paths ["."] + :replace-deps {io.github.clojure/tools.build + {:git/tag "v0.9.4" :git/sha "76b78fe"}} + :ns-default build}}} diff --git a/resources/practicalli/service/dev/mulog_events.clj b/resources/practicalli/service/dev/mulog_events.clj new file mode 100644 index 0000000..13d6741 --- /dev/null +++ b/resources/practicalli/service/dev/mulog_events.clj @@ -0,0 +1,55 @@ +;; --------------------------------------------------------- +;; Mulog Global Context and Custom Publisher +;; +;; - set event log global context +;; - tap publisher for use with Portal and other tap sources +;; - publish all mulog events to Portal tap source +;; --------------------------------------------------------- + +(ns mulog-events + (:require + [com.brunobonacci.mulog :as mulog] + [com.brunobonacci.mulog.buffer :as mulog-buffer])) + +;; --------------------------------------------------------- +;; Set event global context +;; - information added to every event for REPL workflow +(mulog/set-global-context! {:app-name "{{main/ns}} Service", + :version "0.1.0", :env "dev"}) +;; --------------------------------------------------------- + +;; --------------------------------------------------------- +;; Mulog event publishing + +(deftype TapPublisher + [buffer transform] + com.brunobonacci.mulog.publisher.PPublisher + (agent-buffer [_] buffer) + (publish-delay [_] 200) + (publish [_ buffer] + (doseq [item (transform (map second (mulog-buffer/items buffer)))] + (tap> item)) + (mulog-buffer/clear buffer))) + +#_{:clj-kondo/ignore [:unused-private-var]} +(defn ^:private tap-events + [{:keys [transform] :as _config}] + (TapPublisher. (mulog-buffer/agent-buffer 10000) (or transform identity))) + +(def tap-publisher + "Start mulog custom tap publisher to send all events to Portal + and other tap sources + `mulog-tap-publisher` to stop publisher" + (mulog/start-publisher! + {:type :custom, :fqn-function "mulog-events/tap-events"})) + +#_{:clj-kondo/ignore [:unused-public-var]} +(defn stop + "Stop mulog tap publisher to ensure multiple publishers are not started + Recommended before using `(restart)` or evaluating the `user` namespace" + [] + tap-publisher) + +;; Example mulog event message +;; (mulog/log ::dev-user-ns :message "Example event message" :ns (ns-publics *ns*)) +;; --------------------------------------------------------- diff --git a/resources/practicalli/service/dev/mulog_publisher.clj b/resources/practicalli/service/dev/mulog_publisher.clj deleted file mode 100644 index 240f5ad..0000000 --- a/resources/practicalli/service/dev/mulog_publisher.clj +++ /dev/null @@ -1,26 +0,0 @@ -;; --------------------------------------------------------- -;; Mulog Custom Publishers -;; -;; - tap publisher for use with Portal and other tap sources -;; --------------------------------------------------------- - - -(ns mulog-publisher - (:require - [com.brunobonacci.mulog.buffer :as mulog-buffer])) - -(deftype TapPublisher - [buffer transform] - com.brunobonacci.mulog.publisher.PPublisher - (agent-buffer [_] buffer) - (publish-delay [_] 200) - (publish [_ buffer] - (doseq [item (transform (map second (mulog-buffer/items buffer)))] - (tap> item)) - (mulog-buffer/clear buffer))) - - -#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} -(defn tap - [{:keys [transform] :as _config}] - (TapPublisher. (mulog-buffer/agent-buffer 10000) (or transform identity))) diff --git a/resources/practicalli/service/dev/portal.clj b/resources/practicalli/service/dev/portal.clj new file mode 100644 index 0000000..63492cb --- /dev/null +++ b/resources/practicalli/service/dev/portal.clj @@ -0,0 +1,24 @@ +(ns portal + (:require + ;; Data inspector + [portal.api :as inspect])) + + +;; --------------------------------------------------------- +;; Start Portal and capture all evaluation results + +;; Open Portal window in browser with dark theme +;; https://cljdoc.org/d/djblue/portal/0.37.1/doc/ui-concepts/themes +;; Portal options: +;; - light theme {:portal.colors/theme :portal.colors/solarized-light} +;; - dark theme {:portal.colors/theme :portal.colors/gruvbox} + +(def instance + "Open portal window if no portal sessions have been created. + A portal session is created when opening a portal window" + (or (seq (inspect/sessions)) + (inspect/open {:portal.colors/theme :portal.colors/gruvbox}))) + +;; Add portal as tapsource (add to clojure.core/tapset) +(add-tap #'portal.api/submit) +;; --------------------------------------------------------- diff --git a/resources/practicalli/service/dev/system_repl.clj.template b/resources/practicalli/service/dev/system_repl.clj.template new file mode 100644 index 0000000..81b8c8b --- /dev/null +++ b/resources/practicalli/service/dev/system_repl.clj.template @@ -0,0 +1,51 @@ +;; --------------------------------------------------------- +;; System REPL - Atom Restart +;; +;; Tools for REPl workflow with Aton reference to HTTP server +;; https://practical.li/clojure-web-services/app-servers/simple-restart/ +;; --------------------------------------------------------- + +(ns system-repl + (:require + [org.httpkit.server :as http-server] + [clojure.tools.namespace.repl :refer [refresh]] + [{{top/ns}}.{{main/ns}}.service :as service])) + +;; --------------------------------------------------------- +;; HTTP Server State + +(defonce http-server-instance (atom nil)) +;; --------------------------------------------------------- + + +;; --------------------------------------------------------- +;; REPL workflow commands + +(defn stop + "Gracefully shutdown the server, waiting 100ms" + [] + (when-not (nil? @http-server-instance) + (@http-server-instance :timeout 100) + (reset! http-server-instance nil) + (println "INFO: HTTP server shutting down..."))) + +(defn start + "Start the application server and run the application" + [port] + (let [port (Integer/parseInt + (or port + (System/getenv "PORT") + "8080"))]) + (println "INFO: Starting server on port: " port) + + (reset! http-server-instance + (service/http-service-start (Integer/parseInt port)))) + +(defn restart + "Stop the http server, refresh changed namespacea and start the http server again" + [] + (stop) + (refresh) ;; Refresh changed namespaces + (start)) +;; --------------------------------------------------------- + diff --git a/resources/practicalli/service/dev/system_repl_donut.clj.template b/resources/practicalli/service/dev/system_repl_donut.clj.template new file mode 100644 index 0000000..220a767 --- /dev/null +++ b/resources/practicalli/service/dev/system_repl_donut.clj.template @@ -0,0 +1,36 @@ +;; --------------------------------------------------------- +;; Donut System REPL +;; +;; Tools for REPl workflow with Donut system components +;; --------------------------------------------------------- + +(ns system-repl + "Tools for REPl workflow with Donut system components" + (:require + [donut.system :as donut] + [donut.system.repl :as donut-repl] + [donut.system.repl.state :as donut-repl-state] + [{{top/ns}}.{{main/ns}}.system :as system])) + + +(defmethod donut/named-system :donut.system/repl + [_] system/main) + +(defn start + "Start system with donut, optionally passing a named system" + ([] (donut-repl/start)) + ([system-config] (donut-repl/start system-config))) + +(defn stop + "Stop the currently running system" + [] (donut-repl/stop)) + +(defn restart + "Restart the system with donut repl, + Uses clojure.tools.namespace.repl to reload namespaces + `(clojure.tools.namespace.repl/refresh :after 'donut.system.repl/start)`" + [] (donut-repl/restart)) + +(defn system + "Return: fully qualified hash-map of system state" + [] donut-repl-state/system) diff --git a/resources/practicalli/service/dev/system.clj.template b/resources/practicalli/service/dev/system_repl_integrant.clj.template similarity index 99% rename from resources/practicalli/service/dev/system.clj.template rename to resources/practicalli/service/dev/system_repl_integrant.clj.template index e16e753..cb0f315 100644 --- a/resources/practicalli/service/dev/system.clj.template +++ b/resources/practicalli/service/dev/system_repl_integrant.clj.template @@ -17,7 +17,7 @@ ;; --------------------------------------------------- -(ns system +(ns system-repl "Configure the system components and provide Integrant REPL convenience functions to start/stop/restart components and show system configuration" (:require diff --git a/resources/practicalli/service/dev/user.clj.template b/resources/practicalli/service/dev/user.clj.template new file mode 100644 index 0000000..5494724 --- /dev/null +++ b/resources/practicalli/service/dev/user.clj.template @@ -0,0 +1,115 @@ +;; --------------------------------------------------------- +;; REPL workflow development tools +;; +;; Include development tool libraries vai aliases from practicalli/clojure-cli-config +;; Start Rich Terminal UI REPL prompt: +;; `clojure -M:repl/reloaded` +;; +;; Or call clojure jack-in from an editor to start a repl +;; including the `:dev/reloaded` alias +;; - alias included in the Emacs `.dir-locals.el` file +;; --------------------------------------------------------- + +#_{:clj-kondo/ignore [:unused-namespace :unused-referred-var]} +(ns user + "Tools for REPL Driven Development" + (:require + ;; REPL Workflow + [system-repl :refer [start stop restart]] + [clojure.tools.namespace.repl :refer [set-refresh-dirs] + :as ns-repl] + [portal] ; launch portal + [portal.api :as inspect] + + ;; Logging + [com.brunobonacci.mulog :as mulog] ; Event Logging + [mulog-events])) ; Global context & Tap publisher + +;; --------------------------------------------------------- +;; Help + +(println "---------------------------------------------------------") +(println "Loading custom user namespace tools...") +(println "---------------------------------------------------------") + +(defn help + [] + (println "---------------------------------------------------------") + (println "HTTP service") + (println "(start) ; starts all components in system config") + (println "(restart) ; reload changed namespaces & restarts http server") + (println "(stop) ; shutdown all components in the system") + (println) + (println "Hotload libraries: ; Clojure 1.12.x") + (println "(add-lib 'library-name)") + (println "(add-libs '{domain/library-name {:mvn/version \"v1.2.3\"}})") + (println "(sync-deps) ; load dependencies from deps.edn") + (println "- deps-* lsp snippets for adding library") + (println) + (println) + (println "Portal Inspector:") + (println "- portal started by default, listening to all evaluations") + (println "(inspect/clear) ; clear all values in portal") + (println "(remove-tap #'inspect/submit) ; stop sending to portal") + (println "(inspect/close) ; close portal") + (println) + (println "(help) ; print help text") + (println "---------------------------------------------------------")) + +(help) + +;; End of Help +;; --------------------------------------------------------- + +;; --------------------------------------------------------- +;; Avoid reloading `dev` code +;; - code in `dev` directory should be evaluated if changed to reload into repl +(println + "Set REPL refresh directories to " + (set-refresh-dirs "src" "resources")) +;; --------------------------------------------------------- + +;; --------------------------------------------------------- +;; Mulog event logging +;; `mulog-publisher` namespace used to launch tap> events to tap-source (portal) +;; and set global context for all events + +;; Example mulog event message +(mulog/log ::dev-user-ns + :message "Example event from user namespace" + :ns (ns-publics *ns*)) +;; --------------------------------------------------------- + +;; --------------------------------------------------------- +;; Hotload libraries into running REPL +;; `deps-*` LSP snippets to add dependency forms +(comment + ;; Require for Clojure 1.11.x and earlier + (require '[clojure.tools.deps.alpha.repl :refer [add-libs]]) + (add-libs '{domain/library-name {:mvn/version "1.0.0"}}) + + ;; Clojure 1.12.x onward + #_(add-lib 'library-name) ; find and add library + #_(sync-deps) ; load dependencies in deps.edn (if not yet loaded) + #_()) ; End of rich comment +;; --------------------------------------------------------- + +;; --------------------------------------------------------- +;; Portal Data Inspector +(comment + ;; Open a portal inspector in browser window - light theme + ;; (inspect/open {:portal.colors/theme :portal.colors/solarized-light}) + + (inspect/clear) ; Clear all values in portal window (allows garbage collection) + + (remove-tap #'inspect/submit) ; Remove portal from `tap>` sources + + (mulog/publisher) ; stop tap publisher + + (inspect/close) ; Close the portal window + + (inspect/docs) ; View docs locally via Portal + + #_()) ; End of rich comment + +;; --------------------------------------------------------- diff --git a/resources/practicalli/service/dev/user_donut.clj b/resources/practicalli/service/dev/user_donut.clj new file mode 100644 index 0000000..7a4b070 --- /dev/null +++ b/resources/practicalli/service/dev/user_donut.clj @@ -0,0 +1,116 @@ +;; --------------------------------------------------------- +;; REPL workflow development tools +;; +;; Include development tool libraries vai aliases from practicalli/clojure-cli-config +;; Start Rich Terminal UI REPL prompt: +;; `clojure -M:repl/reloaded` +;; +;; Or call clojure jack-in from an editor to start a repl +;; including the `:dev/reloaded` alias +;; - alias included in the Emacs `.dir-locals.el` file +;; --------------------------------------------------------- + +#_{:clj-kondo/ignore [:unused-namespace :unused-referred-var]} +(ns user + "Tools for REPL Driven Development" + (:require + ;; REPL Workflow + [system-repl :refer [start stop system restart]] + [clojure.tools.namespace.repl :refer [set-refresh-dirs]] + [portal] ; launch portal + [portal.api :as inspect] + + ;; Logging + [com.brunobonacci.mulog :as mulog] ; Event Logging + [mulog-events])) ; Global context & Tap publisher + +;; --------------------------------------------------------- +;; Help + +(println "---------------------------------------------------------") +(println "Loading custom user namespace tools...") +(println "---------------------------------------------------------") + +(defn help + [] + (println "---------------------------------------------------------") + (println "System components:") + (println "(start) ; starts all components in system config") + (println "(restart) ; read system config, reloads changed namespaces & restarts system") + (println "(stop) ; shutdown all components in the system") + ;; (println "(system) ; show configuration of the running system") + ;; (println "(config) ; show system configuration") + (println) + (println "Hotload libraries: ; Clojure 1.12.x") + (println "(add-lib 'library-name)") + (println "(add-libs '{domain/library-name {:mvn/version \"v1.2.3\"}})") + (println "(sync-deps) ; load dependencies from deps.edn") + (println "- deps-* lsp snippets for adding library") + (println) + (println) + (println "Portal Inspector:") + (println "- portal started by default, listening to all evaluations") + (println "(inspect/clear) ; clear all values in portal") + (println "(remove-tap #'inspect/submit) ; stop sending to portal") + (println "(inspect/close) ; close portal") + (println) + (println "(help) ; print help text") + (println "---------------------------------------------------------")) + +(help) + +;; End of Help +;; --------------------------------------------------------- + +;; --------------------------------------------------------- +;; Avoid reloading `dev` code +;; - code in `dev` directory should be evaluated if changed to reload into repl +(println + "Set REPL refresh directories to " + (set-refresh-dirs "src" "resources")) +;; --------------------------------------------------------- + +;; --------------------------------------------------------- +;; Mulog event logging +;; `mulog-publisher` namespace used to launch tap> events to tap-source (portal) +;; and set global context for all events + +;; Example mulog event message +(mulog/log ::dev-user-ns + :message "Example event from user namespace" + :ns (ns-publics *ns*)) +;; --------------------------------------------------------- + +;; --------------------------------------------------------- +;; Hotload libraries into running REPL +;; `deps-*` LSP snippets to add dependency forms +(comment + ;; Require for Clojure 1.11.x and earlier + (require '[clojure.tools.deps.alpha.repl :refer [add-libs]]) + (add-libs '{domain/library-name {:mvn/version "1.0.0"}}) + + ;; Clojure 1.12.x onward + #_(add-lib 'library-name) ; find and add library + #_(sync-deps) ; load dependencies in deps.edn (if not yet loaded) + #_()) ; End of rich comment +;; --------------------------------------------------------- + +;; --------------------------------------------------------- +;; Portal Data Inspector +(comment + ;; Open a portal inspector in browser window - light theme + ;; (inspect/open {:portal.colors/theme :portal.colors/solarized-light}) + + (inspect/clear) ; Clear all values in portal window (allows garbage collection) + + (remove-tap #'inspect/submit) ; Remove portal from `tap>` sources + + (mulog/publisher) ; stop tap publisher + + (inspect/close) ; Close the portal window + + (inspect/docs) ; View docs locally via Portal + + #_()) ; End of rich comment + +;; --------------------------------------------------------- diff --git a/resources/practicalli/service/dev/user.clj b/resources/practicalli/service/dev/user_integrant.clj similarity index 72% rename from resources/practicalli/service/dev/user.clj rename to resources/practicalli/service/dev/user_integrant.clj index 18a671c..6a7ab82 100644 --- a/resources/practicalli/service/dev/user.clj +++ b/resources/practicalli/service/dev/user_integrant.clj @@ -10,23 +10,24 @@ ;; - alias included in the Emacs `.dir-locals.el` file ;; --------------------------------------------------------- - -(ns user +#_{:clj-kondo/ignore [:unused-namespace :unused-referred-var]} +(ns practicalli.service.dev.user-integrant "Tools for REPL Driven Development" (:require ;; Service - [system + [system-repl :refer [config restart restart-all start stop system]] ; System component commands ;; REPL Workflow - [portal.api :as inspect] ; Data inspector - [clojure.tools.namespace.repl :refer [set-refresh-dirs]] + [portal] ; launch portal instance + [portal.api :as inspect] ; inspect tools + [clojure.tools.namespace.repl + :refer [set-refresh-dirs]] ;; Logging - [com.brunobonacci.mulog :as mulog] ; Event Logging - [mulog-publisher] ; Tap mulog events - )) + [com.brunobonacci.mulog :as mulog] ; Event Logging + [mulog])) ; Global context & Tap publisher ;; --------------------------------------------------------- ;; Help @@ -76,44 +77,13 @@ ;; --------------------------------------------------------- ;; --------------------------------------------------------- -;; Start Portal and capture all evaluation results - -;; Open Portal window in browser with dark theme -;; https://cljdoc.org/d/djblue/portal/0.37.1/doc/ui-concepts/themes -;; Portal options: -;; - light theme {:portal.colors/theme :portal.colors/solarized-light} -;; - dark theme {:portal.colors/theme :portal.colors/gruvbox} - -(def portal-instance - "Open portal window if no portal sessions have been created. - A portal session is created when opening a portal window" - (or (seq (inspect/sessions)) - (inspect/open {:portal.colors/theme :portal.colors/gruvbox}))) - -;; Add portal as tapsource (add to clojure.core/tapset) -(add-tap #'portal.api/submit) -;; --------------------------------------------------------- - -;; --------------------------------------------------------- -;; Mulog events and publishing +;; Mulog event logging +;; `mulog-publisher` namespace used to launch tap> events to tap-source (portal) ;; set event global context - information added to every event for REPL workflow -(mulog/set-global-context! {:app-name "Practicalli Service", +(mulog/set-global-context! {:app-name "{{main/ns}} Service", :version "0.1.0", :env "dev"}) -(def mulog-tap-publisher - "Start mulog custom tap publisher to send all events to Portal - and other tap sources - `mulog-tap-publisher` to stop publisher" - (mulog/start-publisher! - {:type :custom, :fqn-function "mulog-publisher/tap"})) - -(defn mulog-tap-stop - "Stop mulog tap publisher to ensure multiple publishers are not started - Recommended before using `(restart)` or evaluating the `user` namespace" - [] - mulog-tap-publisher) - ;; Example mulog event message (mulog/log ::dev-user-ns ::ns (ns-publics *ns*)) ;; --------------------------------------------------------- @@ -142,7 +112,7 @@ (remove-tap #'inspect/submit) ; Remove portal from `tap>` sources - (mulog-tap-stop) ; stop tap publisher + (mulog/publisher) ; stop tap publisher (inspect/close) ; Close the portal window diff --git a/resources/practicalli/service/resources/config_donut_env.edn.template b/resources/practicalli/service/resources/config_donut_env.edn.template new file mode 100644 index 0000000..1593f66 --- /dev/null +++ b/resources/practicalli/service/resources/config_donut_env.edn.template @@ -0,0 +1,79 @@ +;; -------------------------------------------------- +;; Donut System environment configuration +;; +;; - Event logging with mulog +;; - HTTP Server +;; - Request routing (reitit) +;; - Persistence (relational) connection +;; +;; Components managed in {{top/ns}}.{{main/ns}}.service namespace +;; +;; #profile used by aero to select the configuration to use for a given profile (dev, test, prod) +;; #long defines Long Integer type (required for Java HTTP server port) +;; #env reads the environment variable of the given name +;; #or uses first non nil value in sequence +;; +;; Environment variables should be defined locally and in deployment provisioner tooling +;; -------------------------------------------------- + +{:env + {:http-server + #profile + {:dev {:port #or [#env "HTTP_PORT" "8080"]} + :prod {:port #env "PORT"}} + :persistence + #profile + {:dev + {:database-host #or [#env "POSTGRES_HOST" "http://localhost"] + :database-port #or [#env "POSTGRES_PORT" "5432"] + :database-username #or [#env "POSTGRES_USERNAME" "clojure"] + :database-password #or [#env "POSTGRES_PASSWORD" "clojure"] + :database-schema #or [#env "POSTGRES_SCHEMA" "clojure"]} + :prod + {:database-host #env "POSTGRES_HOST" + :database-port #env "POSTGRES_PORT" + :database-username #env "POSTGRES_USERNAME" + :database-password #env "POSTGRES_PASSWORD" + :database-schema #env "POSTGRES_SCHEMA"}} + + ;; Type of publisher to use for mulog events + ;; Publish json format logs, captured by fluentd and exposed via OpenDirectory + :mulog + #profile + {:dev + {:type :console-json :pretty? true} + + ;; Multiple publishers using Open Zipkin service (started via docker-compose) + :docker + {:type :multi + :publishers + [{:type :console-json :pretty? false} + {:type :zipkin :url "http://localhost:9411/"}]} + + :prod + {:type :console-json :pretty? false}} + + ;; Configure data API connections + :data-api + #profile + {:dev + {:game-service-base-url #or [#env GAME_SERVICE_BASE_URL "http://localhost"] + :llamasoft-api-uri #or [#env LAMASOFT_API_URI "http://localhost"] + + :polybus-report-uri "/report/polybus" + :moose-life-report-uri "/api/v1/report/moose-life" + :minotaur-arcade-report-uri "/api/v2/minotar-arcade" + :gridrunner-revolution-report-uri "/api/v1.1/gridrunner" + :space-giraffe-report-uri "/api/v1/games/space-giraffe"} + + :prod + {:game-service-base-url #or [#env GAME_SERVICE_BASE_URL "http://localhost"] + :llamasoft-api-uri #or [#env LAMASOFT_API_URI "http://localhost"] + + :polybus-report-uri "/report/polybus" + :moose-life-report-uri "/api/v1/report/moose-life" + :minotaur-arcade-report-uri "/api/v2/minotar-arcade" + :gridrunner-revolution-report-uri "/api/v1.1/gridrunner" + :space-giraffe-report-uri "/api/v1/games/space-giraffe"}}}} + + diff --git a/resources/practicalli/service/resources/config.edn.template b/resources/practicalli/service/resources/config_integrant.edn.template similarity index 98% rename from resources/practicalli/service/resources/config.edn.template rename to resources/practicalli/service/resources/config_integrant.edn.template index dc6241a..5e7c36c 100644 --- a/resources/practicalli/service/resources/config.edn.template +++ b/resources/practicalli/service/resources/config_integrant.edn.template @@ -64,7 +64,7 @@ ;; - connection to services that provide eSports data :{{top/ns}}.{{main/ns}}.service/data-provider - {;; external data providers via Risky + {;; external data providers :game-service-base-url #or [#env GAME_SERVICE_BASE_URL "http://localhost"] :llamasoft-api-uri #or [#env LAMASOFT_API_URI "http://localhost"] :polybus-report-uri "/report/polybus" diff --git a/resources/practicalli/service/src/parse_system.clj.template b/resources/practicalli/service/src/parse_system.clj.template index 92c0e7c..2ab8209 100644 --- a/resources/practicalli/service/src/parse_system.clj.template +++ b/resources/practicalli/service/src/parse_system.clj.template @@ -1,20 +1,11 @@ ;; -------------------------------------------------- ;; Prepare system configuration with juxt/aero -;; -;; aero parsing of Integrant configuration from `user` and `service` namespaces -;; -;; Integrant and Integrant-repl are separate workflows for managing system components -;; however they share the same parsing of the system configuration code with Aero -;; - Integrant manages components when the service is run via `main` function -;; - Integrant REPL manages components during development from `user` namespace ;; -------------------------------------------------- - (ns {{top/ns}}.{{main/ns}}.parse-system (:require [aero.core :as aero] [clojure.java.io :as io] - [integrant.core :as ig] ;; Debug aero parsing - comment by default ;; [com.brunobonacci.mulog :as mulog] @@ -24,14 +15,6 @@ ;; Parse system configuration ;; - update system configuration with respect to a given profile -;; Parse Integrant key with with aero tag literals -;; returning key/value from given profile value -#_{:clj-kondo/ignore [:unused-binding]} -(defmethod aero/reader 'ig/ref - [_ tag value] - ;; (mulog/log ::aero-parse-key :key _ :tag tag :value value :local-time (java.time.LocalDateTime/now) - (ig/ref value)) - (defn aero-config "Profile specific configuration for all services. Profiles supported: :dev :test :prod" @@ -39,16 +22,5 @@ ;; (mulog/log ::aero-parse-config :profile profile :local-time (java.time.LocalDateTime/now) (aero/read-config (io/resource "config.edn") {:profile profile})) -(defn aero-prep - "Parse the system config and update values for the given profile (:dev, :test :prod) - Top-level keys in `config.edn` use fully qualified namespace name for `ig/init-key` defmethod - `ig/load-namespaces` automatically loads each namespace referenced by a top-level key - Return: configuration hash-map for specified profile (:dev :test :prod) with aero tags resolved" - [profile] - (let [config (aero-config profile)] - ;; (mulog/log ::integrant-load-namespaces :config config :local-time (java.time.LocalDateTime/now) - (ig/load-namespaces config) - config)) - ;; End of Aero Parsing ;; --------------------------------------------------------- diff --git a/resources/practicalli/service/src/parse_system_integrant.clj.template b/resources/practicalli/service/src/parse_system_integrant.clj.template new file mode 100644 index 0000000..92c0e7c --- /dev/null +++ b/resources/practicalli/service/src/parse_system_integrant.clj.template @@ -0,0 +1,54 @@ +;; -------------------------------------------------- +;; Prepare system configuration with juxt/aero +;; +;; aero parsing of Integrant configuration from `user` and `service` namespaces +;; +;; Integrant and Integrant-repl are separate workflows for managing system components +;; however they share the same parsing of the system configuration code with Aero +;; - Integrant manages components when the service is run via `main` function +;; - Integrant REPL manages components during development from `user` namespace +;; -------------------------------------------------- + + +(ns {{top/ns}}.{{main/ns}}.parse-system + (:require + [aero.core :as aero] + [clojure.java.io :as io] + [integrant.core :as ig] + + ;; Debug aero parsing - comment by default + ;; [com.brunobonacci.mulog :as mulog] + )) + +;; -------------------------------------------------- +;; Parse system configuration +;; - update system configuration with respect to a given profile + +;; Parse Integrant key with with aero tag literals +;; returning key/value from given profile value +#_{:clj-kondo/ignore [:unused-binding]} +(defmethod aero/reader 'ig/ref + [_ tag value] + ;; (mulog/log ::aero-parse-key :key _ :tag tag :value value :local-time (java.time.LocalDateTime/now) + (ig/ref value)) + +(defn aero-config + "Profile specific configuration for all services. + Profiles supported: :dev :test :prod" + [profile] + ;; (mulog/log ::aero-parse-config :profile profile :local-time (java.time.LocalDateTime/now) + (aero/read-config (io/resource "config.edn") {:profile profile})) + +(defn aero-prep + "Parse the system config and update values for the given profile (:dev, :test :prod) + Top-level keys in `config.edn` use fully qualified namespace name for `ig/init-key` defmethod + `ig/load-namespaces` automatically loads each namespace referenced by a top-level key + Return: configuration hash-map for specified profile (:dev :test :prod) with aero tags resolved" + [profile] + (let [config (aero-config profile)] + ;; (mulog/log ::integrant-load-namespaces :config config :local-time (java.time.LocalDateTime/now) + (ig/load-namespaces config) + config)) + +;; End of Aero Parsing +;; --------------------------------------------------------- diff --git a/resources/practicalli/service/src/service.clj.template b/resources/practicalli/service/src/service.clj.template index 48f1408..783492c 100644 --- a/resources/practicalli/service/src/service.clj.template +++ b/resources/practicalli/service/src/service.clj.template @@ -3,126 +3,61 @@ ;; ;; {{description}} ;; -;; Start the service using Integrant configuration and an environment profile. -;; A profile is injected into the configuration in the `practicalli.gameboard.environment` namespace -;; and the resulting configuration is used by Integrant to start the system components +;; Start the service using `(start)` in the rich comment form ;; ;; The service consist of ;; - httpkit web application server ;; - metosin/reitit for routing and ring for request / response management ;; - mulog event logging service -;; -;; Related namespaces -;; `resources/config.edn` system configuration with environment #profile placeholders -;; `{{top/ns}}.environment` injects profile & other aero tag values into a resulting configuration ;; --------------------------------------------------------- - (ns {{top/ns}}.{{main/ns}}.service - "Gameboard service component lifecycle management" + "Gameboard service" (:gen-class) (:require - - ;; Application dependencies + ;; Requests [{{top/ns}}.{{main/ns}}.router :as router] - ;; Component system - [{{top/ns}}.{{main/ns}}.parse-system :as parse-system] - ;; System dependencies [org.httpkit.server :as http-server] - [integrant.core :as ig] [com.brunobonacci.mulog :as mulog])) +;; --------------------------------------------------------- +;; HTTP Server -;; -------------------------------------------------- -;; Configure and start application components - -;; Start mulog publisher for the given publisher type, i.e. console, cloud-watch -#_{:clj-kondo/ignore [:unused-binding]} -(defmethod ig/init-key ::log-publish - [_ {:keys [mulog] :as config}] - (mulog/log ::log-publish-component :publisher-config mulog :local-time (java.time.LocalDateTime/now)) - (let [publisher (mulog/start-publisher! mulog)] - publisher)) - -;; Connection for Relational Database Persistence -;; return hash-map of connection values: endpoint, access-key, secret-key -;; TODO: add example of connection pool -(defmethod ig/init-key ::relational-store - [_ {:keys [connection] :as config}] - (mulog/log ::persistence-component :connection connection :local-time (java.time.LocalDateTime/now)) - config) - -;; Connections for data services -(defmethod ig/init-key ::data-provider - [_ config] - (mulog/log ::data-provider-component :configuration config :local-time (java.time.LocalDateTime/now)) - config) - -;; Configure environment for router application, e.g. database connection details, etc. -(defmethod ig/init-key ::router - [_ config] - (mulog/log ::app-routing-component :app-config config) - (router/app config)) - -;; HTTP server start - returns function to stop the server -(defmethod ig/init-key ::http-server - [_ {:keys [handler port join?]}] - (mulog/log ::http-server-component :handler handler :port port :local-time (java.time.LocalDateTime/now)) - (http-server/run-server handler {:port port :join? join?})) - -;; Shutdown HTTP service -(defmethod ig/halt-key! ::http-server - [_ http-server-instance] - (mulog/log ::http-server-component-shutdown :http-server-object http-server-instance :local-time (java.time.LocalDateTime/now)) - ;; Calling http instance shuts down that instance - (http-server-instance)) - -;; Shutdown Log publishing -(defmethod ig/halt-key! ::log-publish - [_ publisher] - (mulog/log ::log-publish-component-shutdown :publisher-object publisher :local-time (java.time.LocalDateTime/now)) - ;; Pause so final messages have chance to be published - (Thread/sleep 250) - ;; Call publisher again to stop publishing - (publisher)) - -(defn stop - "Stop service using Integrant halt!" - [system] - (mulog/log ::http-server-sigterm :system system :local-time (java.time.LocalDateTime/now)) - ;; (println "Shutdown of Billie Fraud API service via Integrant") - (ig/halt! system)) +(defn http-server-start + "Start the application server and run the application" + [port] + (http-server/run-server #'router/app {:port port})) -;; -------------------------------------------------- +;; End of HTTP Server +;; --------------------------------------------------------- ;; -------------------------------------------------- ;; Application entry point (defn -main - "{{top/ns}} {{main/ns}} service is started with `ig/init` and the Integrant configuration, - with the return value bound to the namespace level `system` name. - Aero is used to configure Integrant configuration based on profile (dev, test, prod), - allowing environment specific configuration, e.g. mulog publisher - The shutdown hook calling a zero arity function, gracefully stopping the service - on receipt of a SIGTERM from the infrastructure, giving the application 30 seconds before forced termination." + "{{top/ns}} {{main/ns}} service providing the foundation of an API. + The shutdown hook gracefully stops the service on receipt of a SIGTERM from the infrastructure, + giving the application 30 seconds before forced termination." [] - (let [profile (or (keyword (System/getenv "SERVICE_PROFILE")) - :dev) + (mulog/set-global-context! + {:app-name "{{top/ns}} {{main/ns}} service" :version "0.1.0"}) +( port) - ;; Add keys to every event / publish profile use to start the service - _ (mulog/set-global-context! - {:app-name "{{top/ns}} {{main/ns}} service" :version "0.1.0" :env profile}) + (let [port (Integer/parseInt (or port + (System/getenv "PORT") + 8080))] + + (running-system (http-server-start port)) - system (ig/init (parse-system/aero-prep profile)) - - _ (mulog/log ::gameboard-system :system-config system)] + (.addShutdownHook + (Runtime/getRuntime) + (Thread. ^Runnable #(running-system :timeout 100))))) +;; -------------------------------------------------- - ;; TODO: capture the reason for the shutdown - i.e. can we capture the sigterm - (.addShutdownHook (Runtime/getRuntime) (Thread. ^Runnable #(stop system))))) ;; -------------------------------------------------- ;; Example clojure.exec function @@ -140,6 +75,6 @@ ;; -------------------------------------------------- ;; REPL workflow commands - (greet {:team-name "{{developer}}"}) + (greet {:team-name "{{developer}}"})) - ) ; End of rich comment + ; End of rich comment diff --git a/resources/practicalli/service/src/service_donut.clj.template b/resources/practicalli/service/src/service_donut.clj.template new file mode 100644 index 0000000..06fef4a --- /dev/null +++ b/resources/practicalli/service/src/service_donut.clj.template @@ -0,0 +1,76 @@ +;; --------------------------------------------------------- +;; {{top/ns}}.{{main/ns}} +;; +;; {{description}} +;; +;; Start the service using donut system configuration +;; defined in `system.clj` +;; +;; The service consist of +;; - httpkit web application server +;; - metosin/reitit for routing and ring for request / response management +;; - mulog event logging service +;; +;; Related namespaces +;; `{{top/ns}}.{{main/ns}}/system` donut system configuration +;; --------------------------------------------------------- + + +(ns {{top/ns}}.{{main/ns}}.service + "Gameboard service component lifecycle management" + (:gen-class) + (:require + + ;; Component system + [donut.system :as donut] + [{{top/ns}}.{{main/ns}}.system :as system] + + ;; System dependencies + [org.httpkit.server :as http-server] + [com.brunobonacci.mulog :as mulog])) + + +;; -------------------------------------------------- +;; Application entry point + +(defn -main + "{{top/ns}} {{main/ns}} service managed by donut system, + Aero is used to configure the donut system configuration based on profile (dev, test, prod), + allowing environment specific configuration, e.g. mulog publisher + The shutdown hook gracefully stops the service on receipt of a SIGTERM from the infrastructure, + giving the application 30 seconds before forced termination." + [] + + (mulog/set-global-context! + {:app-name "{{top/ns}} {{main/ns}} service" :version "0.1.0"}) + + (let [profile (or (keyword (System/getenv "SERVICE_PROFILE")) + :dev) + + running-system (donut/start (or (profile :profile) :prod))] + + (.addShutdownHook + (Runtime/getRuntime) + (Thread. ^Runnable #(donut/signal running-system ::donut/stop))))) +;; -------------------------------------------------- + + +;; -------------------------------------------------- +;; Example clojure.exec function + +#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} +(defn greet + "Greeting message via Clojure CLI clojure.exec" + ;; TODO: call greet with hash-map argument + ([] (greet "secret engineering")) + ([{:keys [team-name]}] + (str "{{top/ns}} {{main/ns}} service developed by the " team-name " team"))) + + +(comment + ;; -------------------------------------------------- + ;; REPL workflow commands + + (greet {:team-name "{{developer}}"})) + + ; End of rich comment diff --git a/resources/practicalli/service/src/service_integrant.clj.template b/resources/practicalli/service/src/service_integrant.clj.template new file mode 100644 index 0000000..48f1408 --- /dev/null +++ b/resources/practicalli/service/src/service_integrant.clj.template @@ -0,0 +1,145 @@ +;; --------------------------------------------------------- +;; {{top/ns}}.{{main/ns}} +;; +;; {{description}} +;; +;; Start the service using Integrant configuration and an environment profile. +;; A profile is injected into the configuration in the `practicalli.gameboard.environment` namespace +;; and the resulting configuration is used by Integrant to start the system components +;; +;; The service consist of +;; - httpkit web application server +;; - metosin/reitit for routing and ring for request / response management +;; - mulog event logging service +;; +;; Related namespaces +;; `resources/config.edn` system configuration with environment #profile placeholders +;; `{{top/ns}}.environment` injects profile & other aero tag values into a resulting configuration +;; --------------------------------------------------------- + + +(ns {{top/ns}}.{{main/ns}}.service + "Gameboard service component lifecycle management" + (:gen-class) + (:require + + ;; Application dependencies + [{{top/ns}}.{{main/ns}}.router :as router] + + ;; Component system + [{{top/ns}}.{{main/ns}}.parse-system :as parse-system] + + ;; System dependencies + [org.httpkit.server :as http-server] + [integrant.core :as ig] + [com.brunobonacci.mulog :as mulog])) + + +;; -------------------------------------------------- +;; Configure and start application components + +;; Start mulog publisher for the given publisher type, i.e. console, cloud-watch +#_{:clj-kondo/ignore [:unused-binding]} +(defmethod ig/init-key ::log-publish + [_ {:keys [mulog] :as config}] + (mulog/log ::log-publish-component :publisher-config mulog :local-time (java.time.LocalDateTime/now)) + (let [publisher (mulog/start-publisher! mulog)] + publisher)) + +;; Connection for Relational Database Persistence +;; return hash-map of connection values: endpoint, access-key, secret-key +;; TODO: add example of connection pool +(defmethod ig/init-key ::relational-store + [_ {:keys [connection] :as config}] + (mulog/log ::persistence-component :connection connection :local-time (java.time.LocalDateTime/now)) + config) + +;; Connections for data services +(defmethod ig/init-key ::data-provider + [_ config] + (mulog/log ::data-provider-component :configuration config :local-time (java.time.LocalDateTime/now)) + config) + +;; Configure environment for router application, e.g. database connection details, etc. +(defmethod ig/init-key ::router + [_ config] + (mulog/log ::app-routing-component :app-config config) + (router/app config)) + +;; HTTP server start - returns function to stop the server +(defmethod ig/init-key ::http-server + [_ {:keys [handler port join?]}] + (mulog/log ::http-server-component :handler handler :port port :local-time (java.time.LocalDateTime/now)) + (http-server/run-server handler {:port port :join? join?})) + +;; Shutdown HTTP service +(defmethod ig/halt-key! ::http-server + [_ http-server-instance] + (mulog/log ::http-server-component-shutdown :http-server-object http-server-instance :local-time (java.time.LocalDateTime/now)) + ;; Calling http instance shuts down that instance + (http-server-instance)) + +;; Shutdown Log publishing +(defmethod ig/halt-key! ::log-publish + [_ publisher] + (mulog/log ::log-publish-component-shutdown :publisher-object publisher :local-time (java.time.LocalDateTime/now)) + ;; Pause so final messages have chance to be published + (Thread/sleep 250) + ;; Call publisher again to stop publishing + (publisher)) + +(defn stop + "Stop service using Integrant halt!" + [system] + (mulog/log ::http-server-sigterm :system system :local-time (java.time.LocalDateTime/now)) + ;; (println "Shutdown of Billie Fraud API service via Integrant") + (ig/halt! system)) + +;; -------------------------------------------------- + + +;; -------------------------------------------------- +;; Application entry point + +(defn -main + "{{top/ns}} {{main/ns}} service is started with `ig/init` and the Integrant configuration, + with the return value bound to the namespace level `system` name. + Aero is used to configure Integrant configuration based on profile (dev, test, prod), + allowing environment specific configuration, e.g. mulog publisher + The shutdown hook calling a zero arity function, gracefully stopping the service + on receipt of a SIGTERM from the infrastructure, giving the application 30 seconds before forced termination." + [] + + (let [profile (or (keyword (System/getenv "SERVICE_PROFILE")) + :dev) + + ;; Add keys to every event / publish profile use to start the service + _ (mulog/set-global-context! + {:app-name "{{top/ns}} {{main/ns}} service" :version "0.1.0" :env profile}) + + system (ig/init (parse-system/aero-prep profile)) + + _ (mulog/log ::gameboard-system :system-config system)] + + ;; TODO: capture the reason for the shutdown - i.e. can we capture the sigterm + (.addShutdownHook (Runtime/getRuntime) (Thread. ^Runnable #(stop system))))) + +;; -------------------------------------------------- +;; Example clojure.exec function + +#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} +(defn greet + "Greeting message via Clojure CLI clojure.exec" + ;; TODO: call greet with hash-map argument + ([] (greet "secret engineering")) + ([{:keys [team-name]}] + (str "{{top/ns}} {{main/ns}} service developed by the " team-name " team"))) + + +(comment + ;; -------------------------------------------------- + ;; REPL workflow commands + + (greet {:team-name "{{developer}}"}) + + ) ; End of rich comment diff --git a/resources/practicalli/service/src/system_donut.clj.template b/resources/practicalli/service/src/system_donut.clj.template new file mode 100644 index 0000000..1bef97c --- /dev/null +++ b/resources/practicalli/service/src/system_donut.clj.template @@ -0,0 +1,97 @@ +;; --------------------------------------------------------- +;; {{top/ns}}.{{main/ns}} +;; +;; {{description}} +;; +;; Start the service using donut configuration and an environment profile. +;; --------------------------------------------------------- + +(ns {{top/ns}}.{{main/ns}}.system + "Service component lifecycle management" + (:gen-class) + (:require + ;; Application dependencies + [practicalli.hole-in-one.router :as router] + + ;; Component system + [donut.system :as donut] + ;; [{{top/ns}}.{{main/ns}}.parse-system :as parse-system] + + ;; System dependencies + [org.httpkit.server :as http-server] + [com.brunobonacci.mulog :as mulog])) + +;; --------------------------------------------------------- +;; Donut Party System configuration + +(def main + "System Component management with Donut" + {::donut/defs + ;; Option: move :env data to resources/config.edn and parse with aero reader + {:env + {:http-port 8080 + :persistence + {:database-host (or (System/getenv "POSTGRES_HOST") "http://localhost") + :database-port (or (System/getenv "POSTGRES_PORT") "5432") + :database-username (or (System/getenv "POSTGRES_USERNAME") "clojure") + :database-password (or (System/getenv "POSTGRES_PASSWORD") "clojure") + :database-schema (or (System/getenv "POSTGRES_SCHEMA") "clojure")}} + + ;; Configure data API connections + ;; TODO: example system defined with aero + ;; :data-api + ;; {:game-service-base-url #or [#env GAME_SERVICE_BASE_URL "http://localhost"] + ;; :llamasoft-api-uri #or [#env LAMASOFT_API_URI "http://localhost"] + ;; :polybus-report-uri "/report/polybus" + ;; :moose-life-report-uri "/api/v1/report/moose-life" + ;; :minotaur-arcade-report-uri "/api/v2/minotar-arcade" + ;; :gridrunner-revolution-report-uri "/api/v1.1/gridrunner" + ;; :space-giraffe-report-uri "/api/v1/games/space-giraffe"} + + ;; mulog publisher for a given publisher type, i.e. console, cloud-watch + :event-log + {:publisher + #::donut{:start (fn mulog-publisher-start + [{{:keys [publisher]} ::donut/config}] + (mulog/log ::log-publish-component + :publisher-config publisher + :local-time (java.time.LocalDateTime/now)) + (mulog/start-publisher! publisher)) + + :stop (fn mulog-publisher-stop + [{::donut/keys [instance]}] + (mulog/log ::log-publish-component-shutdown :publisher instance :local-time (java.time.LocalDateTime/now)) + ;; Pause so final messages have chance to be published + (Thread/sleep 250) + (instance)) + + :config {:publisher {:type :console :pretty? true}}}} + + ;; HTTP server start - returns function to stop the server + :http + {:server + #::donut{:start (fn http-kit-run-server + [{{:keys [handler options]} ::donut/config}] + (mulog/log ::http-server-component + :handler handler + :port (options :port) + :local-time (java.time.LocalDateTime/now)) + (http-server/run-server handler options)) + + :stop (fn http-kit-stop-server + [{::donut/keys [instance]}] + (mulog/log ::http-server-component-shutdown + :http-server-instance instance + :local-time (java.time.LocalDateTime/now)) + (instance)) + + :config {:handler (donut/local-ref [:handler]) + :options {:port (donut/ref [:env :http-port]) + :join? false}}} + + ;; Function handling all requests, passing system environment + ;; Configure environment for router application, e.g. database connection details, etc. + :handler (router/app (donut/ref [:env :persistence]))}}}) + +;; End of Donut Party System configuration +;; --------------------------------------------------------- diff --git a/resources/practicalli/service/src/system_donut_aero.clj.template b/resources/practicalli/service/src/system_donut_aero.clj.template new file mode 100644 index 0000000..39d9add --- /dev/null +++ b/resources/practicalli/service/src/system_donut_aero.clj.template @@ -0,0 +1,119 @@ +;; --------------------------------------------------------- +;; {{top/ns}}.{{main/ns}} +;; +;; {{description}} +;; +;; Start the service using donut configuration and an environment profile. +;; --------------------------------------------------------- + +(ns {{top/ns}}.{{main/ns}}.system + "Service component lifecycle management" + (:gen-class) + (:require + ;; Application dependencies + [practicalli.hole-in-one.router :as router] + + ;; Component system + [donut.system :as donut] + [aero.core :as aero] + + ;; System dependencies + [org.httpkit.server :as http-server] + [com.brunobonacci.mulog :as mulog])) + + +;; --------------------------------------------------------- +;; Parse system config with aero + +;; Use aero for all configuration +(defn parse-config [& [profile]] + (aero/read-config (io/resource "resources/config.edn") + (when profile {:profile profile}))) + + + ;; End of Parse system config with aero +;; --------------------------------------------------------- + + + +;; --------------------------------------------------------- +;; Donut Party System configuration + +(def base-system + "System Component management with Donut" + {::donut/defs + ;; Option: move :env data to resources/config.edn and parse with aero reader + {:env {} + + ;; mulog publisher for a given publisher type, i.e. console, cloud-watch + :event-log + {:publisher + #::donut{:start (fn mulog-publisher-start + [{{:keys [publisher]} ::donut/config}] + (mulog/log ::log-publish-component + :publisher-config publisher + :local-time (java.time.LocalDateTime/now)) + (mulog/start-publisher! publisher)) + + :stop (fn mulog-publisher-stop + [{::donut/keys [instance]}] + (mulog/log ::log-publish-component-shutdown :publisher instance :local-time (java.time.LocalDateTime/now)) + ;; Pause so final messages have chance to be published + (Thread/sleep 250) + (instance)) + + :config {:publisher {:type :console :pretty? true}}}} + + ;; HTTP server start - returns function to stop the server + :http + {:server + #::donut{:start (fn http-kit-run-server + [{{:keys [handler options]} ::donut/config}] + (mulog/log ::http-server-component + :handler handler + :port (options :port) + :local-time (java.time.LocalDateTime/now)) + (http-server/run-server handler options)) + + :stop (fn http-kit-stop-server + [{::donut/keys [instance]}] + (mulog/log ::http-server-component-shutdown + :http-server-instance instance + :local-time (java.time.LocalDateTime/now)) + (instance)) + + :config {:handler (donut/local-ref [:handler]) + :options {:port (donut/ref [:env :http-port]) + :join? false}}} + + ;; Function handling all requests, passing system environment + ;; Configure environment for router application, e.g. database connection details, etc. + :handler (router/app (donut/ref [:env :persistence]))}}}) + +;; End of Donut Party System configuration +;; --------------------------------------------------------- + +;; --------------------------------------------------------- +;; Donut named systems + +(defmethod ds/named-system :base + [_] + base-system) + +(defmethod ds/named-system :dev + [_] + (ds/system :base {[:env] (env-config :dev)})) + +(defmethod ds/named-system :donut.system/repl + [_] + (ds/system :dev)) + +(defmethod ds/named-system :prod + [_] + (ds/system :base {[:env] (env-config :prod)})) + +;; End of Donut named systems +;; --------------------------------------------------------- + + + diff --git a/resources/practicalli/service/src/system_integrant.clj.template b/resources/practicalli/service/src/system_integrant.clj.template new file mode 100644 index 0000000..1c56ba5 --- /dev/null +++ b/resources/practicalli/service/src/system_integrant.clj.template @@ -0,0 +1,91 @@ +;; --------------------------------------------------------- +;; {{top/ns}}.{{main/ns}} +;; +;; {{description}} +;; +;; Start the service using Integrant configuration and an environment profile. +;; A profile is injected into the configuration in the `practicalli.gameboard.environment` namespace +;; and the resulting configuration is used by Integrant to start the system components +;; +;; The service consist of +;; - httpkit web application server +;; - metosin/reitit for routing and ring for request / response management +;; - mulog event logging service +;; +;; Related namespaces +;; `resources/config.edn` system configuration with environment #profile placeholders +;; `{{top/ns}}.environment` injects profile & other aero tag values into a resulting configuration +;; --------------------------------------------------------- + +(ns {{top/ns}}.{{main/ns}}.system + "Service component lifecycle management" + (:gen-class) + (:require + ;; Component system + [{{top/ns}}.{{main/ns}}.parse-system :as parse-system] + + ;; System dependencies + [integrant.core :as ig] + [com.brunobonacci.mulog :as mulog])) + +;; -------------------------------------------------- +;; Configure and start application components + +;; Start mulog publisher for the given publisher type, i.e. console, cloud-watch +#_{:clj-kondo/ignore [:unused-binding]} +(defmethod ig/init-key ::log-publish + [_ {:keys [mulog] :as config}] + (mulog/log ::log-publish-component :publisher-config mulog :local-time (java.time.LocalDateTime/now)) + (let [publisher (mulog/start-publisher! mulog)] + publisher)) + +;; Connection for Relational Database Persistence +;; return hash-map of connection values: endpoint, access-key, secret-key +;; TODO: add example of connection pool +(defmethod ig/init-key ::relational-store + [_ {:keys [connection] :as config}] + (mulog/log ::persistence-component :connection connection :local-time (java.time.LocalDateTime/now)) + config) + +;; Connections for data services +(defmethod ig/init-key ::data-provider + [_ config] + (mulog/log ::data-provider-component :configuration config :local-time (java.time.LocalDateTime/now)) + config) + +;; Configure environment for router application, e.g. database connection details, etc. +(defmethod ig/init-key ::router + [_ config] + (mulog/log ::app-routing-component :app-config config) + (router/app config)) + +;; HTTP server start - returns function to stop the server +(defmethod ig/init-key ::http-server + [_ {:keys [handler port join?]}] + (mulog/log ::http-server-component :handler handler :port port :local-time (java.time.LocalDateTime/now)) + (http-server/run-server handler {:port port :join? join?})) + +;; Shutdown HTTP service +(defmethod ig/halt-key! ::http-server + [_ http-server-instance] + (mulog/log ::http-server-component-shutdown :http-server-object http-server-instance :local-time (java.time.LocalDateTime/now)) + ;; Calling http instance shuts down that instance + (http-server-instance)) + +;; Shutdown Log publishing +(defmethod ig/halt-key! ::log-publish + [_ publisher] + (mulog/log ::log-publish-component-shutdown :publisher-object publisher :local-time (java.time.LocalDateTime/now)) + ;; Pause so final messages have chance to be published + (Thread/sleep 250) + ;; Call publisher again to stop publishing + (publisher)) + +(defn stop + "Stop service using Integrant halt!" + [system] + (mulog/log ::http-server-sigterm :system system :local-time (java.time.LocalDateTime/now)) + ;; (println "Shutdown of Billie Fraud API service via Integrant") + (ig/halt! system)) + +;; -------------------------------------------------- diff --git a/resources/practicalli/service/template.edn b/resources/practicalli/service/template.edn index 6f34b6a..29f333a 100644 --- a/resources/practicalli/service/template.edn +++ b/resources/practicalli/service/template.edn @@ -40,21 +40,22 @@ "scoreboard.clj.template" "scoreboard.clj"}] ["build" "" {"build.clj.template" "build.clj" - "deps.edn.template" "deps.edn"}] + "deps.edn.template" "deps.edn"} + :only] ["dev" "dev" - {"system.clj.template" "system.clj"}] + {"system_repl.clj.template" "system_repl.clj" + "user.clj" "user.clj"} + :only] ["docker" "" {"compose-service.yaml.template" "compose.yaml" "compose-service-postgres.yaml.template" "compose-service-postgres.yaml" "Dockerfile.template" "Dockerfile"}] - ["resources" "resources" - {"config.edn.template" "config.edn"}] ["src" "src/{{top/file}}/{{main/file}}" {"middleware.clj.template" "middleware.clj" - "parse_system.clj.template" "parse_system.clj" "router.clj.template" "router.clj" "service.clj.template" "service.clj" - "spec.clj.template" "spec.clj"}] + "spec.clj.template" "spec.clj"} + :only] ["test" "test/{{top/file}}/{{main/file}}" {"service_test.clj.template" "service_test.clj"}]]} ;; --------------------------------------------------------- diff --git a/src/practicalli/rules.clj b/src/practicalli/rules.clj new file mode 100644 index 0000000..005c75a --- /dev/null +++ b/src/practicalli/rules.clj @@ -0,0 +1,209 @@ +;; --------------------------------------------------------- +;; Declarative rule sets for deps-new templates +;; +;; Pre-defined rule sets returned by `template-edn` function, +;; based on command line options passed when creating a new project +;; --------------------------------------------------------- + +(ns practicalli.rules + "Declarative rule sets for template options") + +;; --------------------------------------------------------- +;; Service rules + +(def base + "Common declarative rules for all templates" + [["api" "src/{{top/file}}/{{main/file}}/api" + {"system_admin.clj.template" "system_admin.clj" + "scoreboard.clj.template" "scoreboard.clj"}] + ["build" "" + {"build.clj.template" "build.clj" + "deps_donut.edn.template" "deps.edn"} + :only] + ["dev" "dev" + {"mulog.clj" "mulog.clj" + "portal.clj" "portal.clj" + "user.clj" "user.clj"} + :only] + ["docker" "" + {"compose-service.yaml.template" "compose.yaml" + "compose-service-postgres.yaml.template" "compose-service-postgres.yaml" + "Dockerfile.template" "Dockerfile"}] + ["src" "src/{{top/file}}/{{main/file}}" + {"middleware.clj.template" "middleware.clj" + "router.clj.template" "router.clj" + "service_donut.clj.template" "service.clj" + "spec.clj.template" "spec.clj" + "system_donut.clj.template" "system.clj"} + :only] + ["test" "test/{{top/file}}/{{main/file}}" + {"service_test.clj.template" "service_test.clj"}]]) + +(def donut + "Practicalli Service template with `:component :donut` option" + [["api" "src/{{top/file}}/{{main/file}}/api" + {"system_admin.clj.template" "system_admin.clj" + "scoreboard.clj.template" "scoreboard.clj"}] + ["build" "" + {"build.clj.template" "build.clj" + "deps_donut.edn.template" "deps.edn"} + :only] + ["dev" "dev" + {"mulog_events.clj" "mulog_events.clj" + "portal.clj" "portal.clj" + "system_repl_donut.clj.template" "system_repl.clj" + "user_donut.clj" "user.clj"} + :only] + ["docker" "" + {"compose-service.yaml.template" "compose.yaml" + "compose-service-postgres.yaml.template" "compose-service-postgres.yaml" + "Dockerfile.template" "Dockerfile"}] + ["resources" "" + {"config_donut_env.edn.template" "config_donut_env.edn"} + :only] + ["src" "src/{{top/file}}/{{main/file}}" + {"middleware.clj.template" "middleware.clj" + "router.clj.template" "router.clj" + "service_donut.clj.template" "service.clj" + "spec.clj.template" "spec.clj" + "system_donut.clj.template" "system.clj"} + :only] + ["test" "test/{{top/file}}/{{main/file}}" + {"service_test.clj.template" "service_test.clj"}]]) + +(def integrant + "Practicalli Service template with `:component :integrant` option" + [["api" "src/{{top/file}}/{{main/file}}/api" + {"system_admin.clj.template" "system_admin.clj" + "scoreboard.clj.template" "scoreboard.clj"}] + ["build" "" + {"build.clj.template" "build.clj" + "deps_integrant.edn.template" "deps.edn"}] + ["dev" "dev" + {"mulog.clj" "mulog.clj" + "portal.clj" "portal.clj" + "system_repl_integrant.clj.template" "system_repl.clj" + "user_integrant.clj" "user.clj"} + :only] + ["docker" "" + {"compose-service.yaml.template" "compose.yaml" + "compose-service-postgres.yaml.template" "compose-service-postgres.yaml" + "Dockerfile.template" "Dockerfile"}] + ["resources" "resources" + {"config_integrant.edn.template" "config.edn"}] + ["src" "src/{{top/file}}/{{main/file}}" + {"middleware.clj.template" "middleware.clj" + "parse_system_integrant.clj.template" "parse_system.clj" + "router.clj.template" "router.clj" + "service_integrant.clj.template" "service.clj" + "spec.clj.template" "spec.clj" + "system_integrant.clj.template" "system.clj"} + :only] + ["test" "test/{{top/file}}/{{main/file}}" + {"service_test.clj.template" "service_test.clj"}]]) + +;; End of Service rules +;; --------------------------------------------------------- + +(comment + +;; (defn merge-ruleset +;; "Merges two rule sets together" +;; [] +;; (let [merged [] +;; rules (into [] (comp cat (map first) (distinct)) [col [x]])] +;; (into [] mapcat merged) rules)) +;; +;; (let [col [["src" "src/top/main" {"service.clj.template" "service.clj"}] +;; ["dev" "" {"user_donut.clj" "user.clj"}]] +;; x ["dev" "" {"mulog.clj" "mulog.clj"}] +;; ks (into [] (comp cat (map first) (distinct)) [col [x]]) +;; mrg (fn [& fns] +;; (fn [[a] [b]] +;; [(mapv #(%1 %2 %3) fns a b)])) +;; m (->> col +;; (group-by first) +;; (merge-with (mrg #(first %&) str merge) {(first x) [x]}))] +;; (into [] (mapcat m) ks)) +;; ;; [["src" "src/top/main" {"service.clj.template" "service.clj"}] +;; ;; ["dev" "" {"mulog.clj" "mulog.clj", "user_donut.clj" "user.clj"}]] +;; +;; (let [col [["src" "src/top/main" {"service.clj.template" "service.clj"} +;; ["dev" "" {"user_donut.clj" "user.clj"}]]] +;; x ["dev" "" {"mulog.clj" "mulog.clj"}] +;; key-func (juxt first second) +;; mrg (fn [& fns] +;; (fn [[a] [b]] +;; [(mapv #(%1 %2 %3) fns a b)])) +;; m (->> col +;; (group-by key-func) +;; (merge-with (mrg #(first %&) #(first %&) merge) {(key-func x) [x]}))] +;; (into [] (comp cat (map key-func) (distinct) (mapcat m)) [col [x]]))) + + (merge + {"user_donut.clj" "user.clj"} + {"mulog.clj" "mulog.clj"}) +;; {"user_donut.clj" "user.clj", "mulog.clj" "mulog.clj"} + + (merge-with merge + ["src" "src/top/main" {"service.clj.template" "service.clj"} + ["dev" "" {"user_donut.clj" "user.clj"}]] + ["dev" "" {"mulog.clj" "mulog.clj"}]) + + (merge-with merge + {"src" {"service.clj.template" "service.clj"}} + {"dev" {"user_donut.clj" "user.clj"}} + {"dev" {"mulog.clj" "mulog.clj"}}) +;; {"src" {"service.clj.template" "service.clj"}, +;; "dev" {"user_donut.clj" "user.clj", "mulog.clj" "mulog.clj"}} + + (conj + [["src" "src/top/main" {"service.clj.template" "service.clj"}] + ["dev" "" {"user_donut.clj" "user.clj"}]] + ["dev" "" {"mulog.clj" "mulog.clj"}]) +;;[["src" "src/top/main" {"service.clj.template" "service.clj"}] +;; ["dev" "" {"user_donut.clj" "user.clj"}] +;; ["dev" "" {"mulog.clj" "mulog.clj"}]] + + (into + [["dev" "" {"mulog.clj" "mulog.clj"}]] + [["src" "src/top/main" {"service.clj.template" "service.clj"}] + ["dev" "" {"user_donut.clj" "user.clj"}]]) +;;[["dev" "" {"mulog.clj" "mulog.clj"}] +;; ["src" "src/top/main" {"service.clj.template" "service.clj"}] +;; ["dev" "" {"user_donut.clj" "user.clj"}]] + + (into + [["src" "src/top/main" {"service.clj.template" "service.clj"}] + ["dev" "" {"user_donut.clj" "user.clj"}]] + [["dev" "" {"mulog.clj" "mulog.clj"}]]) +;;[["src" "src/top/main" {"service.clj.template" "service.clj"}] +;; ["dev" "" {"user_donut.clj" "user.clj"}] +;; ["dev" "" {"mulog.clj" "mulog.clj"}]] + + (into + [["src" "src/top/main" {"service.clj.template" "service.clj"}] + ["dev" "" {"user_donut.clj" "user.clj"}]] + ["dev" "" {"mulog.clj" "mulog.clj"}]) +;;[["src" "src/top/main" {"service.clj.template" "service.clj"}] +;; ["dev" "" {"user_donut.clj" "user.clj"}] +;; "dev" +;; "" +;; {"mulog.clj" "mulog.clj"}] + + (assoc + [["src" "src/top/main" {"service.clj.template" "service.clj"}] + ["dev" "" {"user_donut.clj" "user.clj"}]] + "dev" {"mulog.clj" "mulog.clj"}) + + (conj + [] + ["dev" "" {"user_donut.clj" "user.clj"}] + ["dev" "" {"mulog.clj" "mulog.clj"}]) +;;[["dev" "" {"user_donut.clj" "user.clj"}] +;; ["dev" "" {"mulog.clj" "mulog.clj"}]] + + (["dev" "" {"user_donut.clj" "user.clj"}] + ["dev" "" {"mulog.clj" "mulog.clj"}])) + + diff --git a/src/practicalli/service.clj b/src/practicalli/service.clj index 7843f76..cc79160 100644 --- a/src/practicalli/service.clj +++ b/src/practicalli/service.clj @@ -10,7 +10,10 @@ (ns practicalli.service - "Programmatic transformation of template data transformation rules") + "Programmatic transformation of template data transformation rules" + (:require + [clojure.pprint :as pprint] + [practicalli.rules :as rules])) #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} @@ -37,6 +40,13 @@ - new template.edn configuration" [edn data] + (pprint/pprint data) ;; Link to Practicalli Clojure Project Templates guide - (println "Template guide: https://practical.li/clojure/clojure-cli/projects/templates/") - edn) + ;; (println "Template guide: https://practical.li/clojure/clojure-cli/projects/templates/") + + (println "Component" (data :component)) + + (cond + (= :donut (data :component)) (assoc edn :transform rules/donut) + (= :integrant (data :component)) (assoc edn :transform rules/integrant) + :else edn))