From d764e551faa6484ad12254f54aac2b3aa5435966 Mon Sep 17 00:00:00 2001 From: <> Date: Fri, 20 Oct 2023 13:46:25 +0000 Subject: [PATCH] Deployed 556c394 with MkDocs version: 1.5.3 --- 404.html | 4 +- .../using-cond-function/index.html | 4 +- app-servers/app-server-logging/index.html | 4 +- app-servers/atom-based-restart/index.html | 4 +- app-servers/clojure-project/index.html | 4 +- app-servers/create-server/index.html | 4 +- app-servers/debugging/index.html | 4 +- .../http-kit-server-options/index.html | 4 +- app-servers/index.html | 4 +- app-servers/java-system-properties/index.html | 4 +- app-servers/jetty-server-options/index.html | 4 +- app-servers/middleware/index.html | 4 +- app-servers/overview/index.html | 4 +- app-servers/route-requests/index.html | 4 +- app-servers/routing-libraries/index.html | 4 +- app-servers/routing/index.html | 4 +- app-servers/set-listen-port/index.html | 4 +- app-servers/simple-restart/index.html | 4 +- app-servers/start-server/index.html | 4 +- app-servers/static-content/index.html | 4 +- assets/images/social/index.html | 4 +- assets/javascripts/lunr/min/lunr.el.min.js | 1 + assets/stylesheets/main.35e1ed30.min.css | 1 + assets/stylesheets/main.35e1ed30.min.css.map | 1 + assets/stylesheets/main.6a10b989.min.css | 1 - assets/stylesheets/main.6a10b989.min.css.map | 1 - building-api/cheshire/index.html | 4 +- .../compojure-api-template/index.html | 4 +- .../create-compojure-api-project/index.html | 4 +- .../end-to-end-testing/curl/index.html | 4 +- .../end-to-end-testing/httpie/index.html | 4 +- building-api/end-to-end-testing/index.html | 4 +- .../end-to-end-testing/postman/index.html | 4 +- .../end-to-end-testing/swagger/index.html | 4 +- building-api/index.html | 4 +- building-api/json-files/index.html | 4 +- building-api/plumatic-schema/index.html | 4 +- .../create-project/index.html | 4 +- .../projects/game-scoreboard-ui/index.html | 4 +- .../defining-scoreboard/index.html | 4 +- .../defining-scores/index.html | 4 +- .../projects/game-scoreboard/index.html | 4 +- building-api/reitit/index.html | 4 +- building-api/ring-mock/index.html | 4 +- building-api/ring-swagger/index.html | 4 +- building-api/swagger/index.html | 4 +- building-api/terminology/index.html | 4 +- building-api/testing-api/index.html | 4 +- clojure-databases/crux/index.html | 4 +- clojure-databases/index.html | 4 +- full-app/index.html | 4 +- index.html | 4 +- introduction/contributing/index.html | 4 +- introduction/overview/index.html | 4 +- introduction/repl-workflow/index.html | 4 +- introduction/requirements/index.html | 4 +- introduction/writing-tips/index.html | 51 +- .../reitit/constructing-routes/index.html | 4 +- libraries/reitit/index.html | 4 +- micro-framework/edge/index.html | 4 +- micro-framework/index.html | 4 +- micro-framework/luminus/index.html | 4 +- micro-framework/pedestal/index.html | 4 +- micro-services/index.html | 4 +- .../add-alias-to-database/index.html | 4 +- .../add-static-resources/index.html | 4 +- .../alias-generator/index.html | 4 +- .../compojure-template/index.html | 4 +- .../create-database/index.html | 4 +- .../create-html-form/index.html | 4 +- .../create-project/index.html | 4 +- .../delete-alias-from-database/index.html | 4 +- .../design-data-structure/index.html | 4 +- .../disable-anti-forgery-check/index.html | 4 +- .../get-alias-from-database/index.html | 4 +- project-url-shortner/html-form/index.html | 4 +- .../if-let-function/index.html | 4 +- project-url-shortner/index.html | 4 +- .../named-alias-handler/index.html | 4 +- .../persist-aliases/index.html | 4 +- .../postgres-setup/index.html | 4 +- .../redirect-to-full-url/index.html | 4 +- project-url-shortner/redis-setup/index.html | 4 +- .../refacor-hiccup-form/index.html | 4 +- .../return-short-url/index.html | 4 +- .../return-url-aliases/index.html | 4 +- project-url-shortner/run-project/index.html | 4 +- .../test-app-reloading/index.html | 4 +- .../using-ring-redirect/index.html | 4 +- .../whats-in-a-request/index.html | 4 +- .../account-overview-page/index.html | 4 +- .../clojure-server-project/index.html | 4 +- .../index.html | 4 +- .../continuous-integration/index.html | 4 +- .../create-records/index.html | 4 +- .../cyclic-load-dependency/index.html | 4 +- .../database-queries/index.html | 4 +- .../database-tables/index.html | 4 +- .../delete-records/index.html | 4 +- .../deployment-pipeline/index.html | 4 +- .../deployment-via-ci/index.html | 4 +- .../development-database/index.html | 4 +- .../generate-data-from-specs/index.html | 4 +- .../banking-on-clojure/honeysql/index.html | 4 +- projects/banking-on-clojure/index.html | 4 +- .../instrument-next-jdbc-functions/index.html | 4 +- .../namespace-design/index.html | 4 +- .../production-database/index.html | 4 +- .../read-records/index.html | 4 +- .../refactor-handler/index.html | 4 +- .../spec-generative-testing/index.html | 4 +- .../ui-handler-functions/index.html | 4 +- .../unit-testing-the-database/index.html | 4 +- .../banking-on-clojure/unit-tests/index.html | 4 +- .../update-records/index.html | 4 +- projects/game-scoreboard-api/index.html | 4 +- projects/index.html | 4 +- .../todo-app/compojure/about/index.html | 4 +- .../compojure/adding-dependency/index.html | 4 +- .../compojure/adding-goodbye-route/index.html | 4 +- .../todo-app/compojure/code-so-far/index.html | 4 +- .../todo-app/compojure/defroutes/index.html | 4 +- .../leiningen/todo-app/compojure/index.html | 4 +- .../compojure/lisp-calculator/index.html | 4 +- .../compojure/show-request-info/index.html | 4 +- .../theory-local-name-bindings/index.html | 4 +- .../compojure/theory-routing/index.html | 4 +- .../theory-using-hash-maps/index.html | 4 +- .../compojure/using-compojure/index.html | 4 +- .../variable-path-elements/index.html | 4 +- .../add-database-dependencies/index.html | 4 +- .../define-db-connection/index.html | 4 +- .../todo-app/connect-to-postgres/index.html | 4 +- .../add-not-found/index.html | 4 +- .../code-so-far/index.html | 4 +- .../if-function/index.html | 4 +- .../create-a-handler-function/index.html | 4 +- .../maps-and-keywords/index.html | 4 +- .../create-a-project/code-so-far/index.html | 4 +- .../todo-app/create-a-project/index.html | 4 +- .../update-project-details/index.html | 4 +- .../add-a-jetty-webserver/index.html | 4 +- .../add-ring-dependency/index.html | 4 +- .../code-so-far/index.html | 4 +- .../coersing-types-and-java-lang/index.html | 4 +- .../configure-main-namespace/index.html | 4 +- .../include-ring-library/index.html | 4 +- .../create-a-webserver-with-ring/index.html | 4 +- .../namespaces/index.html | 4 +- .../run-webserver/index.html | 4 +- .../alternative-approaches/index.html | 4 +- .../database-model/create-table/index.html | 4 +- .../database-model/create-task/index.html | 4 +- .../database-model/delete-task/index.html | 4 +- .../todo-app/database-model/index.html | 4 +- .../database-model/show-all-task/index.html | 4 +- .../todo-app/heroku/code-so-far/index.html | 4 +- .../todo-app/heroku/deploy/index.html | 4 +- projects/leiningen/todo-app/heroku/index.html | 4 +- .../todo-app/heroku/procfile/index.html | 4 +- .../todo-app/heroku/update-project/index.html | 4 +- .../todo-app/hiccup/code-so-far/index.html | 4 +- .../hiccup/create-new-handler/index.html | 4 +- projects/leiningen/todo-app/hiccup/index.html | 4 +- .../updating-handlers-with-hiccup/index.html | 4 +- projects/leiningen/todo-app/index.html | 4 +- .../todo-app/introducing-ring/index.html | 4 +- .../index.html | 4 +- .../todo-app/postgres/dataclips/index.html | 4 +- .../postgres/environment-variables/index.html | 4 +- .../leiningen/todo-app/postgres/index.html | 4 +- .../todo-app/postgres/install/index.html | 4 +- .../todo-app/postgres/jira-ticket/index.html | 4 +- .../postgres/lobo-table-creation/index.html | 4 +- .../todo-app/postgres/pg-admin/index.html | 4 +- .../todo-app/postgres/postgres-cli/index.html | 4 +- .../postgres/postgres-commands/index.html | 4 +- .../postgres-performance-analytics/index.html | 4 +- .../postgres-toolbelt-commands/index.html | 4 +- .../refactor-namespace/base-routes/index.html | 4 +- .../refactor-namespace/code-so-far/index.html | 4 +- .../refactor-namespace/core/index.html | 4 +- .../todo-app/refactor-namespace/index.html | 4 +- .../refactor-namespace/play-routes/index.html | 4 +- .../refactor-namespace/task-routes/index.html | 4 +- .../code-so-far/index.html | 4 +- .../reloading-the-application/index.html | 4 +- .../middleware/index.html | 4 +- .../test-your-code-reloads/index.html | 4 +- .../task-handlers/add-a-task/index.html | 4 +- .../task-handlers/delete-a-task/index.html | 4 +- .../todo-app/task-handlers/index.html | 4 +- .../task-handlers/show-task/index.html | 4 +- .../unit-test-handler-function/index.html | 4 +- projects/leiningen/working-example/index.html | 4 +- .../slack-app/create-slack-app/index.html | 4 +- projects/slack-app/index.html | 4 +- .../slack-app/slack-api-methods/index.html | 4 +- projects/slack-app/slack-scopes/index.html | 4 +- .../application-server/index.html | 4 +- .../continuous-integration/index.html | 4 +- .../debugging-requests/index.html | 4 +- .../deployment-via-ci/index.html | 4 +- projects/status-monitor-deps/index.html | 4 +- .../refactor-handlers-and-tests/index.html | 4 +- .../unit-test-mocking-handlers/index.html | 4 +- .../continuous-integration/heroku/index.html | 4 +- reference/index.html | 4 +- reference/ring/index.html | 4 +- reference/ring/request-map/index.html | 4 +- .../h2-database/database-tools/index.html | 4 +- .../h2-database/index.html | 4 +- .../h2-database/schema-design/index.html | 4 +- relational-databases-and-sql/index.html | 4 +- .../managing-connections/index.html | 4 +- .../add-to-project/index.html | 4 +- .../connection-pool-lifecycle/index.html | 4 +- .../database-specifications/index.html | 4 +- .../next-jdbc-library/index.html | 4 +- .../next-jdbc-and-resultsets/index.html | 4 +- .../simple-example/index.html | 4 +- .../postgresql-database/index.html | 4 +- search/search_index.json | 2 +- service-repl-workflow/aero/index.html | 4 +- service-repl-workflow/donut-system/index.html | 4 +- service-repl-workflow/index.html | 4 +- service-repl-workflow/integrant/index.html | 4 +- .../integrant/integrant-system/index.html | 4 +- .../integrant/repl/index.html | 4 +- service-repl-workflow/mulog-events/index.html | 4 +- service-repl-workflow/portal/index.html | 4 +- service-repl-workflow/system-repl/index.html | 4 +- sitemap.xml | 452 +++++++++--------- sitemap.xml.gz | Bin 2265 -> 2264 bytes 234 files changed, 683 insertions(+), 727 deletions(-) create mode 100644 assets/javascripts/lunr/min/lunr.el.min.js create mode 100644 assets/stylesheets/main.35e1ed30.min.css create mode 100644 assets/stylesheets/main.35e1ed30.min.css.map delete mode 100644 assets/stylesheets/main.6a10b989.min.css delete mode 100644 assets/stylesheets/main.6a10b989.min.css.map diff --git a/404.html b/404.html index f948424d..09a117a4 100644 --- a/404.html +++ b/404.html @@ -28,7 +28,7 @@ - + @@ -36,7 +36,7 @@ - + diff --git a/adding-more-route/using-cond-function/index.html b/adding-more-route/using-cond-function/index.html index 421ad9ed..d899fd37 100644 --- a/adding-more-route/using-cond-function/index.html +++ b/adding-more-route/using-cond-function/index.html @@ -25,7 +25,7 @@ - + @@ -33,7 +33,7 @@ - + diff --git a/app-servers/app-server-logging/index.html b/app-servers/app-server-logging/index.html index 6a97ceb2..d67ffb9f 100644 --- a/app-servers/app-server-logging/index.html +++ b/app-servers/app-server-logging/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/app-servers/atom-based-restart/index.html b/app-servers/atom-based-restart/index.html index 66f3d3ba..d7b2c975 100644 --- a/app-servers/atom-based-restart/index.html +++ b/app-servers/atom-based-restart/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/app-servers/clojure-project/index.html b/app-servers/clojure-project/index.html index 5f5f3e0c..cd39de68 100644 --- a/app-servers/clojure-project/index.html +++ b/app-servers/clojure-project/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/app-servers/create-server/index.html b/app-servers/create-server/index.html index 7d87e4bc..1c118847 100644 --- a/app-servers/create-server/index.html +++ b/app-servers/create-server/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/app-servers/debugging/index.html b/app-servers/debugging/index.html index 196be36e..61183134 100644 --- a/app-servers/debugging/index.html +++ b/app-servers/debugging/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/app-servers/http-kit-server-options/index.html b/app-servers/http-kit-server-options/index.html index ce2e4d3e..19343737 100644 --- a/app-servers/http-kit-server-options/index.html +++ b/app-servers/http-kit-server-options/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/app-servers/index.html b/app-servers/index.html index f2de6ef3..895b483b 100644 --- a/app-servers/index.html +++ b/app-servers/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/app-servers/java-system-properties/index.html b/app-servers/java-system-properties/index.html index 946872c3..360546f2 100644 --- a/app-servers/java-system-properties/index.html +++ b/app-servers/java-system-properties/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/app-servers/jetty-server-options/index.html b/app-servers/jetty-server-options/index.html index 513bad2d..7b3cab76 100644 --- a/app-servers/jetty-server-options/index.html +++ b/app-servers/jetty-server-options/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/app-servers/middleware/index.html b/app-servers/middleware/index.html index b7112438..a87576e4 100644 --- a/app-servers/middleware/index.html +++ b/app-servers/middleware/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/app-servers/overview/index.html b/app-servers/overview/index.html index 6bf09556..c4078a64 100644 --- a/app-servers/overview/index.html +++ b/app-servers/overview/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/app-servers/route-requests/index.html b/app-servers/route-requests/index.html index b2e1f6e6..7df62686 100644 --- a/app-servers/route-requests/index.html +++ b/app-servers/route-requests/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/app-servers/routing-libraries/index.html b/app-servers/routing-libraries/index.html index 2988cd88..446a9a62 100644 --- a/app-servers/routing-libraries/index.html +++ b/app-servers/routing-libraries/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/app-servers/routing/index.html b/app-servers/routing/index.html index 33ebfb4a..37830598 100644 --- a/app-servers/routing/index.html +++ b/app-servers/routing/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/app-servers/set-listen-port/index.html b/app-servers/set-listen-port/index.html index 1ea9e8bc..0f827114 100644 --- a/app-servers/set-listen-port/index.html +++ b/app-servers/set-listen-port/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/app-servers/simple-restart/index.html b/app-servers/simple-restart/index.html index 33a9cb6a..8aa86070 100644 --- a/app-servers/simple-restart/index.html +++ b/app-servers/simple-restart/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/app-servers/start-server/index.html b/app-servers/start-server/index.html index f17af99c..24672150 100644 --- a/app-servers/start-server/index.html +++ b/app-servers/start-server/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/app-servers/static-content/index.html b/app-servers/static-content/index.html index b43c62ca..bac872e2 100644 --- a/app-servers/static-content/index.html +++ b/app-servers/static-content/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/assets/images/social/index.html b/assets/images/social/index.html index 45f9bef8..c2ffb87e 100644 --- a/assets/images/social/index.html +++ b/assets/images/social/index.html @@ -25,7 +25,7 @@ - + @@ -33,7 +33,7 @@ - + diff --git a/assets/javascripts/lunr/min/lunr.el.min.js b/assets/javascripts/lunr/min/lunr.el.min.js new file mode 100644 index 00000000..ace017bd --- /dev/null +++ b/assets/javascripts/lunr/min/lunr.el.min.js @@ -0,0 +1 @@ +!function(e,t){"function"==typeof define&&define.amd?define(t):"object"==typeof exports?module.exports=t():t()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.el=function(){this.pipeline.reset(),void 0===this.searchPipeline&&this.pipeline.add(e.el.trimmer,e.el.normilizer),this.pipeline.add(e.el.stopWordFilter,e.el.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.el.stemmer))},e.el.wordCharacters="A-Za-zΑαΒβΓγΔδΕεΖζΗηΘθΙιΚκΛλΜμΝνΞξΟοΠπΡρΣσςΤτΥυΦφΧχΨψΩωΆάΈέΉήΊίΌόΎύΏώΪΐΫΰΐΰ",e.el.trimmer=e.trimmerSupport.generateTrimmer(e.el.wordCharacters),e.Pipeline.registerFunction(e.el.trimmer,"trimmer-el"),e.el.stemmer=function(){function e(e){return s.test(e)}function t(e){return/[ΑΕΗΙΟΥΩ]$/.test(e)}function r(e){return/[ΑΕΗΙΟΩ]$/.test(e)}function n(n){var s=n;if(n.length<3)return s;if(!e(n))return s;if(i.indexOf(n)>=0)return s;var u=new RegExp("(.*)("+Object.keys(l).join("|")+")$"),o=u.exec(s);return null!==o&&(s=o[1]+l[o[2]]),null!==(o=/^(.+?)(ΑΔΕΣ|ΑΔΩΝ)$/.exec(s))&&(s=o[1],/(ΟΚ|ΜΑΜ|ΜΑΝ|ΜΠΑΜΠ|ΠΑΤΕΡ|ΓΙΑΓΙ|ΝΤΑΝΤ|ΚΥΡ|ΘΕΙ|ΠΕΘΕΡ|ΜΟΥΣΑΜ|ΚΑΠΛΑΜ|ΠΑΡ|ΨΑΡ|ΤΖΟΥΡ|ΤΑΜΠΟΥΡ|ΓΑΛΑΤ|ΦΑΦΛΑΤ)$/.test(o[1])||(s+="ΑΔ")),null!==(o=/^(.+?)(ΕΔΕΣ|ΕΔΩΝ)$/.exec(s))&&(s=o[1],/(ΟΠ|ΙΠ|ΕΜΠ|ΥΠ|ΓΗΠ|ΔΑΠ|ΚΡΑΣΠ|ΜΙΛ)$/.test(o[1])&&(s+="ΕΔ")),null!==(o=/^(.+?)(ΟΥΔΕΣ|ΟΥΔΩΝ)$/.exec(s))&&(s=o[1],/(ΑΡΚ|ΚΑΛΙΑΚ|ΠΕΤΑΛ|ΛΙΧ|ΠΛΕΞ|ΣΚ|Σ|ΦΛ|ΦΡ|ΒΕΛ|ΛΟΥΛ|ΧΝ|ΣΠ|ΤΡΑΓ|ΦΕ)$/.test(o[1])&&(s+="ΟΥΔ")),null!==(o=/^(.+?)(ΕΩΣ|ΕΩΝ|ΕΑΣ|ΕΑ)$/.exec(s))&&(s=o[1],/^(Θ|Δ|ΕΛ|ΓΑΛ|Ν|Π|ΙΔ|ΠΑΡ|ΣΤΕΡ|ΟΡΦ|ΑΝΔΡ|ΑΝΤΡ)$/.test(o[1])&&(s+="Ε")),null!==(o=/^(.+?)(ΕΙΟ|ΕΙΟΣ|ΕΙΟΙ|ΕΙΑ|ΕΙΑΣ|ΕΙΕΣ|ΕΙΟΥ|ΕΙΟΥΣ|ΕΙΩΝ)$/.exec(s))&&o[1].length>4&&(s=o[1]),null!==(o=/^(.+?)(ΙΟΥΣ|ΙΑΣ|ΙΕΣ|ΙΟΣ|ΙΟΥ|ΙΟΙ|ΙΩΝ|ΙΟΝ|ΙΑ|ΙΟ)$/.exec(s))&&(s=o[1],(t(s)||s.length<2||/^(ΑΓ|ΑΓΓΕΛ|ΑΓΡ|ΑΕΡ|ΑΘΛ|ΑΚΟΥΣ|ΑΞ|ΑΣ|Β|ΒΙΒΛ|ΒΥΤ|Γ|ΓΙΑΓ|ΓΩΝ|Δ|ΔΑΝ|ΔΗΛ|ΔΗΜ|ΔΟΚΙΜ|ΕΛ|ΖΑΧΑΡ|ΗΛ|ΗΠ|ΙΔ|ΙΣΚ|ΙΣΤ|ΙΟΝ|ΙΩΝ|ΚΙΜΩΛ|ΚΟΛΟΝ|ΚΟΡ|ΚΤΗΡ|ΚΥΡ|ΛΑΓ|ΛΟΓ|ΜΑΓ|ΜΠΑΝ|ΜΠΡ|ΝΑΥΤ|ΝΟΤ|ΟΠΑΛ|ΟΞ|ΟΡ|ΟΣ|ΠΑΝΑΓ|ΠΑΤΡ|ΠΗΛ|ΠΗΝ|ΠΛΑΙΣ|ΠΟΝΤ|ΡΑΔ|ΡΟΔ|ΣΚ|ΣΚΟΡΠ|ΣΟΥΝ|ΣΠΑΝ|ΣΤΑΔ|ΣΥΡ|ΤΗΛ|ΤΙΜ|ΤΟΚ|ΤΟΠ|ΤΡΟΧ|ΦΙΛ|ΦΩΤ|Χ|ΧΙΛ|ΧΡΩΜ|ΧΩΡ)$/.test(o[1]))&&(s+="Ι"),/^(ΠΑΛ)$/.test(o[1])&&(s+="ΑΙ")),null!==(o=/^(.+?)(ΙΚΟΣ|ΙΚΟΝ|ΙΚΕΙΣ|ΙΚΟΙ|ΙΚΕΣ|ΙΚΟΥΣ|ΙΚΗ|ΙΚΗΣ|ΙΚΟ|ΙΚΑ|ΙΚΟΥ|ΙΚΩΝ|ΙΚΩΣ)$/.exec(s))&&(s=o[1],(t(s)||/^(ΑΔ|ΑΛ|ΑΜΑΝ|ΑΜΕΡ|ΑΜΜΟΧΑΛ|ΑΝΗΘ|ΑΝΤΙΔ|ΑΠΛ|ΑΤΤ|ΑΦΡ|ΒΑΣ|ΒΡΩΜ|ΓΕΝ|ΓΕΡ|Δ|ΔΙΚΑΝ|ΔΥΤ|ΕΙΔ|ΕΝΔ|ΕΞΩΔ|ΗΘ|ΘΕΤ|ΚΑΛΛΙΝ|ΚΑΛΠ|ΚΑΤΑΔ|ΚΟΥΖΙΝ|ΚΡ|ΚΩΔ|ΛΟΓ|Μ|ΜΕΡ|ΜΟΝΑΔ|ΜΟΥΛ|ΜΟΥΣ|ΜΠΑΓΙΑΤ|ΜΠΑΝ|ΜΠΟΛ|ΜΠΟΣ|ΜΥΣΤ|Ν|ΝΙΤ|ΞΙΚ|ΟΠΤ|ΠΑΝ|ΠΕΤΣ|ΠΙΚΑΝΤ|ΠΙΤΣ|ΠΛΑΣΤ|ΠΛΙΑΤΣ|ΠΟΝΤ|ΠΟΣΤΕΛΝ|ΠΡΩΤΟΔ|ΣΕΡΤ|ΣΗΜΑΝΤ|ΣΤΑΤ|ΣΥΝΑΔ|ΣΥΝΟΜΗΛ|ΤΕΛ|ΤΕΧΝ|ΤΡΟΠ|ΤΣΑΜ|ΥΠΟΔ|Φ|ΦΙΛΟΝ|ΦΥΛΟΔ|ΦΥΣ|ΧΑΣ)$/.test(o[1])||/(ΦΟΙΝ)$/.test(o[1]))&&(s+="ΙΚ")),"ΑΓΑΜΕ"===s&&(s="ΑΓΑΜ"),null!==(o=/^(.+?)(ΑΓΑΜΕ|ΗΣΑΜΕ|ΟΥΣΑΜΕ|ΗΚΑΜΕ|ΗΘΗΚΑΜΕ)$/.exec(s))&&(s=o[1]),null!==(o=/^(.+?)(ΑΜΕ)$/.exec(s))&&(s=o[1],/^(ΑΝΑΠ|ΑΠΟΘ|ΑΠΟΚ|ΑΠΟΣΤ|ΒΟΥΒ|ΞΕΘ|ΟΥΛ|ΠΕΘ|ΠΙΚΡ|ΠΟΤ|ΣΙΧ|Χ)$/.test(o[1])&&(s+="ΑΜ")),null!==(o=/^(.+?)(ΑΓΑΝΕ|ΗΣΑΝΕ|ΟΥΣΑΝΕ|ΙΟΝΤΑΝΕ|ΙΟΤΑΝΕ|ΙΟΥΝΤΑΝΕ|ΟΝΤΑΝΕ|ΟΤΑΝΕ|ΟΥΝΤΑΝΕ|ΗΚΑΝΕ|ΗΘΗΚΑΝΕ)$/.exec(s))&&(s=o[1],/^(ΤΡ|ΤΣ)$/.test(o[1])&&(s+="ΑΓΑΝ")),null!==(o=/^(.+?)(ΑΝΕ)$/.exec(s))&&(s=o[1],(r(s)||/^(ΒΕΤΕΡ|ΒΟΥΛΚ|ΒΡΑΧΜ|Γ|ΔΡΑΔΟΥΜ|Θ|ΚΑΛΠΟΥΖ|ΚΑΣΤΕΛ|ΚΟΡΜΟΡ|ΛΑΟΠΛ|ΜΩΑΜΕΘ|Μ|ΜΟΥΣΟΥΛΜΑΝ|ΟΥΛ|Π|ΠΕΛΕΚ|ΠΛ|ΠΟΛΙΣ|ΠΟΡΤΟΛ|ΣΑΡΑΚΑΤΣ|ΣΟΥΛΤ|ΤΣΑΡΛΑΤ|ΟΡΦ|ΤΣΙΓΓ|ΤΣΟΠ|ΦΩΤΟΣΤΕΦ|Χ|ΨΥΧΟΠΛ|ΑΓ|ΟΡΦ|ΓΑΛ|ΓΕΡ|ΔΕΚ|ΔΙΠΛ|ΑΜΕΡΙΚΑΝ|ΟΥΡ|ΠΙΘ|ΠΟΥΡΙΤ|Σ|ΖΩΝΤ|ΙΚ|ΚΑΣΤ|ΚΟΠ|ΛΙΧ|ΛΟΥΘΗΡ|ΜΑΙΝΤ|ΜΕΛ|ΣΙΓ|ΣΠ|ΣΤΕΓ|ΤΡΑΓ|ΤΣΑΓ|Φ|ΕΡ|ΑΔΑΠ|ΑΘΙΓΓ|ΑΜΗΧ|ΑΝΙΚ|ΑΝΟΡΓ|ΑΠΗΓ|ΑΠΙΘ|ΑΤΣΙΓΓ|ΒΑΣ|ΒΑΣΚ|ΒΑΘΥΓΑΛ|ΒΙΟΜΗΧ|ΒΡΑΧΥΚ|ΔΙΑΤ|ΔΙΑΦ|ΕΝΟΡΓ|ΘΥΣ|ΚΑΠΝΟΒΙΟΜΗΧ|ΚΑΤΑΓΑΛ|ΚΛΙΒ|ΚΟΙΛΑΡΦ|ΛΙΒ|ΜΕΓΛΟΒΙΟΜΗΧ|ΜΙΚΡΟΒΙΟΜΗΧ|ΝΤΑΒ|ΞΗΡΟΚΛΙΒ|ΟΛΙΓΟΔΑΜ|ΟΛΟΓΑΛ|ΠΕΝΤΑΡΦ|ΠΕΡΗΦ|ΠΕΡΙΤΡ|ΠΛΑΤ|ΠΟΛΥΔΑΠ|ΠΟΛΥΜΗΧ|ΣΤΕΦ|ΤΑΒ|ΤΕΤ|ΥΠΕΡΗΦ|ΥΠΟΚΟΠ|ΧΑΜΗΛΟΔΑΠ|ΨΗΛΟΤΑΒ)$/.test(o[1]))&&(s+="ΑΝ")),null!==(o=/^(.+?)(ΗΣΕΤΕ)$/.exec(s))&&(s=o[1]),null!==(o=/^(.+?)(ΕΤΕ)$/.exec(s))&&(s=o[1],(r(s)||/(ΟΔ|ΑΙΡ|ΦΟΡ|ΤΑΘ|ΔΙΑΘ|ΣΧ|ΕΝΔ|ΕΥΡ|ΤΙΘ|ΥΠΕΡΘ|ΡΑΘ|ΕΝΘ|ΡΟΘ|ΣΘ|ΠΥΡ|ΑΙΝ|ΣΥΝΔ|ΣΥΝ|ΣΥΝΘ|ΧΩΡ|ΠΟΝ|ΒΡ|ΚΑΘ|ΕΥΘ|ΕΚΘ|ΝΕΤ|ΡΟΝ|ΑΡΚ|ΒΑΡ|ΒΟΛ|ΩΦΕΛ)$/.test(o[1])||/^(ΑΒΑΡ|ΒΕΝ|ΕΝΑΡ|ΑΒΡ|ΑΔ|ΑΘ|ΑΝ|ΑΠΛ|ΒΑΡΟΝ|ΝΤΡ|ΣΚ|ΚΟΠ|ΜΠΟΡ|ΝΙΦ|ΠΑΓ|ΠΑΡΑΚΑΛ|ΣΕΡΠ|ΣΚΕΛ|ΣΥΡΦ|ΤΟΚ|Υ|Δ|ΕΜ|ΘΑΡΡ|Θ)$/.test(o[1]))&&(s+="ΕΤ")),null!==(o=/^(.+?)(ΟΝΤΑΣ|ΩΝΤΑΣ)$/.exec(s))&&(s=o[1],/^ΑΡΧ$/.test(o[1])&&(s+="ΟΝΤ"),/ΚΡΕ$/.test(o[1])&&(s+="ΩΝΤ")),null!==(o=/^(.+?)(ΟΜΑΣΤΕ|ΙΟΜΑΣΤΕ)$/.exec(s))&&(s=o[1],/^ΟΝ$/.test(o[1])&&(s+="ΟΜΑΣΤ")),null!==(o=/^(.+?)(ΙΕΣΤΕ)$/.exec(s))&&(s=o[1],/^(Π|ΑΠ|ΣΥΜΠ|ΑΣΥΜΠ|ΑΚΑΤΑΠ|ΑΜΕΤΑΜΦ)$/.test(o[1])&&(s+="ΙΕΣΤ")),null!==(o=/^(.+?)(ΕΣΤΕ)$/.exec(s))&&(s=o[1],/^(ΑΛ|ΑΡ|ΕΚΤΕΛ|Ζ|Μ|Ξ|ΠΑΡΑΚΑΛ|ΠΡΟ|ΝΙΣ)$/.test(o[1])&&(s+="ΕΣΤ")),null!==(o=/^(.+?)(ΗΘΗΚΑ|ΗΘΗΚΕΣ|ΗΘΗΚΕ)$/.exec(s))&&(s=o[1]),null!==(o=/^(.+?)(ΗΚΑ|ΗΚΕΣ|ΗΚΕ)$/.exec(s))&&(s=o[1],(/(ΣΚΩΛ|ΣΚΟΥΛ|ΝΑΡΘ|ΣΦ|ΟΘ|ΠΙΘ)$/.test(o[1])||/^(ΔΙΑΘ|Θ|ΠΑΡΑΚΑΤΑΘ|ΠΡΟΣΘ|ΣΥΝΘ)$/.test(o[1]))&&(s+="ΗΚ")),null!==(o=/^(.+?)(ΟΥΣΑ|ΟΥΣΕΣ|ΟΥΣΕ)$/.exec(s))&&(s=o[1],(t(s)||/^(ΦΑΡΜΑΚ|ΧΑΔ|ΑΓΚ|ΑΝΑΡΡ|ΒΡΟΜ|ΕΚΛΙΠ|ΛΑΜΠΙΔ|ΛΕΧ|Μ|ΠΑΤ|Ρ|Λ|ΜΕΔ|ΜΕΣΑΖ|ΥΠΟΤΕΙΝ|ΑΜ|ΑΙΘ|ΑΝΗΚ|ΔΕΣΠΟΖ|ΕΝΔΙΑΦΕΡ)$/.test(o[1])||/(ΠΟΔΑΡ|ΒΛΕΠ|ΠΑΝΤΑΧ|ΦΡΥΔ|ΜΑΝΤΙΛ|ΜΑΛΛ|ΚΥΜΑΤ|ΛΑΧ|ΛΗΓ|ΦΑΓ|ΟΜ|ΠΡΩΤ)$/.test(o[1]))&&(s+="ΟΥΣ")),null!==(o=/^(.+?)(ΑΓΑ|ΑΓΕΣ|ΑΓΕ)$/.exec(s))&&(s=o[1],(/^(ΑΒΑΣΤ|ΠΟΛΥΦ|ΑΔΗΦ|ΠΑΜΦ|Ρ|ΑΣΠ|ΑΦ|ΑΜΑΛ|ΑΜΑΛΛΙ|ΑΝΥΣΤ|ΑΠΕΡ|ΑΣΠΑΡ|ΑΧΑΡ|ΔΕΡΒΕΝ|ΔΡΟΣΟΠ|ΞΕΦ|ΝΕΟΠ|ΝΟΜΟΤ|ΟΛΟΠ|ΟΜΟΤ|ΠΡΟΣΤ|ΠΡΟΣΩΠΟΠ|ΣΥΜΠ|ΣΥΝΤ|Τ|ΥΠΟΤ|ΧΑΡ|ΑΕΙΠ|ΑΙΜΟΣΤ|ΑΝΥΠ|ΑΠΟΤ|ΑΡΤΙΠ|ΔΙΑΤ|ΕΝ|ΕΠΙΤ|ΚΡΟΚΑΛΟΠ|ΣΙΔΗΡΟΠ|Λ|ΝΑΥ|ΟΥΛΑΜ|ΟΥΡ|Π|ΤΡ|Μ)$/.test(o[1])||/(ΟΦ|ΠΕΛ|ΧΟΡΤ|ΛΛ|ΣΦ|ΡΠ|ΦΡ|ΠΡ|ΛΟΧ|ΣΜΗΝ)$/.test(o[1])&&!/^(ΨΟΦ|ΝΑΥΛΟΧ)$/.test(o[1])||/(ΚΟΛΛ)$/.test(o[1]))&&(s+="ΑΓ")),null!==(o=/^(.+?)(ΗΣΕ|ΗΣΟΥ|ΗΣΑ)$/.exec(s))&&(s=o[1],/^(Ν|ΧΕΡΣΟΝ|ΔΩΔΕΚΑΝ|ΕΡΗΜΟΝ|ΜΕΓΑΛΟΝ|ΕΠΤΑΝ|Ι)$/.test(o[1])&&(s+="ΗΣ")),null!==(o=/^(.+?)(ΗΣΤΕ)$/.exec(s))&&(s=o[1],/^(ΑΣΒ|ΣΒ|ΑΧΡ|ΧΡ|ΑΠΛ|ΑΕΙΜΝ|ΔΥΣΧΡ|ΕΥΧΡ|ΚΟΙΝΟΧΡ|ΠΑΛΙΜΨ)$/.test(o[1])&&(s+="ΗΣΤ")),null!==(o=/^(.+?)(ΟΥΝΕ|ΗΣΟΥΝΕ|ΗΘΟΥΝΕ)$/.exec(s))&&(s=o[1],/^(Ν|Ρ|ΣΠΙ|ΣΤΡΑΒΟΜΟΥΤΣ|ΚΑΚΟΜΟΥΤΣ|ΕΞΩΝ)$/.test(o[1])&&(s+="ΟΥΝ")),null!==(o=/^(.+?)(ΟΥΜΕ|ΗΣΟΥΜΕ|ΗΘΟΥΜΕ)$/.exec(s))&&(s=o[1],/^(ΠΑΡΑΣΟΥΣ|Φ|Χ|ΩΡΙΟΠΛ|ΑΖ|ΑΛΛΟΣΟΥΣ|ΑΣΟΥΣ)$/.test(o[1])&&(s+="ΟΥΜ")),null!=(o=/^(.+?)(ΜΑΤΟΙ|ΜΑΤΟΥΣ|ΜΑΤΟ|ΜΑΤΑ|ΜΑΤΩΣ|ΜΑΤΩΝ|ΜΑΤΟΣ|ΜΑΤΕΣ|ΜΑΤΗ|ΜΑΤΗΣ|ΜΑΤΟΥ)$/.exec(s))&&(s=o[1]+"Μ",/^(ΓΡΑΜ)$/.test(o[1])?s+="Α":/^(ΓΕ|ΣΤΑ)$/.test(o[1])&&(s+="ΑΤ")),null!==(o=/^(.+?)(ΟΥΑ)$/.exec(s))&&(s=o[1]+"ΟΥ"),n.length===s.length&&null!==(o=/^(.+?)(Α|ΑΓΑΤΕ|ΑΓΑΝ|ΑΕΙ|ΑΜΑΙ|ΑΝ|ΑΣ|ΑΣΑΙ|ΑΤΑΙ|ΑΩ|Ε|ΕΙ|ΕΙΣ|ΕΙΤΕ|ΕΣΑΙ|ΕΣ|ΕΤΑΙ|Ι|ΙΕΜΑΙ|ΙΕΜΑΣΤΕ|ΙΕΤΑΙ|ΙΕΣΑΙ|ΙΕΣΑΣΤΕ|ΙΟΜΑΣΤΑΝ|ΙΟΜΟΥΝ|ΙΟΜΟΥΝΑ|ΙΟΝΤΑΝ|ΙΟΝΤΟΥΣΑΝ|ΙΟΣΑΣΤΑΝ|ΙΟΣΑΣΤΕ|ΙΟΣΟΥΝ|ΙΟΣΟΥΝΑ|ΙΟΤΑΝ|ΙΟΥΜΑ|ΙΟΥΜΑΣΤΕ|ΙΟΥΝΤΑΙ|ΙΟΥΝΤΑΝ|Η|ΗΔΕΣ|ΗΔΩΝ|ΗΘΕΙ|ΗΘΕΙΣ|ΗΘΕΙΤΕ|ΗΘΗΚΑΤΕ|ΗΘΗΚΑΝ|ΗΘΟΥΝ|ΗΘΩ|ΗΚΑΤΕ|ΗΚΑΝ|ΗΣ|ΗΣΑΝ|ΗΣΑΤΕ|ΗΣΕΙ|ΗΣΕΣ|ΗΣΟΥΝ|ΗΣΩ|Ο|ΟΙ|ΟΜΑΙ|ΟΜΑΣΤΑΝ|ΟΜΟΥΝ|ΟΜΟΥΝΑ|ΟΝΤΑΙ|ΟΝΤΑΝ|ΟΝΤΟΥΣΑΝ|ΟΣ|ΟΣΑΣΤΑΝ|ΟΣΑΣΤΕ|ΟΣΟΥΝ|ΟΣΟΥΝΑ|ΟΤΑΝ|ΟΥ|ΟΥΜΑΙ|ΟΥΜΑΣΤΕ|ΟΥΝ|ΟΥΝΤΑΙ|ΟΥΝΤΑΝ|ΟΥΣ|ΟΥΣΑΝ|ΟΥΣΑΤΕ|Υ||ΥΑ|ΥΣ|Ω|ΩΝ|ΟΙΣ)$/.exec(s))&&(s=o[1]),null!=(o=/^(.+?)(ΕΣΤΕΡ|ΕΣΤΑΤ|ΟΤΕΡ|ΟΤΑΤ|ΥΤΕΡ|ΥΤΑΤ|ΩΤΕΡ|ΩΤΑΤ)$/.exec(s))&&(/^(ΕΞ|ΕΣ|ΑΝ|ΚΑΤ|Κ|ΠΡ)$/.test(o[1])||(s=o[1]),/^(ΚΑ|Μ|ΕΛΕ|ΛΕ|ΔΕ)$/.test(o[1])&&(s+="ΥΤ")),s}var l={"ΦΑΓΙΑ":"ΦΑ","ΦΑΓΙΟΥ":"ΦΑ","ΦΑΓΙΩΝ":"ΦΑ","ΣΚΑΓΙΑ":"ΣΚΑ","ΣΚΑΓΙΟΥ":"ΣΚΑ","ΣΚΑΓΙΩΝ":"ΣΚΑ","ΣΟΓΙΟΥ":"ΣΟ","ΣΟΓΙΑ":"ΣΟ","ΣΟΓΙΩΝ":"ΣΟ","ΤΑΤΟΓΙΑ":"ΤΑΤΟ","ΤΑΤΟΓΙΟΥ":"ΤΑΤΟ","ΤΑΤΟΓΙΩΝ":"ΤΑΤΟ","ΚΡΕΑΣ":"ΚΡΕ","ΚΡΕΑΤΟΣ":"ΚΡΕ","ΚΡΕΑΤΑ":"ΚΡΕ","ΚΡΕΑΤΩΝ":"ΚΡΕ","ΠΕΡΑΣ":"ΠΕΡ","ΠΕΡΑΤΟΣ":"ΠΕΡ","ΠΕΡΑΤΑ":"ΠΕΡ","ΠΕΡΑΤΩΝ":"ΠΕΡ","ΤΕΡΑΣ":"ΤΕΡ","ΤΕΡΑΤΟΣ":"ΤΕΡ","ΤΕΡΑΤΑ":"ΤΕΡ","ΤΕΡΑΤΩΝ":"ΤΕΡ","ΦΩΣ":"ΦΩ","ΦΩΤΟΣ":"ΦΩ","ΦΩΤΑ":"ΦΩ","ΦΩΤΩΝ":"ΦΩ","ΚΑΘΕΣΤΩΣ":"ΚΑΘΕΣΤ","ΚΑΘΕΣΤΩΤΟΣ":"ΚΑΘΕΣΤ","ΚΑΘΕΣΤΩΤΑ":"ΚΑΘΕΣΤ","ΚΑΘΕΣΤΩΤΩΝ":"ΚΑΘΕΣΤ","ΓΕΓΟΝΟΣ":"ΓΕΓΟΝ","ΓΕΓΟΝΟΤΟΣ":"ΓΕΓΟΝ","ΓΕΓΟΝΟΤΑ":"ΓΕΓΟΝ","ΓΕΓΟΝΟΤΩΝ":"ΓΕΓΟΝ","ΕΥΑ":"ΕΥ"},i=["ΑΚΡΙΒΩΣ","ΑΛΑ","ΑΛΛΑ","ΑΛΛΙΩΣ","ΑΛΛΟΤΕ","ΑΜΑ","ΑΝΩ","ΑΝΑ","ΑΝΑΜΕΣΑ","ΑΝΑΜΕΤΑΞΥ","ΑΝΕΥ","ΑΝΤΙ","ΑΝΤΙΠΕΡΑ","ΑΝΤΙΟ","ΑΞΑΦΝΑ","ΑΠΟ","ΑΠΟΨΕ","ΑΡΑ","ΑΡΑΓΕ","ΑΥΡΙΟ","ΑΦΟΙ","ΑΦΟΥ","ΑΦΟΤΟΥ","ΒΡΕ","ΓΕΙΑ","ΓΙΑ","ΓΙΑΤΙ","ΓΡΑΜΜΑ","ΔΕΗ","ΔΕΝ","ΔΗΛΑΔΗ","ΔΙΧΩΣ","ΔΥΟ","ΕΑΝ","ΕΓΩ","ΕΔΩ","ΕΔΑ","ΕΙΘΕ","ΕΙΜΑΙ","ΕΙΜΑΣΤΕ","ΕΙΣΑΙ","ΕΙΣΑΣΤΕ","ΕΙΝΑΙ","ΕΙΣΤΕ","ΕΙΤΕ","ΕΚΕΙ","ΕΚΟ","ΕΛΑ","ΕΜΑΣ","ΕΜΕΙΣ","ΕΝΤΕΛΩΣ","ΕΝΤΟΣ","ΕΝΤΩΜΕΤΑΞΥ","ΕΝΩ","ΕΞΙ","ΕΞΙΣΟΥ","ΕΞΗΣ","ΕΞΩ","ΕΟΚ","ΕΠΑΝΩ","ΕΠΕΙΔΗ","ΕΠΕΙΤΑ","ΕΠΙ","ΕΠΙΣΗΣ","ΕΠΟΜΕΝΩΣ","ΕΠΤΑ","ΕΣΑΣ","ΕΣΕΙΣ","ΕΣΤΩ","ΕΣΥ","ΕΣΩ","ΕΤΣΙ","ΕΥΓΕ","ΕΦΕ","ΕΦΕΞΗΣ","ΕΧΤΕΣ","ΕΩΣ","ΗΔΗ","ΗΜΙ","ΗΠΑ","ΗΤΟΙ","ΘΕΣ","ΙΔΙΩΣ","ΙΔΗ","ΙΚΑ","ΙΣΩΣ","ΚΑΘΕ","ΚΑΘΕΤΙ","ΚΑΘΟΛΟΥ","ΚΑΘΩΣ","ΚΑΙ","ΚΑΝ","ΚΑΠΟΤΕ","ΚΑΠΟΥ","ΚΑΤΑ","ΚΑΤΙ","ΚΑΤΟΠΙΝ","ΚΑΤΩ","ΚΕΙ","ΚΙΧ","ΚΚΕ","ΚΟΛΑΝ","ΚΥΡΙΩΣ","ΚΩΣ","ΜΑΚΑΡΙ","ΜΑΛΙΣΤΑ","ΜΑΛΛΟΝ","ΜΑΙ","ΜΑΟ","ΜΑΟΥΣ","ΜΑΣ","ΜΕΘΑΥΡΙΟ","ΜΕΣ","ΜΕΣΑ","ΜΕΤΑ","ΜΕΤΑΞΥ","ΜΕΧΡΙ","ΜΗΔΕ","ΜΗΝ","ΜΗΠΩΣ","ΜΗΤΕ","ΜΙΑ","ΜΙΑΣ","ΜΙΣ","ΜΜΕ","ΜΟΛΟΝΟΤΙ","ΜΟΥ","ΜΠΑ","ΜΠΑΣ","ΜΠΟΥΦΑΝ","ΜΠΡΟΣ","ΝΑΙ","ΝΕΣ","ΝΤΑ","ΝΤΕ","ΞΑΝΑ","ΟΗΕ","ΟΚΤΩ","ΟΜΩΣ","ΟΝΕ","ΟΠΑ","ΟΠΟΥ","ΟΠΩΣ","ΟΣΟ","ΟΤΑΝ","ΟΤΕ","ΟΤΙ","ΟΥΤΕ","ΟΧΙ","ΠΑΛΙ","ΠΑΝ","ΠΑΝΟ","ΠΑΝΤΟΤΕ","ΠΑΝΤΟΥ","ΠΑΝΤΩΣ","ΠΑΝΩ","ΠΑΡΑ","ΠΕΡΑ","ΠΕΡΙ","ΠΕΡΙΠΟΥ","ΠΙΑ","ΠΙΟ","ΠΙΣΩ","ΠΛΑΙ","ΠΛΕΟΝ","ΠΛΗΝ","ΠΟΤΕ","ΠΟΥ","ΠΡΟ","ΠΡΟΣ","ΠΡΟΧΤΕΣ","ΠΡΟΧΘΕΣ","ΡΟΔΙ","ΠΩΣ","ΣΑΙ","ΣΑΣ","ΣΑΝ","ΣΕΙΣ","ΣΙΑ","ΣΚΙ","ΣΟΙ","ΣΟΥ","ΣΡΙ","ΣΥΝ","ΣΥΝΑΜΑ","ΣΧΕΔΟΝ","ΤΑΔΕ","ΤΑΞΙ","ΤΑΧΑ","ΤΕΙ","ΤΗΝ","ΤΗΣ","ΤΙΠΟΤΑ","ΤΙΠΟΤΕ","ΤΙΣ","ΤΟΝ","ΤΟΤΕ","ΤΟΥ","ΤΟΥΣ","ΤΣΑ","ΤΣΕ","ΤΣΙ","ΤΣΟΥ","ΤΩΝ","ΥΠΟ","ΥΠΟΨΗ","ΥΠΟΨΙΝ","ΥΣΤΕΡΑ","ΦΕΤΟΣ","ΦΙΣ","ΦΠΑ","ΧΑΦ","ΧΘΕΣ","ΧΤΕΣ","ΧΩΡΙΣ","ΩΣ","ΩΣΑΝ","ΩΣΟΤΟΥ","ΩΣΠΟΥ","ΩΣΤΕ","ΩΣΤΟΣΟ"],s=new RegExp("^[ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ]+$");return function(e){return"function"==typeof e.update?e.update(function(e){return n(e.toUpperCase()).toLowerCase()}):n(e.toUpperCase()).toLowerCase()}}(),e.Pipeline.registerFunction(e.el.stemmer,"stemmer-el"),e.el.stopWordFilter=e.generateStopWordFilter("αλλα αν αντι απο αυτα αυτεσ αυτη αυτο αυτοι αυτοσ αυτουσ αυτων για δε δεν εαν ειμαι ειμαστε ειναι εισαι ειστε εκεινα εκεινεσ εκεινη εκεινο εκεινοι εκεινοσ εκεινουσ εκεινων ενω επι η θα ισωσ κ και κατα κι μα με μετα μη μην να ο οι ομωσ οπωσ οσο οτι παρα ποια ποιεσ ποιο ποιοι ποιοσ ποιουσ ποιων που προσ πωσ σε στη στην στο στον τα την τησ το τον τοτε του των ωσ".split(" ")),e.Pipeline.registerFunction(e.el.stopWordFilter,"stopWordFilter-el"),e.el.normilizer=function(){var e={"Ά":"Α","ά":"α","Έ":"Ε","έ":"ε","Ή":"Η","ή":"η","Ί":"Ι","ί":"ι","Ό":"Ο","ο":"ο","Ύ":"Υ","ύ":"υ","Ώ":"Ω","ώ":"ω","Ϊ":"Ι","ϊ":"ι","Ϋ":"Υ","ϋ":"υ","ΐ":"ι","ΰ":"υ"};return function(t){if("function"==typeof t.update)return t.update(function(t){for(var r="",n=0;n');--md-typeset-table-sort-icon--asc:url('data:image/svg+xml;charset=utf-8,');--md-typeset-table-sort-icon--desc:url('data:image/svg+xml;charset=utf-8,')}.md-typeset{-webkit-print-color-adjust:exact;color-adjust:exact;font-size:.8rem;line-height:1.6}@media print{.md-typeset{font-size:.68rem}}.md-typeset blockquote,.md-typeset dl,.md-typeset figure,.md-typeset ol,.md-typeset pre,.md-typeset ul{margin-bottom:1em;margin-top:1em}.md-typeset h1{color:var(--md-default-fg-color--light);font-size:2em;line-height:1.3;margin:0 0 1.25em}.md-typeset h1,.md-typeset h2{font-weight:300;letter-spacing:-.01em}.md-typeset h2{font-size:1.5625em;line-height:1.4;margin:1.6em 0 .64em}.md-typeset h3{font-size:1.25em;font-weight:400;letter-spacing:-.01em;line-height:1.5;margin:1.6em 0 .8em}.md-typeset h2+h3{margin-top:.8em}.md-typeset h4{font-weight:700;letter-spacing:-.01em;margin:1em 0}.md-typeset h5,.md-typeset h6{color:var(--md-default-fg-color--light);font-size:.8em;font-weight:700;letter-spacing:-.01em;margin:1.25em 0}.md-typeset h5{text-transform:uppercase}.md-typeset hr{border-bottom:.05rem solid var(--md-default-fg-color--lightest);display:flow-root;margin:1.5em 0}.md-typeset a{color:var(--md-typeset-a-color);word-break:break-word}.md-typeset a,.md-typeset a:before{transition:color 125ms}.md-typeset a:focus,.md-typeset a:hover{color:var(--md-accent-fg-color)}.md-typeset a:focus code,.md-typeset a:hover code{background-color:var(--md-accent-fg-color--transparent)}.md-typeset a code{color:currentcolor;transition:background-color 125ms}.md-typeset a.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-typeset code,.md-typeset kbd,.md-typeset pre{color:var(--md-code-fg-color);direction:ltr;font-variant-ligatures:none}@media print{.md-typeset code,.md-typeset kbd,.md-typeset pre{white-space:pre-wrap}}.md-typeset code{background-color:var(--md-code-bg-color);border-radius:.1rem;-webkit-box-decoration-break:clone;box-decoration-break:clone;font-size:.85em;padding:0 .2941176471em;word-break:break-word}.md-typeset code:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}.md-typeset pre{display:flow-root;line-height:1.4;position:relative}.md-typeset pre>code{-webkit-box-decoration-break:slice;box-decoration-break:slice;box-shadow:none;display:block;margin:0;outline-color:var(--md-accent-fg-color);overflow:auto;padding:.7720588235em 1.1764705882em;scrollbar-color:var(--md-default-fg-color--lighter) #0000;scrollbar-width:thin;touch-action:auto;word-break:normal}.md-typeset pre>code:hover{scrollbar-color:var(--md-accent-fg-color) #0000}.md-typeset pre>code::-webkit-scrollbar{height:.2rem;width:.2rem}.md-typeset pre>code::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-typeset pre>code::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}.md-typeset kbd{background-color:var(--md-typeset-kbd-color);border-radius:.1rem;box-shadow:0 .1rem 0 .05rem var(--md-typeset-kbd-border-color),0 .1rem 0 var(--md-typeset-kbd-border-color),0 -.1rem .2rem var(--md-typeset-kbd-accent-color) inset;color:var(--md-default-fg-color);display:inline-block;font-size:.75em;padding:0 .6666666667em;vertical-align:text-top;word-break:break-word}.md-typeset mark{background-color:var(--md-typeset-mark-color);-webkit-box-decoration-break:clone;box-decoration-break:clone;color:inherit;word-break:break-word}.md-typeset abbr{border-bottom:.05rem dotted var(--md-default-fg-color--light);cursor:help;text-decoration:none}@media (hover:none){.md-typeset abbr[title]:focus:after,.md-typeset abbr[title]:hover:after{background-color:var(--md-default-fg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z3);color:var(--md-default-bg-color);content:attr(title);font-size:.7rem;left:.8rem;margin-top:2em;padding:.2rem .3rem;position:absolute;right:.8rem}}.md-typeset small{opacity:.75}[dir=ltr] .md-typeset sub,[dir=ltr] .md-typeset sup{margin-left:.078125em}[dir=rtl] .md-typeset sub,[dir=rtl] .md-typeset sup{margin-right:.078125em}[dir=ltr] .md-typeset blockquote{padding-left:.6rem}[dir=rtl] .md-typeset blockquote{padding-right:.6rem}[dir=ltr] .md-typeset blockquote{border-left:.2rem solid var(--md-default-fg-color--lighter)}[dir=rtl] .md-typeset blockquote{border-right:.2rem solid var(--md-default-fg-color--lighter)}.md-typeset blockquote{color:var(--md-default-fg-color--light);margin-left:0;margin-right:0}.md-typeset ul{list-style-type:disc}[dir=ltr] .md-typeset ol,[dir=ltr] .md-typeset ul{margin-left:.625em}[dir=rtl] .md-typeset ol,[dir=rtl] .md-typeset ul{margin-right:.625em}.md-typeset ol,.md-typeset ul{padding:0}.md-typeset ol:not([hidden]),.md-typeset ul:not([hidden]){display:flow-root}.md-typeset ol ol,.md-typeset ul ol{list-style-type:lower-alpha}.md-typeset ol ol ol,.md-typeset ul ol ol{list-style-type:lower-roman}[dir=ltr] .md-typeset ol li,[dir=ltr] .md-typeset ul li{margin-left:1.25em}[dir=rtl] .md-typeset ol li,[dir=rtl] .md-typeset ul li{margin-right:1.25em}.md-typeset ol li,.md-typeset ul li{margin-bottom:.5em}.md-typeset ol li blockquote,.md-typeset ol li p,.md-typeset ul li blockquote,.md-typeset ul li p{margin:.5em 0}.md-typeset ol li:last-child,.md-typeset ul li:last-child{margin-bottom:0}[dir=ltr] .md-typeset ol li ol,[dir=ltr] .md-typeset ol li ul,[dir=ltr] .md-typeset ul li ol,[dir=ltr] .md-typeset ul li ul{margin-left:.625em}[dir=rtl] .md-typeset ol li ol,[dir=rtl] .md-typeset ol li ul,[dir=rtl] .md-typeset ul li ol,[dir=rtl] .md-typeset ul li ul{margin-right:.625em}.md-typeset ol li ol,.md-typeset ol li ul,.md-typeset ul li ol,.md-typeset ul li ul{margin-bottom:.5em;margin-top:.5em}[dir=ltr] .md-typeset dd{margin-left:1.875em}[dir=rtl] .md-typeset dd{margin-right:1.875em}.md-typeset dd{margin-bottom:1.5em;margin-top:1em}.md-typeset img,.md-typeset svg,.md-typeset video{height:auto;max-width:100%}.md-typeset img[align=left]{margin:1em 1em 1em 0}.md-typeset img[align=right]{margin:1em 0 1em 1em}.md-typeset img[align]:only-child{margin-top:0}.md-typeset figure{display:flow-root;margin:1em auto;max-width:100%;text-align:center;width:-webkit-fit-content;width:-moz-fit-content;width:fit-content}.md-typeset figure img{display:block}.md-typeset figcaption{font-style:italic;margin:1em auto;max-width:24rem}.md-typeset iframe{max-width:100%}.md-typeset table:not([class]){background-color:var(--md-default-bg-color);border:.05rem solid var(--md-typeset-table-color);border-radius:.1rem;display:inline-block;font-size:.64rem;max-width:100%;overflow:auto;touch-action:auto}@media print{.md-typeset table:not([class]){display:table}}.md-typeset table:not([class])+*{margin-top:1.5em}.md-typeset table:not([class]) td>:first-child,.md-typeset table:not([class]) th>:first-child{margin-top:0}.md-typeset table:not([class]) td>:last-child,.md-typeset table:not([class]) th>:last-child{margin-bottom:0}.md-typeset table:not([class]) td:not([align]),.md-typeset table:not([class]) th:not([align]){text-align:left}[dir=rtl] .md-typeset table:not([class]) td:not([align]),[dir=rtl] .md-typeset table:not([class]) th:not([align]){text-align:right}.md-typeset table:not([class]) th{font-weight:700;min-width:5rem;padding:.9375em 1.25em;vertical-align:top}.md-typeset table:not([class]) td{border-top:.05rem solid var(--md-typeset-table-color);padding:.9375em 1.25em;vertical-align:top}.md-typeset table:not([class]) tbody tr{transition:background-color 125ms}.md-typeset table:not([class]) tbody tr:hover{background-color:var(--md-typeset-table-color--light);box-shadow:0 .05rem 0 var(--md-default-bg-color) inset}.md-typeset table:not([class]) a{word-break:normal}.md-typeset table th[role=columnheader]{cursor:pointer}[dir=ltr] .md-typeset table th[role=columnheader]:after{margin-left:.5em}[dir=rtl] .md-typeset table th[role=columnheader]:after{margin-right:.5em}.md-typeset table th[role=columnheader]:after{content:"";display:inline-block;height:1.2em;-webkit-mask-image:var(--md-typeset-table-sort-icon);mask-image:var(--md-typeset-table-sort-icon);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color 125ms;vertical-align:text-bottom;width:1.2em}.md-typeset table th[role=columnheader]:hover:after{background-color:var(--md-default-fg-color--lighter)}.md-typeset table th[role=columnheader][aria-sort=ascending]:after{background-color:var(--md-default-fg-color--light);-webkit-mask-image:var(--md-typeset-table-sort-icon--asc);mask-image:var(--md-typeset-table-sort-icon--asc)}.md-typeset table th[role=columnheader][aria-sort=descending]:after{background-color:var(--md-default-fg-color--light);-webkit-mask-image:var(--md-typeset-table-sort-icon--desc);mask-image:var(--md-typeset-table-sort-icon--desc)}.md-typeset__scrollwrap{margin:1em -.8rem;overflow-x:auto;touch-action:auto}.md-typeset__table{display:inline-block;margin-bottom:.5em;padding:0 .8rem}@media print{.md-typeset__table{display:block}}html .md-typeset__table table{display:table;margin:0;overflow:hidden;width:100%}@media screen and (max-width:44.984375em){.md-content__inner>pre{margin:1em -.8rem}.md-content__inner>pre code{border-radius:0}}.md-typeset .md-author{display:block;flex-shrink:0;height:1.6rem;overflow:hidden;position:relative;transition:color 125ms,transform 125ms;width:1.6rem}.md-typeset .md-author img{border-radius:100%;display:block}.md-typeset .md-author--more{background:var(--md-default-fg-color--lightest);color:var(--md-default-fg-color--lighter);font-size:.6rem;font-weight:700;line-height:1.6rem;text-align:center}.md-typeset .md-author--long{height:2.4rem;width:2.4rem}.md-typeset a.md-author{transform:scale(1)}.md-typeset a.md-author img{filter:grayscale(100%) opacity(75%);transition:filter 125ms}.md-typeset a.md-author:focus,.md-typeset a.md-author:hover{transform:scale(1.1);z-index:1}.md-typeset a.md-author:focus img,.md-typeset a.md-author:hover img{filter:grayscale(0)}.md-banner{background-color:var(--md-footer-bg-color);color:var(--md-footer-fg-color);overflow:auto}@media print{.md-banner{display:none}}.md-banner--warning{background-color:var(--md-warning-bg-color);color:var(--md-warning-fg-color)}.md-banner__inner{font-size:.7rem;margin:.6rem auto;padding:0 .8rem}[dir=ltr] .md-banner__button{float:right}[dir=rtl] .md-banner__button{float:left}.md-banner__button{color:inherit;cursor:pointer;transition:opacity .25s}.no-js .md-banner__button{display:none}.md-banner__button:hover{opacity:.7}html{font-size:125%;height:100%;overflow-x:hidden}@media screen and (min-width:100em){html{font-size:137.5%}}@media screen and (min-width:125em){html{font-size:150%}}body{background-color:var(--md-default-bg-color);display:flex;flex-direction:column;font-size:.5rem;min-height:100%;position:relative;width:100%}@media print{body{display:block}}@media screen and (max-width:59.984375em){body[data-md-scrolllock]{position:fixed}}.md-grid{margin-left:auto;margin-right:auto;max-width:61rem}.md-container{display:flex;flex-direction:column;flex-grow:1}@media print{.md-container{display:block}}.md-main{flex-grow:1}.md-main__inner{display:flex;height:100%;margin-top:1.5rem}.md-ellipsis{overflow:hidden;text-overflow:ellipsis}.md-toggle{display:none}.md-option{height:0;opacity:0;position:absolute;width:0}.md-option:checked+label:not([hidden]){display:block}.md-option.focus-visible+label{outline-color:var(--md-accent-fg-color);outline-style:auto}.md-skip{background-color:var(--md-default-fg-color);border-radius:.1rem;color:var(--md-default-bg-color);font-size:.64rem;margin:.5rem;opacity:0;outline-color:var(--md-accent-fg-color);padding:.3rem .5rem;position:fixed;transform:translateY(.4rem);z-index:-1}.md-skip:focus{opacity:1;transform:translateY(0);transition:transform .25s cubic-bezier(.4,0,.2,1),opacity 175ms 75ms;z-index:10}@page{margin:25mm}:root{--md-clipboard-icon:url('data:image/svg+xml;charset=utf-8,')}.md-clipboard{border-radius:.1rem;color:var(--md-default-fg-color--lightest);cursor:pointer;height:1.5em;outline-color:var(--md-accent-fg-color);outline-offset:.1rem;position:absolute;right:.5em;top:.5em;transition:color .25s;width:1.5em;z-index:1}@media print{.md-clipboard{display:none}}.md-clipboard:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}:hover>.md-clipboard{color:var(--md-default-fg-color--light)}.md-clipboard:focus,.md-clipboard:hover{color:var(--md-accent-fg-color)}.md-clipboard:after{background-color:currentcolor;content:"";display:block;height:1.125em;margin:0 auto;-webkit-mask-image:var(--md-clipboard-icon);mask-image:var(--md-clipboard-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:1.125em}.md-clipboard--inline{cursor:pointer}.md-clipboard--inline code{transition:color .25s,background-color .25s}.md-clipboard--inline:focus code,.md-clipboard--inline:hover code{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}@keyframes consent{0%{opacity:0;transform:translateY(100%)}to{opacity:1;transform:translateY(0)}}@keyframes overlay{0%{opacity:0}to{opacity:1}}.md-consent__overlay{animation:overlay .25s both;-webkit-backdrop-filter:blur(.1rem);backdrop-filter:blur(.1rem);background-color:#0000008a;height:100%;opacity:1;position:fixed;top:0;width:100%;z-index:5}.md-consent__inner{animation:consent .5s cubic-bezier(.1,.7,.1,1) both;background-color:var(--md-default-bg-color);border:0;border-radius:.1rem;bottom:0;box-shadow:0 0 .2rem #0000001a,0 .2rem .4rem #0003;max-height:100%;overflow:auto;padding:0;position:fixed;width:100%;z-index:5}.md-consent__form{padding:.8rem}.md-consent__settings{display:none;margin:1em 0}input:checked+.md-consent__settings{display:block}.md-consent__controls{margin-bottom:.8rem}.md-typeset .md-consent__controls .md-button{display:inline}@media screen and (max-width:44.984375em){.md-typeset .md-consent__controls .md-button{display:block;margin-top:.4rem;text-align:center;width:100%}}.md-consent label{cursor:pointer}.md-content{flex-grow:1;min-width:0}.md-content__inner{margin:0 .8rem 1.2rem;padding-top:.6rem}@media screen and (min-width:76.25em){[dir=ltr] .md-sidebar--primary:not([hidden])~.md-content>.md-content__inner{margin-left:1.2rem}[dir=ltr] .md-sidebar--secondary:not([hidden])~.md-content>.md-content__inner,[dir=rtl] .md-sidebar--primary:not([hidden])~.md-content>.md-content__inner{margin-right:1.2rem}[dir=rtl] .md-sidebar--secondary:not([hidden])~.md-content>.md-content__inner{margin-left:1.2rem}}.md-content__inner:before{content:"";display:block;height:.4rem}.md-content__inner>:last-child{margin-bottom:0}[dir=ltr] .md-content__button{float:right}[dir=rtl] .md-content__button{float:left}[dir=ltr] .md-content__button{margin-left:.4rem}[dir=rtl] .md-content__button{margin-right:.4rem}.md-content__button{margin:.4rem 0;padding:0}@media print{.md-content__button{display:none}}.md-typeset .md-content__button{color:var(--md-default-fg-color--lighter)}.md-content__button svg{display:inline;vertical-align:top}[dir=rtl] .md-content__button svg{transform:scaleX(-1)}[dir=ltr] .md-dialog{right:.8rem}[dir=rtl] .md-dialog{left:.8rem}.md-dialog{background-color:var(--md-default-fg-color);border-radius:.1rem;bottom:.8rem;box-shadow:var(--md-shadow-z3);min-width:11.1rem;opacity:0;padding:.4rem .6rem;pointer-events:none;position:fixed;transform:translateY(100%);transition:transform 0ms .4s,opacity .4s;z-index:4}@media print{.md-dialog{display:none}}.md-dialog--active{opacity:1;pointer-events:auto;transform:translateY(0);transition:transform .4s cubic-bezier(.075,.85,.175,1),opacity .4s}.md-dialog__inner{color:var(--md-default-bg-color);font-size:.7rem}.md-feedback{margin:2em 0 1em;text-align:center}.md-feedback fieldset{border:none;margin:0;padding:0}.md-feedback__title{font-weight:700;margin:1em auto}.md-feedback__inner{position:relative}.md-feedback__list{align-content:baseline;display:flex;flex-wrap:wrap;justify-content:center;position:relative}.md-feedback__list:hover .md-icon:not(:disabled){color:var(--md-default-fg-color--lighter)}:disabled .md-feedback__list{min-height:1.8rem}.md-feedback__icon{color:var(--md-default-fg-color--light);cursor:pointer;flex-shrink:0;margin:0 .1rem;transition:color 125ms}.md-feedback__icon:not(:disabled).md-icon:hover{color:var(--md-accent-fg-color)}.md-feedback__icon:disabled{color:var(--md-default-fg-color--lightest);pointer-events:none}.md-feedback__note{opacity:0;position:relative;transform:translateY(.4rem);transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .15s}.md-feedback__note>*{margin:0 auto;max-width:16rem}:disabled .md-feedback__note{opacity:1;transform:translateY(0)}.md-footer{background-color:var(--md-footer-bg-color);color:var(--md-footer-fg-color)}@media print{.md-footer{display:none}}.md-footer__inner{justify-content:space-between;overflow:auto;padding:.2rem}.md-footer__inner:not([hidden]){display:flex}.md-footer__link{align-items:end;display:flex;flex-grow:0.01;margin-bottom:.4rem;margin-top:1rem;max-width:100%;outline-color:var(--md-accent-fg-color);overflow:hidden;transition:opacity .25s}.md-footer__link:focus,.md-footer__link:hover{opacity:.7}[dir=rtl] .md-footer__link svg{transform:scaleX(-1)}@media screen and (max-width:44.984375em){.md-footer__link--prev{flex-shrink:0}.md-footer__link--prev .md-footer__title{display:none}}[dir=ltr] .md-footer__link--next{margin-left:auto}[dir=rtl] .md-footer__link--next{margin-right:auto}.md-footer__link--next{text-align:right}[dir=rtl] .md-footer__link--next{text-align:left}.md-footer__title{flex-grow:1;font-size:.9rem;margin-bottom:.7rem;max-width:calc(100% - 2.4rem);padding:0 1rem;white-space:nowrap}.md-footer__button{margin:.2rem;padding:.4rem}.md-footer__direction{font-size:.64rem;opacity:.7}.md-footer-meta{background-color:var(--md-footer-bg-color--dark)}.md-footer-meta__inner{display:flex;flex-wrap:wrap;justify-content:space-between;padding:.2rem}html .md-footer-meta.md-typeset a{color:var(--md-footer-fg-color--light)}html .md-footer-meta.md-typeset a:focus,html .md-footer-meta.md-typeset a:hover{color:var(--md-footer-fg-color)}.md-copyright{color:var(--md-footer-fg-color--lighter);font-size:.64rem;margin:auto .6rem;padding:.4rem 0;width:100%}@media screen and (min-width:45em){.md-copyright{width:auto}}.md-copyright__highlight{color:var(--md-footer-fg-color--light)}.md-social{display:inline-flex;gap:.2rem;margin:0 .4rem;padding:.2rem 0 .6rem}@media screen and (min-width:45em){.md-social{padding:.6rem 0}}.md-social__link{display:inline-block;height:1.6rem;text-align:center;width:1.6rem}.md-social__link:before{line-height:1.9}.md-social__link svg{fill:currentcolor;max-height:.8rem;vertical-align:-25%}.md-typeset .md-button{border:.1rem solid;border-radius:.1rem;color:var(--md-primary-fg-color);cursor:pointer;display:inline-block;font-weight:700;padding:.625em 2em;transition:color 125ms,background-color 125ms,border-color 125ms}.md-typeset .md-button--primary{background-color:var(--md-primary-fg-color);border-color:var(--md-primary-fg-color);color:var(--md-primary-bg-color)}.md-typeset .md-button:focus,.md-typeset .md-button:hover{background-color:var(--md-accent-fg-color);border-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}[dir=ltr] .md-typeset .md-input{border-top-left-radius:.1rem}[dir=ltr] .md-typeset .md-input,[dir=rtl] .md-typeset .md-input{border-top-right-radius:.1rem}[dir=rtl] .md-typeset .md-input{border-top-left-radius:.1rem}.md-typeset .md-input{border-bottom:.1rem solid var(--md-default-fg-color--lighter);box-shadow:var(--md-shadow-z1);font-size:.8rem;height:1.8rem;padding:0 .6rem;transition:border .25s,box-shadow .25s}.md-typeset .md-input:focus,.md-typeset .md-input:hover{border-bottom-color:var(--md-accent-fg-color);box-shadow:var(--md-shadow-z2)}.md-typeset .md-input--stretch{width:100%}.md-header{background-color:var(--md-primary-fg-color);box-shadow:0 0 .2rem #0000,0 .2rem .4rem #0000;color:var(--md-primary-bg-color);display:block;left:0;position:sticky;right:0;top:0;z-index:4}@media print{.md-header{display:none}}.md-header[hidden]{transform:translateY(-100%);transition:transform .25s cubic-bezier(.8,0,.6,1),box-shadow .25s}.md-header--shadow{box-shadow:0 0 .2rem #0000001a,0 .2rem .4rem #0003;transition:transform .25s cubic-bezier(.1,.7,.1,1),box-shadow .25s}.md-header__inner{align-items:center;display:flex;padding:0 .2rem}.md-header__button{color:currentcolor;cursor:pointer;margin:.2rem;outline-color:var(--md-accent-fg-color);padding:.4rem;position:relative;transition:opacity .25s;vertical-align:middle;z-index:1}.md-header__button:hover{opacity:.7}.md-header__button:not([hidden]){display:inline-block}.md-header__button:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}.md-header__button.md-logo{margin:.2rem;padding:.4rem}@media screen and (max-width:76.234375em){.md-header__button.md-logo{display:none}}.md-header__button.md-logo img,.md-header__button.md-logo svg{fill:currentcolor;display:block;height:1.2rem;width:auto}@media screen and (min-width:60em){.md-header__button[for=__search]{display:none}}.no-js .md-header__button[for=__search]{display:none}[dir=rtl] .md-header__button[for=__search] svg{transform:scaleX(-1)}@media screen and (min-width:76.25em){.md-header__button[for=__drawer]{display:none}}.md-header__topic{display:flex;max-width:100%;position:absolute;transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .15s;white-space:nowrap}.md-header__topic+.md-header__topic{opacity:0;pointer-events:none;transform:translateX(1.25rem);transition:transform .4s cubic-bezier(1,.7,.1,.1),opacity .15s;z-index:-1}[dir=rtl] .md-header__topic+.md-header__topic{transform:translateX(-1.25rem)}.md-header__topic:first-child{font-weight:700}[dir=ltr] .md-header__title{margin-left:1rem}[dir=rtl] .md-header__title{margin-right:1rem}[dir=ltr] .md-header__title{margin-right:.4rem}[dir=rtl] .md-header__title{margin-left:.4rem}.md-header__title{flex-grow:1;font-size:.9rem;height:2.4rem;line-height:2.4rem}.md-header__title--active .md-header__topic{opacity:0;pointer-events:none;transform:translateX(-1.25rem);transition:transform .4s cubic-bezier(1,.7,.1,.1),opacity .15s;z-index:-1}[dir=rtl] .md-header__title--active .md-header__topic{transform:translateX(1.25rem)}.md-header__title--active .md-header__topic+.md-header__topic{opacity:1;pointer-events:auto;transform:translateX(0);transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .15s;z-index:0}.md-header__title>.md-header__ellipsis{height:100%;position:relative;width:100%}.md-header__option{display:flex;flex-shrink:0;max-width:100%;transition:max-width 0ms .25s,opacity .25s .25s;white-space:nowrap}[data-md-toggle=search]:checked~.md-header .md-header__option{max-width:0;opacity:0;transition:max-width 0ms,opacity 0ms}.md-header__option>input{bottom:0}.md-header__source{display:none}@media screen and (min-width:60em){[dir=ltr] .md-header__source{margin-left:1rem}[dir=rtl] .md-header__source{margin-right:1rem}.md-header__source{display:block;max-width:11.7rem;width:11.7rem}}@media screen and (min-width:76.25em){[dir=ltr] .md-header__source{margin-left:1.4rem}[dir=rtl] .md-header__source{margin-right:1.4rem}}.md-meta{color:var(--md-default-fg-color--light);font-size:.7rem;line-height:1.3}.md-meta__list{display:inline-flex;flex-wrap:wrap;list-style:none;margin:0;padding:0}.md-meta__item:not(:last-child):after{content:"·";margin-left:.2rem;margin-right:.2rem}.md-meta__link{color:var(--md-typeset-a-color)}.md-meta__link:focus,.md-meta__link:hover{color:var(--md-accent-fg-color)}.md-draft{background-color:#ff1744;border-radius:.125em;color:#fff;display:inline-block;font-weight:700;padding-left:.5714285714em;padding-right:.5714285714em}:root{--md-nav-icon--prev:url('data:image/svg+xml;charset=utf-8,');--md-nav-icon--next:url('data:image/svg+xml;charset=utf-8,');--md-toc-icon:url('data:image/svg+xml;charset=utf-8,')}.md-nav{font-size:.7rem;line-height:1.3}.md-nav__title{color:var(--md-default-fg-color--light);display:block;font-weight:700;overflow:hidden;padding:0 .6rem;text-overflow:ellipsis}.md-nav__title .md-nav__button{display:none}.md-nav__title .md-nav__button img{height:100%;width:auto}.md-nav__title .md-nav__button.md-logo img,.md-nav__title .md-nav__button.md-logo svg{fill:currentcolor;display:block;height:2.4rem;max-width:100%;object-fit:contain;width:auto}.md-nav__list{list-style:none;margin:0;padding:0}.md-nav__link{align-items:flex-start;display:flex;gap:.4rem;margin-top:.625em;scroll-snap-align:start;transition:color 125ms}.md-nav__link--passed{color:var(--md-default-fg-color--light)}.md-nav__item .md-nav__link--active,.md-nav__item .md-nav__link--active code{color:var(--md-typeset-a-color)}.md-nav__link .md-ellipsis{position:relative}[dir=ltr] .md-nav__link .md-icon:last-child{margin-left:auto}[dir=rtl] .md-nav__link .md-icon:last-child{margin-right:auto}.md-nav__link svg{fill:currentcolor;flex-shrink:0;height:1.3em}.md-nav__link[for]:focus,.md-nav__link[for]:hover,.md-nav__link[href]:focus,.md-nav__link[href]:hover{color:var(--md-accent-fg-color);cursor:pointer}.md-nav__link.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-nav--primary .md-nav__link[for=__toc]{display:none}.md-nav--primary .md-nav__link[for=__toc] .md-icon:after{background-color:currentcolor;display:block;height:100%;-webkit-mask-image:var(--md-toc-icon);mask-image:var(--md-toc-icon);width:100%}.md-nav--primary .md-nav__link[for=__toc]~.md-nav{display:none}.md-nav__container>.md-nav__link{margin-top:0}.md-nav__container>.md-nav__link:first-child{flex-grow:1;min-width:0}.md-nav__icon{flex-shrink:0}.md-nav__source{display:none}@media screen and (max-width:76.234375em){.md-nav--primary,.md-nav--primary .md-nav{background-color:var(--md-default-bg-color);display:flex;flex-direction:column;height:100%;left:0;position:absolute;right:0;top:0;z-index:1}.md-nav--primary .md-nav__item,.md-nav--primary .md-nav__title{font-size:.8rem;line-height:1.5}.md-nav--primary .md-nav__title{background-color:var(--md-default-fg-color--lightest);color:var(--md-default-fg-color--light);cursor:pointer;height:5.6rem;line-height:2.4rem;padding:3rem .8rem .2rem;position:relative;white-space:nowrap}[dir=ltr] .md-nav--primary .md-nav__title .md-nav__icon{left:.4rem}[dir=rtl] .md-nav--primary .md-nav__title .md-nav__icon{right:.4rem}.md-nav--primary .md-nav__title .md-nav__icon{display:block;height:1.2rem;margin:.2rem;position:absolute;top:.4rem;width:1.2rem}.md-nav--primary .md-nav__title .md-nav__icon:after{background-color:currentcolor;content:"";display:block;height:100%;-webkit-mask-image:var(--md-nav-icon--prev);mask-image:var(--md-nav-icon--prev);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:100%}.md-nav--primary .md-nav__title~.md-nav__list{background-color:var(--md-default-bg-color);box-shadow:0 .05rem 0 var(--md-default-fg-color--lightest) inset;overflow-y:auto;scroll-snap-type:y mandatory;touch-action:pan-y}.md-nav--primary .md-nav__title~.md-nav__list>:first-child{border-top:0}.md-nav--primary .md-nav__title[for=__drawer]{background-color:var(--md-primary-fg-color);color:var(--md-primary-bg-color);font-weight:700}.md-nav--primary .md-nav__title .md-logo{display:block;left:.2rem;margin:.2rem;padding:.4rem;position:absolute;right:.2rem;top:.2rem}.md-nav--primary .md-nav__list{flex:1}.md-nav--primary .md-nav__item{border-top:.05rem solid var(--md-default-fg-color--lightest)}.md-nav--primary .md-nav__item--active>.md-nav__link{color:var(--md-typeset-a-color)}.md-nav--primary .md-nav__item--active>.md-nav__link:focus,.md-nav--primary .md-nav__item--active>.md-nav__link:hover{color:var(--md-accent-fg-color)}.md-nav--primary .md-nav__link{margin-top:0;padding:.6rem .8rem}.md-nav--primary .md-nav__link svg{margin-top:.1em}.md-nav--primary .md-nav__link>.md-nav__link{padding:0}[dir=ltr] .md-nav--primary .md-nav__link .md-nav__icon{margin-right:-.2rem}[dir=rtl] .md-nav--primary .md-nav__link .md-nav__icon{margin-left:-.2rem}.md-nav--primary .md-nav__link .md-nav__icon{font-size:1.2rem;height:1.2rem;width:1.2rem}.md-nav--primary .md-nav__link .md-nav__icon:after{background-color:currentcolor;content:"";display:block;height:100%;-webkit-mask-image:var(--md-nav-icon--next);mask-image:var(--md-nav-icon--next);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:100%}[dir=rtl] .md-nav--primary .md-nav__icon:after{transform:scale(-1)}.md-nav--primary .md-nav--secondary .md-nav{background-color:initial;position:static}[dir=ltr] .md-nav--primary .md-nav--secondary .md-nav .md-nav__link{padding-left:1.4rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav__link{padding-right:1.4rem}[dir=ltr] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav__link{padding-left:2rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav__link{padding-right:2rem}[dir=ltr] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav__link{padding-left:2.6rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav__link{padding-right:2.6rem}[dir=ltr] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav .md-nav__link{padding-left:3.2rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav .md-nav__link{padding-right:3.2rem}.md-nav--secondary{background-color:initial}.md-nav__toggle~.md-nav{display:flex;opacity:0;transform:translateX(100%);transition:transform .25s cubic-bezier(.8,0,.6,1),opacity 125ms 50ms}[dir=rtl] .md-nav__toggle~.md-nav{transform:translateX(-100%)}.md-nav__toggle:checked~.md-nav{opacity:1;transform:translateX(0);transition:transform .25s cubic-bezier(.4,0,.2,1),opacity 125ms 125ms}.md-nav__toggle:checked~.md-nav>.md-nav__list{-webkit-backface-visibility:hidden;backface-visibility:hidden}}@media screen and (max-width:59.984375em){.md-nav--primary .md-nav__link[for=__toc]{display:flex}.md-nav--primary .md-nav__link[for=__toc] .md-icon:after{content:""}.md-nav--primary .md-nav__link[for=__toc]+.md-nav__link{display:none}.md-nav--primary .md-nav__link[for=__toc]~.md-nav{display:flex}.md-nav__source{background-color:var(--md-primary-fg-color--dark);color:var(--md-primary-bg-color);display:block;padding:0 .2rem}}@media screen and (min-width:60em) and (max-width:76.234375em){.md-nav--integrated .md-nav__link[for=__toc]{display:flex}.md-nav--integrated .md-nav__link[for=__toc] .md-icon:after{content:""}.md-nav--integrated .md-nav__link[for=__toc]+.md-nav__link{display:none}.md-nav--integrated .md-nav__link[for=__toc]~.md-nav{display:flex}}@media screen and (min-width:60em){.md-nav{margin-bottom:-.4rem}.md-nav--secondary .md-nav__title{background:var(--md-default-bg-color);box-shadow:0 0 .4rem .4rem var(--md-default-bg-color);position:sticky;top:0;z-index:1}.md-nav--secondary .md-nav__title[for=__toc]{scroll-snap-align:start}.md-nav--secondary .md-nav__title .md-nav__icon{display:none}[dir=ltr] .md-nav--secondary .md-nav__list{padding-left:.6rem}[dir=rtl] .md-nav--secondary .md-nav__list{padding-right:.6rem}.md-nav--secondary .md-nav__list{padding-bottom:.4rem}[dir=ltr] .md-nav--secondary .md-nav__item>.md-nav__link{margin-right:.4rem}[dir=rtl] .md-nav--secondary .md-nav__item>.md-nav__link{margin-left:.4rem}}@media screen and (min-width:76.25em){.md-nav{margin-bottom:-.4rem;transition:max-height .25s cubic-bezier(.86,0,.07,1)}.md-nav--primary .md-nav__title{background:var(--md-default-bg-color);box-shadow:0 0 .4rem .4rem var(--md-default-bg-color);position:sticky;top:0;z-index:1}.md-nav--primary .md-nav__title[for=__drawer]{scroll-snap-align:start}.md-nav--primary .md-nav__title .md-nav__icon{display:none}[dir=ltr] .md-nav--primary .md-nav__list{padding-left:.6rem}[dir=rtl] .md-nav--primary .md-nav__list{padding-right:.6rem}.md-nav--primary .md-nav__list{padding-bottom:.4rem}[dir=ltr] .md-nav--primary .md-nav__item>.md-nav__link{margin-right:.4rem}[dir=rtl] .md-nav--primary .md-nav__item>.md-nav__link{margin-left:.4rem}.md-nav__toggle~.md-nav{display:grid;grid-template-rows:0fr;opacity:0;transition:grid-template-rows .25s cubic-bezier(.86,0,.07,1),opacity .25s,visibility 0ms .25s;visibility:collapse}.md-nav__toggle~.md-nav>.md-nav__list{overflow:hidden}.md-nav__toggle:checked~.md-nav,.md-nav__toggle:indeterminate~.md-nav{grid-template-rows:1fr;opacity:1;transition:grid-template-rows .25s cubic-bezier(.86,0,.07,1),opacity .15s .1s,visibility 0ms;visibility:visible}.md-nav__item--nested>.md-nav>.md-nav__title{display:none}.md-nav__item--section{display:block;margin:1.25em 0}.md-nav__item--section:last-child{margin-bottom:0}.md-nav__item--section>.md-nav__link{font-weight:700}.md-nav__item--section>.md-nav__link[for]{color:var(--md-default-fg-color--light)}.md-nav__item--section>.md-nav__link:not(.md-nav__container){pointer-events:none}.md-nav__item--section>.md-nav__link .md-icon,.md-nav__item--section>.md-nav__link>[for]{display:none}[dir=ltr] .md-nav__item--section>.md-nav{margin-left:-.6rem}[dir=rtl] .md-nav__item--section>.md-nav{margin-right:-.6rem}.md-nav__item--section>.md-nav{display:block;opacity:1;visibility:visible}.md-nav__item--section>.md-nav>.md-nav__list>.md-nav__item{padding:0}.md-nav__icon{border-radius:100%;height:.9rem;transition:background-color .25s;width:.9rem}.md-nav__icon:hover{background-color:var(--md-accent-fg-color--transparent)}.md-nav__icon:after{background-color:currentcolor;border-radius:100%;content:"";display:inline-block;height:100%;-webkit-mask-image:var(--md-nav-icon--next);mask-image:var(--md-nav-icon--next);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:transform .25s;vertical-align:-.1rem;width:100%}[dir=rtl] .md-nav__icon:after{transform:rotate(180deg)}.md-nav__item--nested .md-nav__toggle:checked~.md-nav__link .md-nav__icon:after,.md-nav__item--nested .md-nav__toggle:indeterminate~.md-nav__link .md-nav__icon:after{transform:rotate(90deg)}.md-nav--lifted>.md-nav__list>.md-nav__item,.md-nav--lifted>.md-nav__title{display:none}.md-nav--lifted>.md-nav__list>.md-nav__item--active{display:block}.md-nav--lifted>.md-nav__list>.md-nav__item--active>.md-nav__link{background:var(--md-default-bg-color);box-shadow:0 0 .4rem .4rem var(--md-default-bg-color);margin-top:0;position:sticky;top:0;z-index:1}.md-nav--lifted>.md-nav__list>.md-nav__item--active>.md-nav__link:not(.md-nav__container){pointer-events:none}.md-nav--lifted>.md-nav__list>.md-nav__item--active.md-nav__item--section{margin:0}[dir=ltr] .md-nav--lifted>.md-nav__list>.md-nav__item>.md-nav{margin-left:-.6rem}[dir=rtl] .md-nav--lifted>.md-nav__list>.md-nav__item>.md-nav{margin-right:-.6rem}.md-nav--lifted>.md-nav__list>.md-nav__item>[for]{color:var(--md-default-fg-color--light)}.md-nav--lifted .md-nav[data-md-level="1"]{grid-template-rows:1fr;opacity:1;visibility:visible}.md-nav--integrated>.md-nav__list>.md-nav__item--active:not(.md-nav__item--nested){padding:0 .6rem}.md-nav--integrated>.md-nav__list>.md-nav__item--active:not(.md-nav__item--nested)>.md-nav__link{padding:0}[dir=ltr] .md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{border-left:.05rem solid var(--md-primary-fg-color)}[dir=rtl] .md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{border-right:.05rem solid var(--md-primary-fg-color)}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{display:block;margin-bottom:1.25em;opacity:1;visibility:visible}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary>.md-nav__list{overflow:visible;padding-bottom:0}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary>.md-nav__title{display:none}}.md-pagination{font-size:.8rem;font-weight:700;gap:.4rem}.md-pagination,.md-pagination>*{align-items:center;display:flex;justify-content:center}.md-pagination>*{border-radius:.2rem;height:1.8rem;min-width:1.8rem;text-align:center}.md-pagination__current{background-color:var(--md-default-fg-color--lightest);color:var(--md-default-fg-color--light)}.md-pagination__link{transition:color 125ms,background-color 125ms}.md-pagination__link:focus,.md-pagination__link:hover{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-pagination__link:focus svg,.md-pagination__link:hover svg{color:var(--md-accent-fg-color)}.md-pagination__link.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-pagination__link svg{fill:currentcolor;color:var(--md-default-fg-color--lighter);display:block;max-height:100%;width:1.2rem}.md-post__back{border-bottom:.05rem solid var(--md-default-fg-color--lightest);margin-bottom:1.2rem;padding-bottom:1.2rem}@media screen and (max-width:76.234375em){.md-post__back{display:none}}[dir=rtl] .md-post__back svg{transform:scaleX(-1)}.md-post__authors{display:flex;flex-direction:column;gap:.6rem;margin:0 .6rem 1.2rem}.md-post .md-post__meta a{transition:color 125ms}.md-post .md-post__meta a:focus,.md-post .md-post__meta a:hover{color:var(--md-accent-fg-color)}.md-post__title{color:var(--md-default-fg-color--light);font-weight:700}.md-post--excerpt{margin-bottom:3.2rem}.md-post--excerpt .md-post__header{align-items:center;display:flex;gap:.6rem;min-height:1.6rem}.md-post--excerpt .md-post__authors{align-items:center;display:inline-flex;flex-direction:row;gap:.2rem;margin:0;min-height:2.4rem}[dir=ltr] .md-post--excerpt .md-post__meta .md-meta__list{margin-right:.4rem}[dir=rtl] .md-post--excerpt .md-post__meta .md-meta__list{margin-left:.4rem}.md-post--excerpt .md-post__content>:first-child{--md-scroll-margin:6rem;margin-top:0}.md-post>.md-nav--secondary{margin:1em 0}.md-profile{align-items:center;display:flex;font-size:.7rem;gap:.6rem;line-height:1.4;width:100%}.md-profile__description{flex-grow:1}.md-content--post{display:flex}@media screen and (max-width:76.234375em){.md-content--post{flex-flow:column-reverse}}.md-content--post>.md-content__inner{min-width:0}@media screen and (min-width:76.25em){[dir=ltr] .md-content--post>.md-content__inner{margin-left:1.2rem}[dir=rtl] .md-content--post>.md-content__inner{margin-right:1.2rem}}@media screen and (max-width:76.234375em){.md-sidebar.md-sidebar--post{padding:0;position:static;width:100%}.md-sidebar.md-sidebar--post .md-sidebar__scrollwrap{overflow:visible}.md-sidebar.md-sidebar--post .md-sidebar__inner{padding:0}.md-sidebar.md-sidebar--post .md-post__meta{margin-left:.6rem;margin-right:.6rem}.md-sidebar.md-sidebar--post .md-nav__item{border:none;display:inline}.md-sidebar.md-sidebar--post .md-nav__list{display:inline-flex;flex-wrap:wrap;gap:.6rem;padding-bottom:.6rem;padding-top:.6rem}.md-sidebar.md-sidebar--post .md-nav__link{padding:0}.md-sidebar.md-sidebar--post .md-nav{height:auto;margin-bottom:0;position:static}}:root{--md-progress-value:0;--md-progress-delay:400ms}.md-progress{background:var(--md-primary-bg-color);height:.075rem;opacity:min(clamp(0,var(--md-progress-value),1),clamp(0,100 - var(--md-progress-value),1));position:fixed;top:0;transform:scaleX(calc(var(--md-progress-value)*1%));transform-origin:left;transition:transform .5s cubic-bezier(.19,1,.22,1),opacity .25s var(--md-progress-delay);width:100%;z-index:4}:root{--md-search-result-icon:url('data:image/svg+xml;charset=utf-8,')}.md-search{position:relative}@media screen and (min-width:60em){.md-search{padding:.2rem 0}}.no-js .md-search{display:none}.md-search__overlay{opacity:0;z-index:1}@media screen and (max-width:59.984375em){[dir=ltr] .md-search__overlay{left:-2.2rem}[dir=rtl] .md-search__overlay{right:-2.2rem}.md-search__overlay{background-color:var(--md-default-bg-color);border-radius:1rem;height:2rem;overflow:hidden;pointer-events:none;position:absolute;top:-1rem;transform-origin:center;transition:transform .3s .1s,opacity .2s .2s;width:2rem}[data-md-toggle=search]:checked~.md-header .md-search__overlay{opacity:1;transition:transform .4s,opacity .1s}}@media screen and (min-width:60em){[dir=ltr] .md-search__overlay{left:0}[dir=rtl] .md-search__overlay{right:0}.md-search__overlay{background-color:#0000008a;cursor:pointer;height:0;position:fixed;top:0;transition:width 0ms .25s,height 0ms .25s,opacity .25s;width:0}[data-md-toggle=search]:checked~.md-header .md-search__overlay{height:200vh;opacity:1;transition:width 0ms,height 0ms,opacity .25s;width:100%}}@media screen and (max-width:29.984375em){[data-md-toggle=search]:checked~.md-header .md-search__overlay{transform:scale(45)}}@media screen and (min-width:30em) and (max-width:44.984375em){[data-md-toggle=search]:checked~.md-header .md-search__overlay{transform:scale(60)}}@media screen and (min-width:45em) and (max-width:59.984375em){[data-md-toggle=search]:checked~.md-header .md-search__overlay{transform:scale(75)}}.md-search__inner{-webkit-backface-visibility:hidden;backface-visibility:hidden}@media screen and (max-width:59.984375em){[dir=ltr] .md-search__inner{left:0}[dir=rtl] .md-search__inner{right:0}.md-search__inner{height:0;opacity:0;overflow:hidden;position:fixed;top:0;transform:translateX(5%);transition:width 0ms .3s,height 0ms .3s,transform .15s cubic-bezier(.4,0,.2,1) .15s,opacity .15s .15s;width:0;z-index:2}[dir=rtl] .md-search__inner{transform:translateX(-5%)}[data-md-toggle=search]:checked~.md-header .md-search__inner{height:100%;opacity:1;transform:translateX(0);transition:width 0ms 0ms,height 0ms 0ms,transform .15s cubic-bezier(.1,.7,.1,1) .15s,opacity .15s .15s;width:100%}}@media screen and (min-width:60em){[dir=ltr] .md-search__inner{float:right}[dir=rtl] .md-search__inner{float:left}.md-search__inner{padding:.1rem 0;position:relative;transition:width .25s cubic-bezier(.1,.7,.1,1);width:11.7rem}}@media screen and (min-width:60em) and (max-width:76.234375em){[data-md-toggle=search]:checked~.md-header .md-search__inner{width:23.4rem}}@media screen and (min-width:76.25em){[data-md-toggle=search]:checked~.md-header .md-search__inner{width:34.4rem}}.md-search__form{background-color:var(--md-default-bg-color);box-shadow:0 0 .6rem #0000;height:2.4rem;position:relative;transition:color .25s,background-color .25s;z-index:2}@media screen and (min-width:60em){.md-search__form{background-color:#00000042;border-radius:.1rem;height:1.8rem}.md-search__form:hover{background-color:#ffffff1f}}[data-md-toggle=search]:checked~.md-header .md-search__form{background-color:var(--md-default-bg-color);border-radius:.1rem .1rem 0 0;box-shadow:0 0 .6rem #00000012;color:var(--md-default-fg-color)}[dir=ltr] .md-search__input{padding-left:3.6rem;padding-right:2.2rem}[dir=rtl] .md-search__input{padding-left:2.2rem;padding-right:3.6rem}.md-search__input{background:#0000;font-size:.9rem;height:100%;position:relative;text-overflow:ellipsis;width:100%;z-index:2}.md-search__input::placeholder{transition:color .25s}.md-search__input::placeholder,.md-search__input~.md-search__icon{color:var(--md-default-fg-color--light)}.md-search__input::-ms-clear{display:none}@media screen and (max-width:59.984375em){.md-search__input{font-size:.9rem;height:2.4rem;width:100%}}@media screen and (min-width:60em){[dir=ltr] .md-search__input{padding-left:2.2rem}[dir=rtl] .md-search__input{padding-right:2.2rem}.md-search__input{color:inherit;font-size:.8rem}.md-search__input::placeholder{color:var(--md-primary-bg-color--light)}.md-search__input+.md-search__icon{color:var(--md-primary-bg-color)}[data-md-toggle=search]:checked~.md-header .md-search__input{text-overflow:clip}[data-md-toggle=search]:checked~.md-header .md-search__input+.md-search__icon{color:var(--md-default-fg-color--light)}[data-md-toggle=search]:checked~.md-header .md-search__input::placeholder{color:#0000}}.md-search__icon{cursor:pointer;display:inline-block;height:1.2rem;transition:color .25s,opacity .25s;width:1.2rem}.md-search__icon:hover{opacity:.7}[dir=ltr] .md-search__icon[for=__search]{left:.5rem}[dir=rtl] .md-search__icon[for=__search]{right:.5rem}.md-search__icon[for=__search]{position:absolute;top:.3rem;z-index:2}[dir=rtl] .md-search__icon[for=__search] svg{transform:scaleX(-1)}@media screen and (max-width:59.984375em){[dir=ltr] .md-search__icon[for=__search]{left:.8rem}[dir=rtl] .md-search__icon[for=__search]{right:.8rem}.md-search__icon[for=__search]{top:.6rem}.md-search__icon[for=__search] svg:first-child{display:none}}@media screen and (min-width:60em){.md-search__icon[for=__search]{pointer-events:none}.md-search__icon[for=__search] svg:last-child{display:none}}[dir=ltr] .md-search__options{right:.5rem}[dir=rtl] .md-search__options{left:.5rem}.md-search__options{pointer-events:none;position:absolute;top:.3rem;z-index:2}@media screen and (max-width:59.984375em){[dir=ltr] .md-search__options{right:.8rem}[dir=rtl] .md-search__options{left:.8rem}.md-search__options{top:.6rem}}[dir=ltr] .md-search__options>.md-icon{margin-left:.2rem}[dir=rtl] .md-search__options>.md-icon{margin-right:.2rem}.md-search__options>.md-icon{color:var(--md-default-fg-color--light);opacity:0;transform:scale(.75);transition:transform .15s cubic-bezier(.1,.7,.1,1),opacity .15s}.md-search__options>.md-icon:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}[data-md-toggle=search]:checked~.md-header .md-search__input:valid~.md-search__options>.md-icon{opacity:1;pointer-events:auto;transform:scale(1)}[data-md-toggle=search]:checked~.md-header .md-search__input:valid~.md-search__options>.md-icon:hover{opacity:.7}[dir=ltr] .md-search__suggest{padding-left:3.6rem;padding-right:2.2rem}[dir=rtl] .md-search__suggest{padding-left:2.2rem;padding-right:3.6rem}.md-search__suggest{align-items:center;color:var(--md-default-fg-color--lighter);display:flex;font-size:.9rem;height:100%;opacity:0;position:absolute;top:0;transition:opacity 50ms;white-space:nowrap;width:100%}@media screen and (min-width:60em){[dir=ltr] .md-search__suggest{padding-left:2.2rem}[dir=rtl] .md-search__suggest{padding-right:2.2rem}.md-search__suggest{font-size:.8rem}}[data-md-toggle=search]:checked~.md-header .md-search__suggest{opacity:1;transition:opacity .3s .1s}[dir=ltr] .md-search__output{border-bottom-left-radius:.1rem}[dir=ltr] .md-search__output,[dir=rtl] .md-search__output{border-bottom-right-radius:.1rem}[dir=rtl] .md-search__output{border-bottom-left-radius:.1rem}.md-search__output{overflow:hidden;position:absolute;width:100%;z-index:1}@media screen and (max-width:59.984375em){.md-search__output{bottom:0;top:2.4rem}}@media screen and (min-width:60em){.md-search__output{opacity:0;top:1.9rem;transition:opacity .4s}[data-md-toggle=search]:checked~.md-header .md-search__output{box-shadow:var(--md-shadow-z3);opacity:1}}.md-search__scrollwrap{-webkit-backface-visibility:hidden;backface-visibility:hidden;background-color:var(--md-default-bg-color);height:100%;overflow-y:auto;touch-action:pan-y}@media (-webkit-max-device-pixel-ratio:1),(max-resolution:1dppx){.md-search__scrollwrap{transform:translateZ(0)}}@media screen and (min-width:60em) and (max-width:76.234375em){.md-search__scrollwrap{width:23.4rem}}@media screen and (min-width:76.25em){.md-search__scrollwrap{width:34.4rem}}@media screen and (min-width:60em){.md-search__scrollwrap{max-height:0;scrollbar-color:var(--md-default-fg-color--lighter) #0000;scrollbar-width:thin}[data-md-toggle=search]:checked~.md-header .md-search__scrollwrap{max-height:75vh}.md-search__scrollwrap:hover{scrollbar-color:var(--md-accent-fg-color) #0000}.md-search__scrollwrap::-webkit-scrollbar{height:.2rem;width:.2rem}.md-search__scrollwrap::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-search__scrollwrap::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}}.md-search-result{color:var(--md-default-fg-color);word-break:break-word}.md-search-result__meta{background-color:var(--md-default-fg-color--lightest);color:var(--md-default-fg-color--light);font-size:.64rem;line-height:1.8rem;padding:0 .8rem;scroll-snap-align:start}@media screen and (min-width:60em){[dir=ltr] .md-search-result__meta{padding-left:2.2rem}[dir=rtl] .md-search-result__meta{padding-right:2.2rem}}.md-search-result__list{list-style:none;margin:0;padding:0;-webkit-user-select:none;user-select:none}.md-search-result__item{box-shadow:0 -.05rem var(--md-default-fg-color--lightest)}.md-search-result__item:first-child{box-shadow:none}.md-search-result__link{display:block;outline:none;scroll-snap-align:start;transition:background-color .25s}.md-search-result__link:focus,.md-search-result__link:hover{background-color:var(--md-accent-fg-color--transparent)}.md-search-result__link:last-child p:last-child{margin-bottom:.6rem}.md-search-result__more>summary{cursor:pointer;display:block;outline:none;position:sticky;scroll-snap-align:start;top:0;z-index:1}.md-search-result__more>summary::marker{display:none}.md-search-result__more>summary::-webkit-details-marker{display:none}.md-search-result__more>summary>div{color:var(--md-typeset-a-color);font-size:.64rem;padding:.75em .8rem;transition:color .25s,background-color .25s}@media screen and (min-width:60em){[dir=ltr] .md-search-result__more>summary>div{padding-left:2.2rem}[dir=rtl] .md-search-result__more>summary>div{padding-right:2.2rem}}.md-search-result__more>summary:focus>div,.md-search-result__more>summary:hover>div{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-search-result__more[open]>summary{background-color:var(--md-default-bg-color)}.md-search-result__article{overflow:hidden;padding:0 .8rem;position:relative}@media screen and (min-width:60em){[dir=ltr] .md-search-result__article{padding-left:2.2rem}[dir=rtl] .md-search-result__article{padding-right:2.2rem}}[dir=ltr] .md-search-result__icon{left:0}[dir=rtl] .md-search-result__icon{right:0}.md-search-result__icon{color:var(--md-default-fg-color--light);height:1.2rem;margin:.5rem;position:absolute;width:1.2rem}@media screen and (max-width:59.984375em){.md-search-result__icon{display:none}}.md-search-result__icon:after{background-color:currentcolor;content:"";display:inline-block;height:100%;-webkit-mask-image:var(--md-search-result-icon);mask-image:var(--md-search-result-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:100%}[dir=rtl] .md-search-result__icon:after{transform:scaleX(-1)}.md-search-result .md-typeset{color:var(--md-default-fg-color--light);font-size:.64rem;line-height:1.6}.md-search-result .md-typeset h1{color:var(--md-default-fg-color);font-size:.8rem;font-weight:400;line-height:1.4;margin:.55rem 0}.md-search-result .md-typeset h1 mark{text-decoration:none}.md-search-result .md-typeset h2{color:var(--md-default-fg-color);font-size:.64rem;font-weight:700;line-height:1.6;margin:.5em 0}.md-search-result .md-typeset h2 mark{text-decoration:none}.md-search-result__terms{color:var(--md-default-fg-color);display:block;font-size:.64rem;font-style:italic;margin:.5em 0}.md-search-result mark{background-color:initial;color:var(--md-accent-fg-color);text-decoration:underline}.md-select{position:relative;z-index:1}.md-select__inner{background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color);left:50%;margin-top:.2rem;max-height:0;opacity:0;position:absolute;top:calc(100% - .2rem);transform:translate3d(-50%,.3rem,0);transition:transform .25s 375ms,opacity .25s .25s,max-height 0ms .5s}.md-select:focus-within .md-select__inner,.md-select:hover .md-select__inner{max-height:10rem;opacity:1;transform:translate3d(-50%,0,0);transition:transform .25s cubic-bezier(.1,.7,.1,1),opacity .25s,max-height 0ms}.md-select__inner:after{border-bottom:.2rem solid #0000;border-bottom-color:var(--md-default-bg-color);border-left:.2rem solid #0000;border-right:.2rem solid #0000;border-top:0;content:"";height:0;left:50%;margin-left:-.2rem;margin-top:-.2rem;position:absolute;top:0;width:0}.md-select__list{border-radius:.1rem;font-size:.8rem;list-style-type:none;margin:0;max-height:inherit;overflow:auto;padding:0}.md-select__item{line-height:1.8rem}[dir=ltr] .md-select__link{padding-left:.6rem;padding-right:1.2rem}[dir=rtl] .md-select__link{padding-left:1.2rem;padding-right:.6rem}.md-select__link{cursor:pointer;display:block;outline:none;scroll-snap-align:start;transition:background-color .25s,color .25s;width:100%}.md-select__link:focus,.md-select__link:hover{color:var(--md-accent-fg-color)}.md-select__link:focus{background-color:var(--md-default-fg-color--lightest)}.md-sidebar{align-self:flex-start;flex-shrink:0;padding:1.2rem 0;position:sticky;top:2.4rem;width:12.1rem}@media print{.md-sidebar{display:none}}@media screen and (max-width:76.234375em){[dir=ltr] .md-sidebar--primary{left:-12.1rem}[dir=rtl] .md-sidebar--primary{right:-12.1rem}.md-sidebar--primary{background-color:var(--md-default-bg-color);display:block;height:100%;position:fixed;top:0;transform:translateX(0);transition:transform .25s cubic-bezier(.4,0,.2,1),box-shadow .25s;width:12.1rem;z-index:5}[data-md-toggle=drawer]:checked~.md-container .md-sidebar--primary{box-shadow:var(--md-shadow-z3);transform:translateX(12.1rem)}[dir=rtl] [data-md-toggle=drawer]:checked~.md-container .md-sidebar--primary{transform:translateX(-12.1rem)}.md-sidebar--primary .md-sidebar__scrollwrap{bottom:0;left:0;margin:0;overflow:hidden;position:absolute;right:0;scroll-snap-type:none;top:0}}@media screen and (min-width:76.25em){.md-sidebar{height:0}.no-js .md-sidebar{height:auto}.md-header--lifted~.md-container .md-sidebar{top:4.8rem}}.md-sidebar--secondary{display:none;order:2}@media screen and (min-width:60em){.md-sidebar--secondary{height:0}.no-js .md-sidebar--secondary{height:auto}.md-sidebar--secondary:not([hidden]){display:block}.md-sidebar--secondary .md-sidebar__scrollwrap{touch-action:pan-y}}.md-sidebar__scrollwrap{scrollbar-gutter:stable;-webkit-backface-visibility:hidden;backface-visibility:hidden;margin:0 .2rem;overflow-y:auto;scrollbar-color:var(--md-default-fg-color--lighter) #0000;scrollbar-width:thin}.md-sidebar__scrollwrap::-webkit-scrollbar{height:.2rem;width:.2rem}.md-sidebar__scrollwrap:focus-within,.md-sidebar__scrollwrap:hover{scrollbar-color:var(--md-accent-fg-color) #0000}.md-sidebar__scrollwrap:focus-within::-webkit-scrollbar-thumb,.md-sidebar__scrollwrap:hover::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-sidebar__scrollwrap:focus-within::-webkit-scrollbar-thumb:hover,.md-sidebar__scrollwrap:hover::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}@supports selector(::-webkit-scrollbar){.md-sidebar__scrollwrap{scrollbar-gutter:auto}[dir=ltr] .md-sidebar__inner{padding-right:calc(100% - 11.5rem)}[dir=rtl] .md-sidebar__inner{padding-left:calc(100% - 11.5rem)}}@media screen and (max-width:76.234375em){.md-overlay{background-color:#0000008a;height:0;opacity:0;position:fixed;top:0;transition:width 0ms .25s,height 0ms .25s,opacity .25s;width:0;z-index:5}[data-md-toggle=drawer]:checked~.md-overlay{height:100%;opacity:1;transition:width 0ms,height 0ms,opacity .25s;width:100%}}@keyframes facts{0%{height:0}to{height:.65rem}}@keyframes fact{0%{opacity:0;transform:translateY(100%)}50%{opacity:0}to{opacity:1;transform:translateY(0)}}:root{--md-source-forks-icon:url('data:image/svg+xml;charset=utf-8,');--md-source-repositories-icon:url('data:image/svg+xml;charset=utf-8,');--md-source-stars-icon:url('data:image/svg+xml;charset=utf-8,');--md-source-version-icon:url('data:image/svg+xml;charset=utf-8,')}.md-source{-webkit-backface-visibility:hidden;backface-visibility:hidden;display:block;font-size:.65rem;line-height:1.2;outline-color:var(--md-accent-fg-color);transition:opacity .25s;white-space:nowrap}.md-source:hover{opacity:.7}.md-source__icon{display:inline-block;height:2.4rem;vertical-align:middle;width:2rem}[dir=ltr] .md-source__icon svg{margin-left:.6rem}[dir=rtl] .md-source__icon svg{margin-right:.6rem}.md-source__icon svg{margin-top:.6rem}[dir=ltr] .md-source__icon+.md-source__repository{padding-left:2rem}[dir=rtl] .md-source__icon+.md-source__repository{padding-right:2rem}[dir=ltr] .md-source__icon+.md-source__repository{margin-left:-2rem}[dir=rtl] .md-source__icon+.md-source__repository{margin-right:-2rem}[dir=ltr] .md-source__repository{margin-left:.6rem}[dir=rtl] .md-source__repository{margin-right:.6rem}.md-source__repository{display:inline-block;max-width:calc(100% - 1.2rem);overflow:hidden;text-overflow:ellipsis;vertical-align:middle}.md-source__facts{display:flex;font-size:.55rem;gap:.4rem;list-style-type:none;margin:.1rem 0 0;opacity:.75;overflow:hidden;padding:0;width:100%}.md-source__repository--active .md-source__facts{animation:facts .25s ease-in}.md-source__fact{overflow:hidden;text-overflow:ellipsis}.md-source__repository--active .md-source__fact{animation:fact .4s ease-out}[dir=ltr] .md-source__fact:before{margin-right:.1rem}[dir=rtl] .md-source__fact:before{margin-left:.1rem}.md-source__fact:before{background-color:currentcolor;content:"";display:inline-block;height:.6rem;-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;vertical-align:text-top;width:.6rem}.md-source__fact:nth-child(1n+2){flex-shrink:0}.md-source__fact--version:before{-webkit-mask-image:var(--md-source-version-icon);mask-image:var(--md-source-version-icon)}.md-source__fact--stars:before{-webkit-mask-image:var(--md-source-stars-icon);mask-image:var(--md-source-stars-icon)}.md-source__fact--forks:before{-webkit-mask-image:var(--md-source-forks-icon);mask-image:var(--md-source-forks-icon)}.md-source__fact--repositories:before{-webkit-mask-image:var(--md-source-repositories-icon);mask-image:var(--md-source-repositories-icon)}:root{--md-status:url('data:image/svg+xml;charset=utf-8,');--md-status--new:url('data:image/svg+xml;charset=utf-8,');--md-status--deprecated:url('data:image/svg+xml;charset=utf-8,');--md-status--encrypted:url('data:image/svg+xml;charset=utf-8,')}.md-status:after{background-color:var(--md-default-fg-color--light);content:"";display:inline-block;height:1.125em;-webkit-mask-image:var(--md-status);mask-image:var(--md-status);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;vertical-align:text-bottom;width:1.125em}.md-status:hover:after{background-color:currentcolor}.md-status--new:after{-webkit-mask-image:var(--md-status--new);mask-image:var(--md-status--new)}.md-status--deprecated:after{-webkit-mask-image:var(--md-status--deprecated);mask-image:var(--md-status--deprecated)}.md-status--encrypted:after{-webkit-mask-image:var(--md-status--encrypted);mask-image:var(--md-status--encrypted)}.md-tabs{background-color:var(--md-primary-fg-color);color:var(--md-primary-bg-color);display:block;line-height:1.3;overflow:auto;width:100%;z-index:3}@media print{.md-tabs{display:none}}@media screen and (max-width:76.234375em){.md-tabs{display:none}}.md-tabs[hidden]{pointer-events:none}[dir=ltr] .md-tabs__list{margin-left:.2rem}[dir=rtl] .md-tabs__list{margin-right:.2rem}.md-tabs__list{contain:content;display:flex;list-style:none;margin:0;overflow:auto;padding:0;scrollbar-width:none;white-space:nowrap}.md-tabs__list::-webkit-scrollbar{display:none}.md-tabs__item{height:2.4rem;padding-left:.6rem;padding-right:.6rem}.md-tabs__item--active .md-tabs__link{color:inherit;opacity:1}.md-tabs__link{-webkit-backface-visibility:hidden;backface-visibility:hidden;display:flex;font-size:.7rem;margin-top:.8rem;opacity:.7;outline-color:var(--md-accent-fg-color);outline-offset:.2rem;transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .25s}.md-tabs__link:focus,.md-tabs__link:hover{color:inherit;opacity:1}[dir=ltr] .md-tabs__link svg{margin-right:.4rem}[dir=rtl] .md-tabs__link svg{margin-left:.4rem}.md-tabs__link svg{fill:currentcolor;height:1.3em}.md-tabs__item:nth-child(2) .md-tabs__link{transition-delay:20ms}.md-tabs__item:nth-child(3) .md-tabs__link{transition-delay:40ms}.md-tabs__item:nth-child(4) .md-tabs__link{transition-delay:60ms}.md-tabs__item:nth-child(5) .md-tabs__link{transition-delay:80ms}.md-tabs__item:nth-child(6) .md-tabs__link{transition-delay:.1s}.md-tabs__item:nth-child(7) .md-tabs__link{transition-delay:.12s}.md-tabs__item:nth-child(8) .md-tabs__link{transition-delay:.14s}.md-tabs__item:nth-child(9) .md-tabs__link{transition-delay:.16s}.md-tabs__item:nth-child(10) .md-tabs__link{transition-delay:.18s}.md-tabs__item:nth-child(11) .md-tabs__link{transition-delay:.2s}.md-tabs__item:nth-child(12) .md-tabs__link{transition-delay:.22s}.md-tabs__item:nth-child(13) .md-tabs__link{transition-delay:.24s}.md-tabs__item:nth-child(14) .md-tabs__link{transition-delay:.26s}.md-tabs__item:nth-child(15) .md-tabs__link{transition-delay:.28s}.md-tabs__item:nth-child(16) .md-tabs__link{transition-delay:.3s}.md-tabs[hidden] .md-tabs__link{opacity:0;transform:translateY(50%);transition:transform 0ms .1s,opacity .1s}:root{--md-tag-icon:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .md-tags:not([hidden]){display:inline-flex;flex-wrap:wrap;gap:.5em;margin-bottom:.75em;margin-top:-.125em}.md-typeset .md-tag{align-items:center;background:var(--md-default-fg-color--lightest);border-radius:2.4rem;display:inline-flex;font-size:.64rem;font-size:min(.8em,.64rem);font-weight:700;gap:.5em;letter-spacing:normal;line-height:1.6;padding:.3125em .78125em}.md-typeset .md-tag[href]{-webkit-tap-highlight-color:transparent;color:inherit;outline:none;transition:color 125ms,background-color 125ms}.md-typeset .md-tag[href]:focus,.md-typeset .md-tag[href]:hover{background-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}[id]>.md-typeset .md-tag{vertical-align:text-top}.md-typeset .md-tag-icon:before{background-color:var(--md-default-fg-color--lighter);content:"";display:inline-block;height:1.2em;-webkit-mask-image:var(--md-tag-icon);mask-image:var(--md-tag-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color 125ms;vertical-align:text-bottom;width:1.2em}.md-typeset .md-tag-icon[href]:focus:before,.md-typeset .md-tag-icon[href]:hover:before{background-color:var(--md-accent-bg-color)}@keyframes pulse{0%{transform:scale(.95)}75%{transform:scale(1)}to{transform:scale(.95)}}:root{--md-annotation-bg-icon:url('data:image/svg+xml;charset=utf-8,');--md-annotation-icon:url('data:image/svg+xml;charset=utf-8,');--md-tooltip-width:20rem}.md-tooltip{-webkit-backface-visibility:hidden;backface-visibility:hidden;background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color);font-family:var(--md-text-font-family);left:clamp(var(--md-tooltip-0,0rem) + .8rem,var(--md-tooltip-x),100vw + var(--md-tooltip-0,0rem) + .8rem - var(--md-tooltip-width) - 2 * .8rem);max-width:calc(100vw - 1.6rem);opacity:0;position:absolute;top:var(--md-tooltip-y);transform:translateY(-.4rem);transition:transform 0ms .25s,opacity .25s,z-index .25s;width:var(--md-tooltip-width);z-index:0}.md-tooltip--active{opacity:1;transform:translateY(0);transition:transform .25s cubic-bezier(.1,.7,.1,1),opacity .25s,z-index 0ms;z-index:2}.focus-visible>.md-tooltip,.md-tooltip:target{outline:var(--md-accent-fg-color) auto}.md-tooltip__inner{font-size:.64rem;padding:.8rem}.md-tooltip__inner.md-typeset>:first-child{margin-top:0}.md-tooltip__inner.md-typeset>:last-child{margin-bottom:0}.md-annotation{font-weight:400;outline:none;vertical-align:text-bottom;white-space:normal}[dir=rtl] .md-annotation{direction:rtl}code .md-annotation{font-family:var(--md-code-font-family);font-size:inherit}.md-annotation:not([hidden]){display:inline-block;line-height:1.25}.md-annotation__index{border-radius:.01px;cursor:pointer;display:inline-block;margin-left:.4ch;margin-right:.4ch;outline:none;overflow:hidden;position:relative;-webkit-user-select:none;user-select:none;vertical-align:text-top;z-index:0}.md-annotation .md-annotation__index{transition:z-index .25s}@media screen{.md-annotation__index{width:2.2ch}[data-md-visible]>.md-annotation__index{animation:pulse 2s infinite}.md-annotation__index:before{background:var(--md-default-bg-color);-webkit-mask-image:var(--md-annotation-bg-icon);mask-image:var(--md-annotation-bg-icon)}.md-annotation__index:after,.md-annotation__index:before{content:"";height:2.2ch;-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:-.1ch;width:2.2ch;z-index:-1}.md-annotation__index:after{background-color:var(--md-default-fg-color--lighter);-webkit-mask-image:var(--md-annotation-icon);mask-image:var(--md-annotation-icon);transform:scale(1.0001);transition:background-color .25s,transform .25s}.md-tooltip--active+.md-annotation__index:after{transform:rotate(45deg)}.md-tooltip--active+.md-annotation__index:after,:hover>.md-annotation__index:after{background-color:var(--md-accent-fg-color)}}.md-tooltip--active+.md-annotation__index{animation-play-state:paused;transition-duration:0ms;z-index:2}.md-annotation__index [data-md-annotation-id]{display:inline-block}@media print{.md-annotation__index [data-md-annotation-id]{background:var(--md-default-fg-color--lighter);border-radius:2ch;color:var(--md-default-bg-color);font-weight:700;padding:0 .6ch;white-space:nowrap}.md-annotation__index [data-md-annotation-id]:after{content:attr(data-md-annotation-id)}}.md-typeset .md-annotation-list{counter-reset:xxx;list-style:none}.md-typeset .md-annotation-list li{position:relative}[dir=ltr] .md-typeset .md-annotation-list li:before{left:-2.125em}[dir=rtl] .md-typeset .md-annotation-list li:before{right:-2.125em}.md-typeset .md-annotation-list li:before{background:var(--md-default-fg-color--lighter);border-radius:2ch;color:var(--md-default-bg-color);content:counter(xxx);counter-increment:xxx;font-size:.8875em;font-weight:700;height:2ch;line-height:1.25;min-width:2ch;padding:0 .6ch;position:absolute;text-align:center;top:.25em}[dir=ltr] .md-top{margin-left:50%}[dir=rtl] .md-top{margin-right:50%}.md-top{background-color:var(--md-default-bg-color);border-radius:1.6rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color--light);cursor:pointer;display:block;font-size:.7rem;outline:none;padding:.4rem .8rem;position:fixed;top:3.2rem;transform:translate(-50%);transition:color 125ms,background-color 125ms,transform 125ms cubic-bezier(.4,0,.2,1),opacity 125ms;z-index:2}@media print{.md-top{display:none}}[dir=rtl] .md-top{transform:translate(50%)}.md-top[hidden]{opacity:0;pointer-events:none;transform:translate(-50%,.2rem);transition-duration:0ms}[dir=rtl] .md-top[hidden]{transform:translate(50%,.2rem)}.md-top:focus,.md-top:hover{background-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}.md-top svg{display:inline-block;vertical-align:-.5em}@keyframes hoverfix{0%{pointer-events:none}}:root{--md-version-icon:url('data:image/svg+xml;charset=utf-8,')}.md-version{flex-shrink:0;font-size:.8rem;height:2.4rem}[dir=ltr] .md-version__current{margin-left:1.4rem;margin-right:.4rem}[dir=rtl] .md-version__current{margin-left:.4rem;margin-right:1.4rem}.md-version__current{color:inherit;cursor:pointer;outline:none;position:relative;top:.05rem}[dir=ltr] .md-version__current:after{margin-left:.4rem}[dir=rtl] .md-version__current:after{margin-right:.4rem}.md-version__current:after{background-color:currentcolor;content:"";display:inline-block;height:.6rem;-webkit-mask-image:var(--md-version-icon);mask-image:var(--md-version-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:.4rem}.md-version__list{background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color);list-style-type:none;margin:.2rem .8rem;max-height:0;opacity:0;overflow:auto;padding:0;position:absolute;scroll-snap-type:y mandatory;top:.15rem;transition:max-height 0ms .5s,opacity .25s .25s;z-index:3}.md-version:focus-within .md-version__list,.md-version:hover .md-version__list{max-height:10rem;opacity:1;transition:max-height 0ms,opacity .25s}@media (hover:none),(pointer:coarse){.md-version:hover .md-version__list{animation:hoverfix .25s forwards}.md-version:focus-within .md-version__list{animation:none}}.md-version__item{line-height:1.8rem}[dir=ltr] .md-version__link{padding-left:.6rem;padding-right:1.2rem}[dir=rtl] .md-version__link{padding-left:1.2rem;padding-right:.6rem}.md-version__link{cursor:pointer;display:block;outline:none;scroll-snap-align:start;transition:color .25s,background-color .25s;white-space:nowrap;width:100%}.md-version__link:focus,.md-version__link:hover{color:var(--md-accent-fg-color)}.md-version__link:focus{background-color:var(--md-default-fg-color--lightest)}:root{--md-admonition-icon--note:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--abstract:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--info:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--tip:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--success:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--question:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--warning:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--failure:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--danger:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--bug:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--example:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--quote:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .admonition,.md-typeset details{background-color:var(--md-admonition-bg-color);border:.075rem solid #448aff;border-radius:.2rem;box-shadow:var(--md-shadow-z1);color:var(--md-admonition-fg-color);display:flow-root;font-size:.64rem;margin:1.5625em 0;padding:0 .6rem;page-break-inside:avoid;transition:box-shadow 125ms}@media print{.md-typeset .admonition,.md-typeset details{box-shadow:none}}.md-typeset .admonition:focus-within,.md-typeset details:focus-within{box-shadow:0 0 0 .2rem #448aff1a}.md-typeset .admonition>*,.md-typeset details>*{box-sizing:border-box}.md-typeset .admonition .admonition,.md-typeset .admonition details,.md-typeset details .admonition,.md-typeset details details{margin-bottom:1em;margin-top:1em}.md-typeset .admonition .md-typeset__scrollwrap,.md-typeset details .md-typeset__scrollwrap{margin:1em -.6rem}.md-typeset .admonition .md-typeset__table,.md-typeset details .md-typeset__table{padding:0 .6rem}.md-typeset .admonition>.tabbed-set:only-child,.md-typeset details>.tabbed-set:only-child{margin-top:0}html .md-typeset .admonition>:last-child,html .md-typeset details>:last-child{margin-bottom:.6rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary{padding-left:2rem;padding-right:.6rem}[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{padding-left:.6rem;padding-right:2rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary{border-left-width:.2rem}[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{border-right-width:.2rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary{border-top-left-radius:.1rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary,[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{border-top-right-radius:.1rem}[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{border-top-left-radius:.1rem}.md-typeset .admonition-title,.md-typeset summary{background-color:#448aff1a;border:none;font-weight:700;margin:0 -.6rem;padding-bottom:.4rem;padding-top:.4rem;position:relative}html .md-typeset .admonition-title:last-child,html .md-typeset summary:last-child{margin-bottom:0}[dir=ltr] .md-typeset .admonition-title:before,[dir=ltr] .md-typeset summary:before{left:.6rem}[dir=rtl] .md-typeset .admonition-title:before,[dir=rtl] .md-typeset summary:before{right:.6rem}.md-typeset .admonition-title:before,.md-typeset summary:before{background-color:#448aff;content:"";height:1rem;-webkit-mask-image:var(--md-admonition-icon--note);mask-image:var(--md-admonition-icon--note);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.625em;width:1rem}.md-typeset .admonition-title code,.md-typeset summary code{box-shadow:0 0 0 .05rem var(--md-default-fg-color--lightest)}.md-typeset .admonition.note,.md-typeset details.note{border-color:#448aff}.md-typeset .admonition.note:focus-within,.md-typeset details.note:focus-within{box-shadow:0 0 0 .2rem #448aff1a}.md-typeset .note>.admonition-title,.md-typeset .note>summary{background-color:#448aff1a}.md-typeset .note>.admonition-title:before,.md-typeset .note>summary:before{background-color:#448aff;-webkit-mask-image:var(--md-admonition-icon--note);mask-image:var(--md-admonition-icon--note)}.md-typeset .note>.admonition-title:after,.md-typeset .note>summary:after{color:#448aff}.md-typeset .admonition.abstract,.md-typeset details.abstract{border-color:#00b0ff}.md-typeset .admonition.abstract:focus-within,.md-typeset details.abstract:focus-within{box-shadow:0 0 0 .2rem #00b0ff1a}.md-typeset .abstract>.admonition-title,.md-typeset .abstract>summary{background-color:#00b0ff1a}.md-typeset .abstract>.admonition-title:before,.md-typeset .abstract>summary:before{background-color:#00b0ff;-webkit-mask-image:var(--md-admonition-icon--abstract);mask-image:var(--md-admonition-icon--abstract)}.md-typeset .abstract>.admonition-title:after,.md-typeset .abstract>summary:after{color:#00b0ff}.md-typeset .admonition.info,.md-typeset details.info{border-color:#00b8d4}.md-typeset .admonition.info:focus-within,.md-typeset details.info:focus-within{box-shadow:0 0 0 .2rem #00b8d41a}.md-typeset .info>.admonition-title,.md-typeset .info>summary{background-color:#00b8d41a}.md-typeset .info>.admonition-title:before,.md-typeset .info>summary:before{background-color:#00b8d4;-webkit-mask-image:var(--md-admonition-icon--info);mask-image:var(--md-admonition-icon--info)}.md-typeset .info>.admonition-title:after,.md-typeset .info>summary:after{color:#00b8d4}.md-typeset .admonition.tip,.md-typeset details.tip{border-color:#00bfa5}.md-typeset .admonition.tip:focus-within,.md-typeset details.tip:focus-within{box-shadow:0 0 0 .2rem #00bfa51a}.md-typeset .tip>.admonition-title,.md-typeset .tip>summary{background-color:#00bfa51a}.md-typeset .tip>.admonition-title:before,.md-typeset .tip>summary:before{background-color:#00bfa5;-webkit-mask-image:var(--md-admonition-icon--tip);mask-image:var(--md-admonition-icon--tip)}.md-typeset .tip>.admonition-title:after,.md-typeset .tip>summary:after{color:#00bfa5}.md-typeset .admonition.success,.md-typeset details.success{border-color:#00c853}.md-typeset .admonition.success:focus-within,.md-typeset details.success:focus-within{box-shadow:0 0 0 .2rem #00c8531a}.md-typeset .success>.admonition-title,.md-typeset .success>summary{background-color:#00c8531a}.md-typeset .success>.admonition-title:before,.md-typeset .success>summary:before{background-color:#00c853;-webkit-mask-image:var(--md-admonition-icon--success);mask-image:var(--md-admonition-icon--success)}.md-typeset .success>.admonition-title:after,.md-typeset .success>summary:after{color:#00c853}.md-typeset .admonition.question,.md-typeset details.question{border-color:#64dd17}.md-typeset .admonition.question:focus-within,.md-typeset details.question:focus-within{box-shadow:0 0 0 .2rem #64dd171a}.md-typeset .question>.admonition-title,.md-typeset .question>summary{background-color:#64dd171a}.md-typeset .question>.admonition-title:before,.md-typeset .question>summary:before{background-color:#64dd17;-webkit-mask-image:var(--md-admonition-icon--question);mask-image:var(--md-admonition-icon--question)}.md-typeset .question>.admonition-title:after,.md-typeset .question>summary:after{color:#64dd17}.md-typeset .admonition.warning,.md-typeset details.warning{border-color:#ff9100}.md-typeset .admonition.warning:focus-within,.md-typeset details.warning:focus-within{box-shadow:0 0 0 .2rem #ff91001a}.md-typeset .warning>.admonition-title,.md-typeset .warning>summary{background-color:#ff91001a}.md-typeset .warning>.admonition-title:before,.md-typeset .warning>summary:before{background-color:#ff9100;-webkit-mask-image:var(--md-admonition-icon--warning);mask-image:var(--md-admonition-icon--warning)}.md-typeset .warning>.admonition-title:after,.md-typeset .warning>summary:after{color:#ff9100}.md-typeset .admonition.failure,.md-typeset details.failure{border-color:#ff5252}.md-typeset .admonition.failure:focus-within,.md-typeset details.failure:focus-within{box-shadow:0 0 0 .2rem #ff52521a}.md-typeset .failure>.admonition-title,.md-typeset .failure>summary{background-color:#ff52521a}.md-typeset .failure>.admonition-title:before,.md-typeset .failure>summary:before{background-color:#ff5252;-webkit-mask-image:var(--md-admonition-icon--failure);mask-image:var(--md-admonition-icon--failure)}.md-typeset .failure>.admonition-title:after,.md-typeset .failure>summary:after{color:#ff5252}.md-typeset .admonition.danger,.md-typeset details.danger{border-color:#ff1744}.md-typeset .admonition.danger:focus-within,.md-typeset details.danger:focus-within{box-shadow:0 0 0 .2rem #ff17441a}.md-typeset .danger>.admonition-title,.md-typeset .danger>summary{background-color:#ff17441a}.md-typeset .danger>.admonition-title:before,.md-typeset .danger>summary:before{background-color:#ff1744;-webkit-mask-image:var(--md-admonition-icon--danger);mask-image:var(--md-admonition-icon--danger)}.md-typeset .danger>.admonition-title:after,.md-typeset .danger>summary:after{color:#ff1744}.md-typeset .admonition.bug,.md-typeset details.bug{border-color:#f50057}.md-typeset .admonition.bug:focus-within,.md-typeset details.bug:focus-within{box-shadow:0 0 0 .2rem #f500571a}.md-typeset .bug>.admonition-title,.md-typeset .bug>summary{background-color:#f500571a}.md-typeset .bug>.admonition-title:before,.md-typeset .bug>summary:before{background-color:#f50057;-webkit-mask-image:var(--md-admonition-icon--bug);mask-image:var(--md-admonition-icon--bug)}.md-typeset .bug>.admonition-title:after,.md-typeset .bug>summary:after{color:#f50057}.md-typeset .admonition.example,.md-typeset details.example{border-color:#7c4dff}.md-typeset .admonition.example:focus-within,.md-typeset details.example:focus-within{box-shadow:0 0 0 .2rem #7c4dff1a}.md-typeset .example>.admonition-title,.md-typeset .example>summary{background-color:#7c4dff1a}.md-typeset .example>.admonition-title:before,.md-typeset .example>summary:before{background-color:#7c4dff;-webkit-mask-image:var(--md-admonition-icon--example);mask-image:var(--md-admonition-icon--example)}.md-typeset .example>.admonition-title:after,.md-typeset .example>summary:after{color:#7c4dff}.md-typeset .admonition.quote,.md-typeset details.quote{border-color:#9e9e9e}.md-typeset .admonition.quote:focus-within,.md-typeset details.quote:focus-within{box-shadow:0 0 0 .2rem #9e9e9e1a}.md-typeset .quote>.admonition-title,.md-typeset .quote>summary{background-color:#9e9e9e1a}.md-typeset .quote>.admonition-title:before,.md-typeset .quote>summary:before{background-color:#9e9e9e;-webkit-mask-image:var(--md-admonition-icon--quote);mask-image:var(--md-admonition-icon--quote)}.md-typeset .quote>.admonition-title:after,.md-typeset .quote>summary:after{color:#9e9e9e}:root{--md-footnotes-icon:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .footnote{color:var(--md-default-fg-color--light);font-size:.64rem}[dir=ltr] .md-typeset .footnote>ol{margin-left:0}[dir=rtl] .md-typeset .footnote>ol{margin-right:0}.md-typeset .footnote>ol>li{transition:color 125ms}.md-typeset .footnote>ol>li:target{color:var(--md-default-fg-color)}.md-typeset .footnote>ol>li:focus-within .footnote-backref{opacity:1;transform:translateX(0);transition:none}.md-typeset .footnote>ol>li:hover .footnote-backref,.md-typeset .footnote>ol>li:target .footnote-backref{opacity:1;transform:translateX(0)}.md-typeset .footnote>ol>li>:first-child{margin-top:0}.md-typeset .footnote-ref{font-size:.75em;font-weight:700}html .md-typeset .footnote-ref{outline-offset:.1rem}.md-typeset [id^="fnref:"]:target>.footnote-ref{outline:auto}.md-typeset .footnote-backref{color:var(--md-typeset-a-color);display:inline-block;font-size:0;opacity:0;transform:translateX(.25rem);transition:color .25s,transform .25s .25s,opacity 125ms .25s;vertical-align:text-bottom}@media print{.md-typeset .footnote-backref{color:var(--md-typeset-a-color);opacity:1;transform:translateX(0)}}[dir=rtl] .md-typeset .footnote-backref{transform:translateX(-.25rem)}.md-typeset .footnote-backref:hover{color:var(--md-accent-fg-color)}.md-typeset .footnote-backref:before{background-color:currentcolor;content:"";display:inline-block;height:.8rem;-webkit-mask-image:var(--md-footnotes-icon);mask-image:var(--md-footnotes-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:.8rem}[dir=rtl] .md-typeset .footnote-backref:before svg{transform:scaleX(-1)}[dir=ltr] .md-typeset .headerlink{margin-left:.5rem}[dir=rtl] .md-typeset .headerlink{margin-right:.5rem}.md-typeset .headerlink{color:var(--md-default-fg-color--lighter);display:inline-block;opacity:0;transition:color .25s,opacity 125ms}@media print{.md-typeset .headerlink{display:none}}.md-typeset .headerlink:focus,.md-typeset :hover>.headerlink,.md-typeset :target>.headerlink{opacity:1;transition:color .25s,opacity 125ms}.md-typeset .headerlink:focus,.md-typeset .headerlink:hover,.md-typeset :target>.headerlink{color:var(--md-accent-fg-color)}.md-typeset :target{--md-scroll-margin:3.6rem;--md-scroll-offset:0rem;scroll-margin-top:calc(var(--md-scroll-margin) - var(--md-scroll-offset))}@media screen and (min-width:76.25em){.md-header--lifted~.md-container .md-typeset :target{--md-scroll-margin:6rem}}.md-typeset h1:target,.md-typeset h2:target,.md-typeset h3:target{--md-scroll-offset:0.2rem}.md-typeset h4:target{--md-scroll-offset:0.15rem}.md-typeset div.arithmatex{overflow:auto}@media screen and (max-width:44.984375em){.md-typeset div.arithmatex{margin:0 -.8rem}}.md-typeset div.arithmatex>*{margin-left:auto!important;margin-right:auto!important;padding:0 .8rem;touch-action:auto;width:-webkit-min-content;width:min-content}.md-typeset div.arithmatex>* mjx-container{margin:0!important}.md-typeset del.critic{background-color:var(--md-typeset-del-color)}.md-typeset del.critic,.md-typeset ins.critic{-webkit-box-decoration-break:clone;box-decoration-break:clone}.md-typeset ins.critic{background-color:var(--md-typeset-ins-color)}.md-typeset .critic.comment{-webkit-box-decoration-break:clone;box-decoration-break:clone;color:var(--md-code-hl-comment-color)}.md-typeset .critic.comment:before{content:"/* "}.md-typeset .critic.comment:after{content:" */"}.md-typeset .critic.block{box-shadow:none;display:block;margin:1em 0;overflow:auto;padding-left:.8rem;padding-right:.8rem}.md-typeset .critic.block>:first-child{margin-top:.5em}.md-typeset .critic.block>:last-child{margin-bottom:.5em}:root{--md-details-icon:url('data:image/svg+xml;charset=utf-8,')}.md-typeset details{display:flow-root;overflow:visible;padding-top:0}.md-typeset details[open]>summary:after{transform:rotate(90deg)}.md-typeset details:not([open]){box-shadow:none;padding-bottom:0}.md-typeset details:not([open])>summary{border-radius:.1rem}[dir=ltr] .md-typeset summary{padding-right:1.8rem}[dir=rtl] .md-typeset summary{padding-left:1.8rem}[dir=ltr] .md-typeset summary{border-top-left-radius:.1rem}[dir=ltr] .md-typeset summary,[dir=rtl] .md-typeset summary{border-top-right-radius:.1rem}[dir=rtl] .md-typeset summary{border-top-left-radius:.1rem}.md-typeset summary{cursor:pointer;display:block;min-height:1rem}.md-typeset summary.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-typeset summary:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}[dir=ltr] .md-typeset summary:after{right:.4rem}[dir=rtl] .md-typeset summary:after{left:.4rem}.md-typeset summary:after{background-color:currentcolor;content:"";height:1rem;-webkit-mask-image:var(--md-details-icon);mask-image:var(--md-details-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.625em;transform:rotate(0deg);transition:transform .25s;width:1rem}[dir=rtl] .md-typeset summary:after{transform:rotate(180deg)}.md-typeset summary::marker{display:none}.md-typeset summary::-webkit-details-marker{display:none}.md-typeset .emojione,.md-typeset .gemoji,.md-typeset .twemoji{display:inline-flex;height:1.125em;vertical-align:text-top}.md-typeset .emojione svg,.md-typeset .gemoji svg,.md-typeset .twemoji svg{fill:currentcolor;max-height:100%;width:1.125em}.highlight .o,.highlight .ow{color:var(--md-code-hl-operator-color)}.highlight .p{color:var(--md-code-hl-punctuation-color)}.highlight .cpf,.highlight .l,.highlight .s,.highlight .s1,.highlight .s2,.highlight .sb,.highlight .sc,.highlight .si,.highlight .ss{color:var(--md-code-hl-string-color)}.highlight .cp,.highlight .se,.highlight .sh,.highlight .sr,.highlight .sx{color:var(--md-code-hl-special-color)}.highlight .il,.highlight .m,.highlight .mb,.highlight .mf,.highlight .mh,.highlight .mi,.highlight .mo{color:var(--md-code-hl-number-color)}.highlight .k,.highlight .kd,.highlight .kn,.highlight .kp,.highlight .kr,.highlight .kt{color:var(--md-code-hl-keyword-color)}.highlight .kc,.highlight .n{color:var(--md-code-hl-name-color)}.highlight .bp,.highlight .nb,.highlight .no{color:var(--md-code-hl-constant-color)}.highlight .nc,.highlight .ne,.highlight .nf,.highlight .nn{color:var(--md-code-hl-function-color)}.highlight .nd,.highlight .ni,.highlight .nl,.highlight .nt{color:var(--md-code-hl-keyword-color)}.highlight .c,.highlight .c1,.highlight .ch,.highlight .cm,.highlight .cs,.highlight .sd{color:var(--md-code-hl-comment-color)}.highlight .na,.highlight .nv,.highlight .vc,.highlight .vg,.highlight .vi{color:var(--md-code-hl-variable-color)}.highlight .ge,.highlight .gh,.highlight .go,.highlight .gp,.highlight .gr,.highlight .gs,.highlight .gt,.highlight .gu{color:var(--md-code-hl-generic-color)}.highlight .gd,.highlight .gi{border-radius:.1rem;margin:0 -.125em;padding:0 .125em}.highlight .gd{background-color:var(--md-typeset-del-color)}.highlight .gi{background-color:var(--md-typeset-ins-color)}.highlight .hll{background-color:var(--md-code-hl-color--light);box-shadow:2px 0 0 0 var(--md-code-hl-color) inset;display:block;margin:0 -1.1764705882em;padding:0 1.1764705882em}.highlight span.filename{background-color:var(--md-code-bg-color);border-bottom:.05rem solid var(--md-default-fg-color--lightest);border-top-left-radius:.1rem;border-top-right-radius:.1rem;display:flow-root;font-size:.85em;font-weight:700;margin-top:1em;padding:.6617647059em 1.1764705882em;position:relative}.highlight span.filename+pre{margin-top:0}.highlight span.filename+pre>code{border-top-left-radius:0;border-top-right-radius:0}.highlight [data-linenos]:before{background-color:var(--md-code-bg-color);box-shadow:-.05rem 0 var(--md-default-fg-color--lightest) inset;color:var(--md-default-fg-color--light);content:attr(data-linenos);float:left;left:-1.1764705882em;margin-left:-1.1764705882em;margin-right:1.1764705882em;padding-left:1.1764705882em;position:sticky;-webkit-user-select:none;user-select:none;z-index:3}.highlight code a[id]{position:absolute;visibility:hidden}.highlight code[data-md-copying] .hll{display:contents}.highlight code[data-md-copying] .md-annotation{display:none}.highlighttable{display:flow-root}.highlighttable tbody,.highlighttable td{display:block;padding:0}.highlighttable tr{display:flex}.highlighttable pre{margin:0}.highlighttable th.filename{flex-grow:1;padding:0;text-align:left}.highlighttable th.filename span.filename{margin-top:0}.highlighttable .linenos{background-color:var(--md-code-bg-color);border-bottom-left-radius:.1rem;border-top-left-radius:.1rem;font-size:.85em;padding:.7720588235em 0 .7720588235em 1.1764705882em;-webkit-user-select:none;user-select:none}.highlighttable .linenodiv{box-shadow:-.05rem 0 var(--md-default-fg-color--lightest) inset;padding-right:.5882352941em}.highlighttable .linenodiv pre{color:var(--md-default-fg-color--light);text-align:right}.highlighttable .code{flex:1;min-width:0}.linenodiv a{color:inherit}.md-typeset .highlighttable{direction:ltr;margin:1em 0}.md-typeset .highlighttable>tbody>tr>.code>div>pre>code{border-bottom-left-radius:0;border-top-left-radius:0}.md-typeset .highlight+.result{border:.05rem solid var(--md-code-bg-color);border-bottom-left-radius:.1rem;border-bottom-right-radius:.1rem;border-top-width:.1rem;margin-top:-1.125em;overflow:visible;padding:0 1em}.md-typeset .highlight+.result:after{clear:both;content:"";display:block}@media screen and (max-width:44.984375em){.md-content__inner>.highlight{margin:1em -.8rem}.md-content__inner>.highlight>.filename,.md-content__inner>.highlight>.highlighttable>tbody>tr>.code>div>pre>code,.md-content__inner>.highlight>.highlighttable>tbody>tr>.filename span.filename,.md-content__inner>.highlight>.highlighttable>tbody>tr>.linenos,.md-content__inner>.highlight>pre>code{border-radius:0}.md-content__inner>.highlight+.result{border-left-width:0;border-radius:0;border-right-width:0;margin-left:-.8rem;margin-right:-.8rem}}.md-typeset .keys kbd:after,.md-typeset .keys kbd:before{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;color:inherit;margin:0;position:relative}.md-typeset .keys span{color:var(--md-default-fg-color--light);padding:0 .2em}.md-typeset .keys .key-alt:before,.md-typeset .keys .key-left-alt:before,.md-typeset .keys .key-right-alt:before{content:"⎇";padding-right:.4em}.md-typeset .keys .key-command:before,.md-typeset .keys .key-left-command:before,.md-typeset .keys .key-right-command:before{content:"⌘";padding-right:.4em}.md-typeset .keys .key-control:before,.md-typeset .keys .key-left-control:before,.md-typeset .keys .key-right-control:before{content:"⌃";padding-right:.4em}.md-typeset .keys .key-left-meta:before,.md-typeset .keys .key-meta:before,.md-typeset .keys .key-right-meta:before{content:"◆";padding-right:.4em}.md-typeset .keys .key-left-option:before,.md-typeset .keys .key-option:before,.md-typeset .keys .key-right-option:before{content:"⌥";padding-right:.4em}.md-typeset .keys .key-left-shift:before,.md-typeset .keys .key-right-shift:before,.md-typeset .keys .key-shift:before{content:"⇧";padding-right:.4em}.md-typeset .keys .key-left-super:before,.md-typeset .keys .key-right-super:before,.md-typeset .keys .key-super:before{content:"❖";padding-right:.4em}.md-typeset .keys .key-left-windows:before,.md-typeset .keys .key-right-windows:before,.md-typeset .keys .key-windows:before{content:"⊞";padding-right:.4em}.md-typeset .keys .key-arrow-down:before{content:"↓";padding-right:.4em}.md-typeset .keys .key-arrow-left:before{content:"←";padding-right:.4em}.md-typeset .keys .key-arrow-right:before{content:"→";padding-right:.4em}.md-typeset .keys .key-arrow-up:before{content:"↑";padding-right:.4em}.md-typeset .keys .key-backspace:before{content:"⌫";padding-right:.4em}.md-typeset .keys .key-backtab:before{content:"⇤";padding-right:.4em}.md-typeset .keys .key-caps-lock:before{content:"⇪";padding-right:.4em}.md-typeset .keys .key-clear:before{content:"⌧";padding-right:.4em}.md-typeset .keys .key-context-menu:before{content:"☰";padding-right:.4em}.md-typeset .keys .key-delete:before{content:"⌦";padding-right:.4em}.md-typeset .keys .key-eject:before{content:"⏏";padding-right:.4em}.md-typeset .keys .key-end:before{content:"⤓";padding-right:.4em}.md-typeset .keys .key-escape:before{content:"⎋";padding-right:.4em}.md-typeset .keys .key-home:before{content:"⤒";padding-right:.4em}.md-typeset .keys .key-insert:before{content:"⎀";padding-right:.4em}.md-typeset .keys .key-page-down:before{content:"⇟";padding-right:.4em}.md-typeset .keys .key-page-up:before{content:"⇞";padding-right:.4em}.md-typeset .keys .key-print-screen:before{content:"⎙";padding-right:.4em}.md-typeset .keys .key-tab:after{content:"⇥";padding-left:.4em}.md-typeset .keys .key-num-enter:after{content:"⌤";padding-left:.4em}.md-typeset .keys .key-enter:after{content:"⏎";padding-left:.4em}:root{--md-tabbed-icon--prev:url('data:image/svg+xml;charset=utf-8,');--md-tabbed-icon--next:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .tabbed-set{border-radius:.1rem;display:flex;flex-flow:column wrap;margin:1em 0;position:relative}.md-typeset .tabbed-set>input{height:0;opacity:0;position:absolute;width:0}.md-typeset .tabbed-set>input:target{--md-scroll-offset:0.625em}.md-typeset .tabbed-set>input.focus-visible~.tabbed-labels:before{background-color:var(--md-accent-fg-color)}.md-typeset .tabbed-labels{-ms-overflow-style:none;box-shadow:0 -.05rem var(--md-default-fg-color--lightest) inset;display:flex;max-width:100%;overflow:auto;scrollbar-width:none}@media print{.md-typeset .tabbed-labels{display:contents}}@media screen{.js .md-typeset .tabbed-labels{position:relative}.js .md-typeset .tabbed-labels:before{background:var(--md-default-fg-color);bottom:0;content:"";display:block;height:2px;left:0;position:absolute;transform:translateX(var(--md-indicator-x));transition:width 225ms,background-color .25s,transform .25s;transition-timing-function:cubic-bezier(.4,0,.2,1);width:var(--md-indicator-width)}}.md-typeset .tabbed-labels::-webkit-scrollbar{display:none}.md-typeset .tabbed-labels>label{border-bottom:.1rem solid #0000;border-radius:.1rem .1rem 0 0;color:var(--md-default-fg-color--light);cursor:pointer;flex-shrink:0;font-size:.64rem;font-weight:700;padding:.78125em 1.25em .625em;scroll-margin-inline-start:1rem;transition:background-color .25s,color .25s;white-space:nowrap;width:auto}@media print{.md-typeset .tabbed-labels>label:first-child{order:1}.md-typeset .tabbed-labels>label:nth-child(2){order:2}.md-typeset .tabbed-labels>label:nth-child(3){order:3}.md-typeset .tabbed-labels>label:nth-child(4){order:4}.md-typeset .tabbed-labels>label:nth-child(5){order:5}.md-typeset .tabbed-labels>label:nth-child(6){order:6}.md-typeset .tabbed-labels>label:nth-child(7){order:7}.md-typeset .tabbed-labels>label:nth-child(8){order:8}.md-typeset .tabbed-labels>label:nth-child(9){order:9}.md-typeset .tabbed-labels>label:nth-child(10){order:10}.md-typeset .tabbed-labels>label:nth-child(11){order:11}.md-typeset .tabbed-labels>label:nth-child(12){order:12}.md-typeset .tabbed-labels>label:nth-child(13){order:13}.md-typeset .tabbed-labels>label:nth-child(14){order:14}.md-typeset .tabbed-labels>label:nth-child(15){order:15}.md-typeset .tabbed-labels>label:nth-child(16){order:16}.md-typeset .tabbed-labels>label:nth-child(17){order:17}.md-typeset .tabbed-labels>label:nth-child(18){order:18}.md-typeset .tabbed-labels>label:nth-child(19){order:19}.md-typeset .tabbed-labels>label:nth-child(20){order:20}}.md-typeset .tabbed-labels>label:hover{color:var(--md-default-fg-color)}.md-typeset .tabbed-content{width:100%}@media print{.md-typeset .tabbed-content{display:contents}}.md-typeset .tabbed-block{display:none}@media print{.md-typeset .tabbed-block{display:block}.md-typeset .tabbed-block:first-child{order:1}.md-typeset .tabbed-block:nth-child(2){order:2}.md-typeset .tabbed-block:nth-child(3){order:3}.md-typeset .tabbed-block:nth-child(4){order:4}.md-typeset .tabbed-block:nth-child(5){order:5}.md-typeset .tabbed-block:nth-child(6){order:6}.md-typeset .tabbed-block:nth-child(7){order:7}.md-typeset .tabbed-block:nth-child(8){order:8}.md-typeset .tabbed-block:nth-child(9){order:9}.md-typeset .tabbed-block:nth-child(10){order:10}.md-typeset .tabbed-block:nth-child(11){order:11}.md-typeset .tabbed-block:nth-child(12){order:12}.md-typeset .tabbed-block:nth-child(13){order:13}.md-typeset .tabbed-block:nth-child(14){order:14}.md-typeset .tabbed-block:nth-child(15){order:15}.md-typeset .tabbed-block:nth-child(16){order:16}.md-typeset .tabbed-block:nth-child(17){order:17}.md-typeset .tabbed-block:nth-child(18){order:18}.md-typeset .tabbed-block:nth-child(19){order:19}.md-typeset .tabbed-block:nth-child(20){order:20}}.md-typeset .tabbed-block>.highlight:first-child>pre,.md-typeset .tabbed-block>pre:first-child{margin:0}.md-typeset .tabbed-block>.highlight:first-child>pre>code,.md-typeset .tabbed-block>pre:first-child>code{border-top-left-radius:0;border-top-right-radius:0}.md-typeset .tabbed-block>.highlight:first-child>.filename{border-top-left-radius:0;border-top-right-radius:0;margin:0}.md-typeset .tabbed-block>.highlight:first-child>.highlighttable{margin:0}.md-typeset .tabbed-block>.highlight:first-child>.highlighttable>tbody>tr>.filename span.filename,.md-typeset .tabbed-block>.highlight:first-child>.highlighttable>tbody>tr>.linenos{border-top-left-radius:0;border-top-right-radius:0;margin:0}.md-typeset .tabbed-block>.highlight:first-child>.highlighttable>tbody>tr>.code>div>pre>code{border-top-left-radius:0;border-top-right-radius:0}.md-typeset .tabbed-block>.highlight:first-child+.result{margin-top:-.125em}.md-typeset .tabbed-block>.tabbed-set{margin:0}.md-typeset .tabbed-button{align-self:center;border-radius:100%;color:var(--md-default-fg-color--light);cursor:pointer;display:block;height:.9rem;margin-top:.1rem;pointer-events:auto;transition:background-color .25s;width:.9rem}.md-typeset .tabbed-button:hover{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-typeset .tabbed-button:after{background-color:currentcolor;content:"";display:block;height:100%;-webkit-mask-image:var(--md-tabbed-icon--prev);mask-image:var(--md-tabbed-icon--prev);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color .25s,transform .25s;width:100%}.md-typeset .tabbed-control{background:linear-gradient(to right,var(--md-default-bg-color) 60%,#0000);display:flex;height:1.9rem;justify-content:start;pointer-events:none;position:absolute;transition:opacity 125ms;width:1.2rem}[dir=rtl] .md-typeset .tabbed-control{transform:rotate(180deg)}.md-typeset .tabbed-control[hidden]{opacity:0}.md-typeset .tabbed-control--next{background:linear-gradient(to left,var(--md-default-bg-color) 60%,#0000);justify-content:end;right:0}.md-typeset .tabbed-control--next .tabbed-button:after{-webkit-mask-image:var(--md-tabbed-icon--next);mask-image:var(--md-tabbed-icon--next)}@media screen and (max-width:44.984375em){[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels{padding-left:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels{padding-right:.8rem}.md-content__inner>.tabbed-set .tabbed-labels{margin:0 -.8rem;max-width:100vw;scroll-padding-inline-start:.8rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels:after{padding-right:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels:after{padding-left:.8rem}.md-content__inner>.tabbed-set .tabbed-labels:after{content:""}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{padding-left:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{padding-right:.8rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{margin-left:-.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{margin-right:-.8rem}.md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{width:2rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{padding-right:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{padding-left:.8rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{margin-right:-.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{margin-left:-.8rem}.md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{width:2rem}}@media screen{.md-typeset .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9){color:var(--md-default-fg-color)}.md-typeset .no-js .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.md-typeset .no-js .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.md-typeset .no-js .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.md-typeset .no-js .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.md-typeset .no-js .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.md-typeset .no-js .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.md-typeset .no-js .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.md-typeset .no-js .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.md-typeset .no-js .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.md-typeset .no-js .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.md-typeset .no-js .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.md-typeset .no-js .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.md-typeset .no-js .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.md-typeset .no-js .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.md-typeset .no-js .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.md-typeset .no-js .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.md-typeset .no-js .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.md-typeset .no-js .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.md-typeset .no-js .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.md-typeset .no-js .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9),.no-js .md-typeset .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.no-js .md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.no-js .md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.no-js .md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.no-js .md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.no-js .md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.no-js .md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.no-js .md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.no-js .md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.no-js .md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.no-js .md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.no-js .md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.no-js .md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.no-js .md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.no-js .md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.no-js .md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.no-js .md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.no-js .md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.no-js .md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.no-js .md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9){border-color:var(--md-default-fg-color)}}.md-typeset .tabbed-set>input:first-child.focus-visible~.tabbed-labels>:first-child,.md-typeset .tabbed-set>input:nth-child(10).focus-visible~.tabbed-labels>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11).focus-visible~.tabbed-labels>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12).focus-visible~.tabbed-labels>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13).focus-visible~.tabbed-labels>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14).focus-visible~.tabbed-labels>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15).focus-visible~.tabbed-labels>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16).focus-visible~.tabbed-labels>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17).focus-visible~.tabbed-labels>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18).focus-visible~.tabbed-labels>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19).focus-visible~.tabbed-labels>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2).focus-visible~.tabbed-labels>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20).focus-visible~.tabbed-labels>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3).focus-visible~.tabbed-labels>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4).focus-visible~.tabbed-labels>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5).focus-visible~.tabbed-labels>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6).focus-visible~.tabbed-labels>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7).focus-visible~.tabbed-labels>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8).focus-visible~.tabbed-labels>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9).focus-visible~.tabbed-labels>:nth-child(9){color:var(--md-accent-fg-color)}.md-typeset .tabbed-set>input:first-child:checked~.tabbed-content>:first-child,.md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-content>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-content>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-content>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-content>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-content>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-content>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-content>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-content>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-content>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-content>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-content>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-content>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-content>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-content>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-content>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-content>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-content>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-content>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-content>:nth-child(9){display:block}:root{--md-tasklist-icon:url('data:image/svg+xml;charset=utf-8,');--md-tasklist-icon--checked:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .task-list-item{list-style-type:none;position:relative}[dir=ltr] .md-typeset .task-list-item [type=checkbox]{left:-2em}[dir=rtl] .md-typeset .task-list-item [type=checkbox]{right:-2em}.md-typeset .task-list-item [type=checkbox]{position:absolute;top:.45em}.md-typeset .task-list-control [type=checkbox]{opacity:0;z-index:-1}[dir=ltr] .md-typeset .task-list-indicator:before{left:-1.5em}[dir=rtl] .md-typeset .task-list-indicator:before{right:-1.5em}.md-typeset .task-list-indicator:before{background-color:var(--md-default-fg-color--lightest);content:"";height:1.25em;-webkit-mask-image:var(--md-tasklist-icon);mask-image:var(--md-tasklist-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.15em;width:1.25em}.md-typeset [type=checkbox]:checked+.task-list-indicator:before{background-color:#00e676;-webkit-mask-image:var(--md-tasklist-icon--checked);mask-image:var(--md-tasklist-icon--checked)}:root>*{--md-mermaid-font-family:var(--md-text-font-family),sans-serif;--md-mermaid-edge-color:var(--md-code-fg-color);--md-mermaid-node-bg-color:var(--md-accent-fg-color--transparent);--md-mermaid-node-fg-color:var(--md-accent-fg-color);--md-mermaid-label-bg-color:var(--md-default-bg-color);--md-mermaid-label-fg-color:var(--md-code-fg-color);--md-mermaid-sequence-actor-bg-color:var(--md-mermaid-label-bg-color);--md-mermaid-sequence-actor-fg-color:var(--md-mermaid-label-fg-color);--md-mermaid-sequence-actor-border-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-actor-line-color:var(--md-default-fg-color--lighter);--md-mermaid-sequence-actorman-bg-color:var(--md-mermaid-label-bg-color);--md-mermaid-sequence-actorman-line-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-box-bg-color:var(--md-mermaid-node-bg-color);--md-mermaid-sequence-box-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-label-bg-color:var(--md-mermaid-node-bg-color);--md-mermaid-sequence-label-fg-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-loop-bg-color:var(--md-mermaid-node-bg-color);--md-mermaid-sequence-loop-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-loop-border-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-message-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-message-line-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-note-bg-color:var(--md-mermaid-label-bg-color);--md-mermaid-sequence-note-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-note-border-color:var(--md-mermaid-label-fg-color);--md-mermaid-sequence-number-bg-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-number-fg-color:var(--md-accent-bg-color)}.mermaid{line-height:normal;margin:1em 0}@media screen and (min-width:45em){[dir=ltr] .md-typeset .inline{float:left}[dir=rtl] .md-typeset .inline{float:right}[dir=ltr] .md-typeset .inline{margin-right:.8rem}[dir=rtl] .md-typeset .inline{margin-left:.8rem}.md-typeset .inline{margin-bottom:.8rem;margin-top:0;width:11.7rem}[dir=ltr] .md-typeset .inline.end{float:right}[dir=rtl] .md-typeset .inline.end{float:left}[dir=ltr] .md-typeset .inline.end{margin-left:.8rem;margin-right:0}[dir=rtl] .md-typeset .inline.end{margin-left:0;margin-right:.8rem}} \ No newline at end of file diff --git a/assets/stylesheets/main.35e1ed30.min.css.map b/assets/stylesheets/main.35e1ed30.min.css.map new file mode 100644 index 00000000..7612577e --- /dev/null +++ b/assets/stylesheets/main.35e1ed30.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["src/templates/assets/stylesheets/main/components/_meta.scss","../../../../src/templates/assets/stylesheets/main.scss","src/templates/assets/stylesheets/main/_resets.scss","src/templates/assets/stylesheets/main/_colors.scss","src/templates/assets/stylesheets/main/_icons.scss","src/templates/assets/stylesheets/main/_typeset.scss","src/templates/assets/stylesheets/utilities/_break.scss","src/templates/assets/stylesheets/main/components/_author.scss","src/templates/assets/stylesheets/main/components/_banner.scss","src/templates/assets/stylesheets/main/components/_base.scss","src/templates/assets/stylesheets/main/components/_clipboard.scss","src/templates/assets/stylesheets/main/components/_consent.scss","src/templates/assets/stylesheets/main/components/_content.scss","src/templates/assets/stylesheets/main/components/_dialog.scss","src/templates/assets/stylesheets/main/components/_feedback.scss","src/templates/assets/stylesheets/main/components/_footer.scss","src/templates/assets/stylesheets/main/components/_form.scss","src/templates/assets/stylesheets/main/components/_header.scss","node_modules/material-design-color/material-color.scss","src/templates/assets/stylesheets/main/components/_nav.scss","src/templates/assets/stylesheets/main/components/_pagination.scss","src/templates/assets/stylesheets/main/components/_post.scss","src/templates/assets/stylesheets/main/components/_progress.scss","src/templates/assets/stylesheets/main/components/_search.scss","src/templates/assets/stylesheets/main/components/_select.scss","src/templates/assets/stylesheets/main/components/_sidebar.scss","src/templates/assets/stylesheets/main/components/_source.scss","src/templates/assets/stylesheets/main/components/_status.scss","src/templates/assets/stylesheets/main/components/_tabs.scss","src/templates/assets/stylesheets/main/components/_tag.scss","src/templates/assets/stylesheets/main/components/_tooltip.scss","src/templates/assets/stylesheets/main/components/_top.scss","src/templates/assets/stylesheets/main/components/_version.scss","src/templates/assets/stylesheets/main/extensions/markdown/_admonition.scss","src/templates/assets/stylesheets/main/extensions/markdown/_footnotes.scss","src/templates/assets/stylesheets/main/extensions/markdown/_toc.scss","src/templates/assets/stylesheets/main/extensions/pymdownx/_arithmatex.scss","src/templates/assets/stylesheets/main/extensions/pymdownx/_critic.scss","src/templates/assets/stylesheets/main/extensions/pymdownx/_details.scss","src/templates/assets/stylesheets/main/extensions/pymdownx/_emoji.scss","src/templates/assets/stylesheets/main/extensions/pymdownx/_highlight.scss","src/templates/assets/stylesheets/main/extensions/pymdownx/_keys.scss","src/templates/assets/stylesheets/main/extensions/pymdownx/_tabbed.scss","src/templates/assets/stylesheets/main/extensions/pymdownx/_tasklist.scss","src/templates/assets/stylesheets/main/integrations/_mermaid.scss","src/templates/assets/stylesheets/main/_modifiers.scss"],"names":[],"mappings":"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gC5vGI,kFACE,ahC8vGN,CgCnxGE,4DACE,oBhCsxGJ,CgCnxGI,sFACE,gChCqxGN,CgChxGE,oEACE,0BhCmxGJ,CgChxGI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ChCkxGN,CgC9wGI,gFACE,ahCgxGN,CgCryGE,4DACE,oBhCwyGJ,CgCryGI,sFACE,gChCuyGN,CgClyGE,oEACE,0BhCqyGJ,CgClyGI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ChCoyGN,CgChyGI,gFACE,ahCkyGN,CgCvzGE,0DACE,oBhC0zGJ,CgCvzGI,oFACE,gChCyzGN,CgCpzGE,kEACE,0BhCuzGJ,CgCpzGI,gFACE,wBAlBG,CAmBH,oDAAA,CAAA,4ChCszGN,CgClzGI,8EACE,ahCozGN,CgCz0GE,oDACE,oBhC40GJ,CgCz0GI,8EACE,gChC20GN,CgCt0GE,4DACE,0BhCy0GJ,CgCt0GI,0EACE,wBAlBG,CAmBH,iDAAA,CAAA,yChCw0GN,CgCp0GI,wEACE,ahCs0GN,CgC31GE,4DACE,oBhC81GJ,CgC31GI,sFACE,gChC61GN,CgCx1GE,oEACE,0BhC21GJ,CgCx1GI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ChC01GN,CgCt1GI,gFACE,ahCw1GN,CgC72GE,wDACE,oBhCg3GJ,CgC72GI,kFACE,gChC+2GN,CgC12GE,gEACE,0BhC62GJ,CgC12GI,8EACE,wBAlBG,CAmBH,mDAAA,CAAA,2ChC42GN,CgCx2GI,4EACE,ahC02GN,CiC9gHA,MACE,wMjCihHF,CiCxgHE,sBAEE,uCAAA,CADA,gBjC4gHJ,CiCxgHI,mCACE,ajC0gHN,CiC3gHI,mCACE,cjC0gHN,CiCtgHM,4BACE,sBjCwgHR,CiCrgHQ,mCACE,gCjCugHV,CiCngHQ,2DACE,SAAA,CAEA,uBAAA,CADA,ejCsgHV,CiCjgHQ,yGACE,SAAA,CACA,uBjCmgHV,CiC//GQ,yCACE,YjCigHV,CiC1/GE,0BACE,eAAA,CACA,ejC4/GJ,CiCz/GI,+BACE,oBjC2/GN,CiCt/GE,gDACE,YjCw/GJ,CiCp/GE,8BAIE,+BAAA,CAHA,oBAAA,CAEA,WAAA,CAGA,SAAA,CAKA,4BAAA,CAJA,4DACE,CAHF,0BjCw/GJ,CiC/+GI,aAdF,8BAeI,+BAAA,CACA,SAAA,CACA,uBjCk/GJ,CACF,CiC/+GI,wCACE,6BjCi/GN,CiC7+GI,oCACE,+BjC++GN,CiC3+GI,qCAKE,6BAAA,CADA,UAAA,CAHA,oBAAA,CAEA,YAAA,CAGA,2CAAA,CAAA,mCAAA,CACA,4BAAA,CAAA,oBAAA,CACA,6BAAA,CAAA,qBAAA,CACA,yBAAA,CAAA,iBAAA,CAPA,WjCo/GN,CiCv+GQ,mDACE,oBjCy+GV,CkCvlHE,kCAEE,iBlC6lHJ,CkC/lHE,kCAEE,kBlC6lHJ,CkC/lHE,wBAGE,yCAAA,CAFA,oBAAA,CAGA,SAAA,CACA,mClC0lHJ,CkCrlHI,aAVF,wBAWI,YlCwlHJ,CACF,CkCplHE,6FAEE,SAAA,CACA,mClCslHJ,CkChlHE,4FAEE,+BlCklHJ,CkC9kHE,oBACE,yBAAA,CACA,uBAAA,CAGA,yElC8kHJ,CK/8GI,sC6BrHE,qDACE,uBlCukHN,CACF,CkClkHE,kEACE,yBlCokHJ,CkChkHE,sBACE,0BlCkkHJ,CmC7nHE,2BACE,anCgoHJ,CK38GI,0C8BtLF,2BAKI,enCgoHJ,CACF,CmC7nHI,6BAGE,0BAAA,CAAA,2BAAA,CADA,eAAA,CAEA,iBAAA,CAHA,yBAAA,CAAA,iBnCkoHN,CmC5nHM,2CACE,kBnC8nHR,CoC/oHE,uBACE,4CpCmpHJ,CoC9oHE,8CAJE,kCAAA,CAAA,0BpCspHJ,CoClpHE,uBACE,4CpCipHJ,CoC5oHE,4BAEE,kCAAA,CAAA,0BAAA,CADA,qCpC+oHJ,CoC3oHI,mCACE,apC6oHN,CoCzoHI,kCACE,apC2oHN,CoCtoHE,0BAKE,eAAA,CAJA,aAAA,CAEA,YAAA,CACA,aAAA,CAFA,kBAAA,CAAA,mBpC2oHJ,CoCroHI,uCACE,epCuoHN,CoCnoHI,sCACE,kBpCqoHN,CqClrHA,MACE,8LrCqrHF,CqC5qHE,oBAGE,iBAAA,CAEA,gBAAA,CADA,arC8qHJ,CqC1qHI,wCACE,uBrC4qHN,CqCxqHI,gCAEE,eAAA,CADA,gBrC2qHN,CqCpqHM,wCACE,mBrCsqHR,CqChqHE,8BAKE,oBrCmqHJ,CqCxqHE,8BAKE,mBrCmqHJ,CqCxqHE,8BAOE,4BrCiqHJ,CqCxqHE,4DAQE,6BrCgqHJ,CqCxqHE,8BAQE,4BrCgqHJ,CqCxqHE,oBAME,cAAA,CAHA,aAAA,CACA,erCoqHJ,CqC7pHI,kCACE,uCAAA,CACA,oBrC+pHN,CqC3pHI,wCAEE,uCAAA,CADA,YrC8pHN,CqCzpHI,oCASE,WrC+pHN,CqCxqHI,oCASE,UrC+pHN,CqCxqHI,0BAME,6BAAA,CADA,UAAA,CADA,WAAA,CAMA,yCAAA,CAAA,iCAAA,CACA,4BAAA,CAAA,oBAAA,CACA,6BAAA,CAAA,qBAAA,CACA,yBAAA,CAAA,iBAAA,CAZA,iBAAA,CACA,UAAA,CAMA,sBAAA,CADA,yBAAA,CAJA,UrCqqHN,CqCxpHM,oCACE,wBrC0pHR,CqCrpHI,4BACE,YrCupHN,CqClpHI,4CACE,YrCopHN,CsC3uHE,+DACE,mBAAA,CACA,cAAA,CACA,uBtC8uHJ,CsC3uHI,2EAGE,iBAAA,CADA,eAAA,CADA,atC+uHN,CuCrvHE,6BACE,sCvCwvHJ,CuCrvHE,cACE,yCvCuvHJ,CuC3uHE,sIACE,oCvC6uHJ,CuCruHE,2EACE,qCvCuuHJ,CuC7tHE,wGACE,oCvC+tHJ,CuCttHE,yFACE,qCvCwtHJ,CuCntHE,6BACE,kCvCqtHJ,CuC/sHE,6CACE,sCvCitHJ,CuC1sHE,4DACE,sCvC4sHJ,CuCrsHE,4DACE,qCvCusHJ,CuC9rHE,yFACE,qCvCgsHJ,CuCxrHE,2EACE,sCvC0rHJ,CuC/qHE,wHACE,qCvCirHJ,CuC5qHE,8BAGE,mBAAA,CADA,gBAAA,CADA,gBvCgrHJ,CuC3qHE,eACE,4CvC6qHJ,CuC1qHE,eACE,4CvC4qHJ,CuCxqHE,gBAIE,+CAAA,CACA,kDAAA,CAJA,aAAA,CAEA,wBAAA,CADA,wBvC6qHJ,CuCtqHE,yBAOE,wCAAA,CACA,+DAAA,CACA,4BAAA,CACA,6BAAA,CARA,iBAAA,CAGA,eAAA,CACA,eAAA,CAFA,cAAA,CADA,oCAAA,CAFA,iBvCirHJ,CuCrqHI,6BACE,YvCuqHN,CuCpqHM,kCACE,wBAAA,CACA,yBvCsqHR,CuChqHE,iCAaE,wCAAA,CACA,+DAAA,CAJA,uCAAA,CACA,0BAAA,CALA,UAAA,CAJA,oBAAA,CAOA,2BAAA,CADA,2BAAA,CADA,2BAAA,CANA,eAAA,CAWA,wBAAA,CAAA,gBAAA,CAPA,SvCyqHJ,CuCvpHE,sBACE,iBAAA,CACA,iBvCypHJ,CuCjpHI,sCACE,gBvCmpHN,CuC/oHI,gDACE,YvCipHN,CuCvoHA,gBACE,iBvC0oHF,CuCtoHE,yCACE,aAAA,CACA,SvCwoHJ,CuCnoHE,mBACE,YvCqoHJ,CuChoHE,oBACE,QvCkoHJ,CuC9nHE,4BACE,WAAA,CACA,SAAA,CACA,evCgoHJ,CuC7nHI,0CACE,YvC+nHN,CuCznHE,yBAKE,wCAAA,CAEA,+BAAA,CADA,4BAAA,CAHA,eAAA,CADA,oDAAA,CAEA,wBAAA,CAAA,gBvC8nHJ,CuCvnHE,2BAEE,+DAAA,CADA,2BvC0nHJ,CuCtnHI,+BACE,uCAAA,CACA,gBvCwnHN,CuCnnHE,sBACE,MAAA,CACA,WvCqnHJ,CuChnHA,aACE,avCmnHF,CuCzmHE,4BAEE,aAAA,CADA,YvC6mHJ,CuCzmHI,wDAEE,2BAAA,CADA,wBvC4mHN,CuCtmHE,+BAKE,2CAAA,CAEA,+BAAA,CADA,gCAAA,CADA,sBAAA,CAHA,mBAAA,CACA,gBAAA,CAFA,avC8mHJ,CuCrmHI,qCAEE,UAAA,CACA,UAAA,CAFA,avCymHN,CK3uHI,0CkCiJF,8BACE,iBvC8lHF,CuCplHE,wSAGE,evC0lHJ,CuCtlHE,sCAEE,mBAAA,CACA,eAAA,CADA,oBAAA,CADA,kBAAA,CAAA,mBvC0lHJ,CACF,CwCl7HI,yDAIE,+BAAA,CACA,8BAAA,CAFA,aAAA,CADA,QAAA,CADA,iBxCw7HN,CwCh7HI,uBAEE,uCAAA,CADA,cxCm7HN,CwC93HM,iHAEE,WAlDkB,CAiDlB,kBxCy4HR,CwC14HM,6HAEE,WAlDkB,CAiDlB,kBxCq5HR,CwCt5HM,6HAEE,WAlDkB,CAiDlB,kBxCi6HR,CwCl6HM,oHAEE,WAlDkB,CAiDlB,kBxC66HR,CwC96HM,0HAEE,WAlDkB,CAiDlB,kBxCy7HR,CwC17HM,uHAEE,WAlDkB,CAiDlB,kBxCq8HR,CwCt8HM,uHAEE,WAlDkB,CAiDlB,kBxCi9HR,CwCl9HM,6HAEE,WAlDkB,CAiDlB,kBxC69HR,CwC99HM,yCAEE,WAlDkB,CAiDlB,kBxCi+HR,CwCl+HM,yCAEE,WAlDkB,CAiDlB,kBxCq+HR,CwCt+HM,0CAEE,WAlDkB,CAiDlB,kBxCy+HR,CwC1+HM,uCAEE,WAlDkB,CAiDlB,kBxC6+HR,CwC9+HM,wCAEE,WAlDkB,CAiDlB,kBxCi/HR,CwCl/HM,sCAEE,WAlDkB,CAiDlB,kBxCq/HR,CwCt/HM,wCAEE,WAlDkB,CAiDlB,kBxCy/HR,CwC1/HM,oCAEE,WAlDkB,CAiDlB,kBxC6/HR,CwC9/HM,2CAEE,WAlDkB,CAiDlB,kBxCigIR,CwClgIM,qCAEE,WAlDkB,CAiDlB,kBxCqgIR,CwCtgIM,oCAEE,WAlDkB,CAiDlB,kBxCygIR,CwC1gIM,kCAEE,WAlDkB,CAiDlB,kBxC6gIR,CwC9gIM,qCAEE,WAlDkB,CAiDlB,kBxCihIR,CwClhIM,mCAEE,WAlDkB,CAiDlB,kBxCqhIR,CwCthIM,qCAEE,WAlDkB,CAiDlB,kBxCyhIR,CwC1hIM,wCAEE,WAlDkB,CAiDlB,kBxC6hIR,CwC9hIM,sCAEE,WAlDkB,CAiDlB,kBxCiiIR,CwCliIM,2CAEE,WAlDkB,CAiDlB,kBxCqiIR,CwC1hIM,iCAEE,WAPkB,CAMlB,iBxC6hIR,CwC9hIM,uCAEE,WAPkB,CAMlB,iBxCiiIR,CwCliIM,mCAEE,WAPkB,CAMlB,iBxCqiIR,CyCvnIA,MACE,qMAAA,CACA,mMzC0nIF,CyCjnIE,wBAKE,mBAAA,CAHA,YAAA,CACA,qBAAA,CACA,YAAA,CAHA,iBzCwnIJ,CyC9mII,8BAGE,QAAA,CACA,SAAA,CAHA,iBAAA,CACA,OzCknIN,CyC7mIM,qCACE,0BzC+mIR,CyCllIM,kEACE,0CzColIR,CyC9kIE,2BAKE,uBAAA,CADA,+DAAA,CAHA,YAAA,CACA,cAAA,CACA,aAAA,CAGA,oBzCglIJ,CyC7kII,aATF,2BAUI,gBzCglIJ,CACF,CyC7kII,cAGE,+BACE,iBzC6kIN,CyC1kIM,sCAQE,qCAAA,CANA,QAAA,CAKA,UAAA,CAHA,aAAA,CAEA,UAAA,CAHA,MAAA,CAFA,iBAAA,CAaA,2CAAA,CALA,2DACE,CAGF,kDAAA,CARA,+BzCklIR,CACF,CyCpkII,8CACE,YzCskIN,CyClkII,iCASE,+BAAA,CACA,6BAAA,CAJA,uCAAA,CAEA,cAAA,CAPA,aAAA,CAGA,gBAAA,CACA,eAAA,CAFA,8BAAA,CAWA,+BAAA,CAHA,2CACE,CALF,kBAAA,CALA,UzC8kIN,CyC/jIM,aAII,6CACE,OzC8jIV,CyC/jIQ,8CACE,OzCikIV,CyClkIQ,8CACE,OzCokIV,CyCrkIQ,8CACE,OzCukIV,CyCxkIQ,8CACE,OzC0kIV,CyC3kIQ,8CACE,OzC6kIV,CyC9kIQ,8CACE,OzCglIV,CyCjlIQ,8CACE,OzCmlIV,CyCplIQ,8CACE,OzCslIV,CyCvlIQ,+CACE,QzCylIV,CyC1lIQ,+CACE,QzC4lIV,CyC7lIQ,+CACE,QzC+lIV,CyChmIQ,+CACE,QzCkmIV,CyCnmIQ,+CACE,QzCqmIV,CyCtmIQ,+CACE,QzCwmIV,CyCzmIQ,+CACE,QzC2mIV,CyC5mIQ,+CACE,QzC8mIV,CyC/mIQ,+CACE,QzCinIV,CyClnIQ,+CACE,QzConIV,CyCrnIQ,+CACE,QzCunIV,CACF,CyClnIM,uCACE,gCzConIR,CyC9mIE,4BACE,UzCgnIJ,CyC7mII,aAJF,4BAKI,gBzCgnIJ,CACF,CyC5mIE,0BACE,YzC8mIJ,CyC3mII,aAJF,0BAKI,azC8mIJ,CyC1mIM,sCACE,OzC4mIR,CyC7mIM,uCACE,OzC+mIR,CyChnIM,uCACE,OzCknIR,CyCnnIM,uCACE,OzCqnIR,CyCtnIM,uCACE,OzCwnIR,CyCznIM,uCACE,OzC2nIR,CyC5nIM,uCACE,OzC8nIR,CyC/nIM,uCACE,OzCioIR,CyCloIM,uCACE,OzCooIR,CyCroIM,wCACE,QzCuoIR,CyCxoIM,wCACE,QzC0oIR,CyC3oIM,wCACE,QzC6oIR,CyC9oIM,wCACE,QzCgpIR,CyCjpIM,wCACE,QzCmpIR,CyCppIM,wCACE,QzCspIR,CyCvpIM,wCACE,QzCypIR,CyC1pIM,wCACE,QzC4pIR,CyC7pIM,wCACE,QzC+pIR,CyChqIM,wCACE,QzCkqIR,CyCnqIM,wCACE,QzCqqIR,CACF,CyC/pII,+FAEE,QzCiqIN,CyC9pIM,yGACE,wBAAA,CACA,yBzCiqIR,CyCxpIM,2DAEE,wBAAA,CACA,yBAAA,CAFA,QzC4pIR,CyCrpIM,iEACE,QzCupIR,CyCppIQ,qLAGE,wBAAA,CACA,yBAAA,CAFA,QzCwpIV,CyClpIQ,6FACE,wBAAA,CACA,yBzCopIV,CyC/oIM,yDACE,kBzCipIR,CyC5oII,sCACE,QzC8oIN,CyCzoIE,2BAEE,iBAAA,CAOA,kBAAA,CAHA,uCAAA,CAEA,cAAA,CAPA,aAAA,CAGA,YAAA,CACA,gBAAA,CAEA,mBAAA,CAGA,gCAAA,CAPA,WzCkpIJ,CyCxoII,iCAEE,uDAAA,CADA,+BzC2oIN,CyCtoII,iCAKE,6BAAA,CADA,UAAA,CAHA,aAAA,CAEA,WAAA,CAMA,8CAAA,CAAA,sCAAA,CACA,4BAAA,CAAA,oBAAA,CACA,6BAAA,CAAA,qBAAA,CACA,yBAAA,CAAA,iBAAA,CANA,+CACE,CALF,UzCgpIN,CyCjoIE,4BAOE,yEACE,CANF,YAAA,CAGA,aAAA,CAFA,qBAAA,CAGA,mBAAA,CALA,iBAAA,CAYA,wBAAA,CATA,YzCuoIJ,CyC3nII,sCACE,wBzC6nIN,CyCznII,oCACE,SzC2nIN,CyCvnII,kCAGE,wEACE,CAFF,mBAAA,CADA,OzC2nIN,CyCjnIM,uDACE,8CAAA,CAAA,sCzCmnIR,CKzuII,0CoCoIF,wDAEE,kBzC2mIF,CyC7mIA,wDAEE,mBzC2mIF,CyC7mIA,8CAGE,eAAA,CAFA,eAAA,CAGA,iCzCymIF,CyCrmIE,8DACE,mBzCwmIJ,CyCzmIE,8DACE,kBzCwmIJ,CyCzmIE,oDAEE,UzCumIJ,CyCnmIE,8EAEE,kBzCsmIJ,CyCxmIE,8EAEE,mBzCsmIJ,CyCxmIE,8EAGE,kBzCqmIJ,CyCxmIE,8EAGE,mBzCqmIJ,CyCxmIE,oEACE,UzCumIJ,CyCjmIE,8EAEE,mBzComIJ,CyCtmIE,8EAEE,kBzComIJ,CyCtmIE,8EAGE,mBzCmmIJ,CyCtmIE,8EAGE,kBzCmmIJ,CyCtmIE,oEACE,UzCqmIJ,CACF,CyCvlIE,cAHF,olDAII,gCzC0lIF,CyCvlIE,g8GACE,uCzCylIJ,CACF,CyCplIA,4sDACE,+BzCulIF,CyCnlIA,wmDACE,azCslIF,C0Cz8IA,MACE,8WAAA,CACA,uX1C48IF,C0Cn8IE,4BAEE,oBAAA,CADA,iB1Cu8IJ,C0Cl8II,sDAGE,S1Co8IN,C0Cv8II,sDAGE,U1Co8IN,C0Cv8II,4CACE,iBAAA,CACA,S1Cq8IN,C0C/7IE,+CAEE,SAAA,CADA,U1Ck8IJ,C0C77IE,kDAOE,W1Cm8IJ,C0C18IE,kDAOE,Y1Cm8IJ,C0C18IE,wCAME,qDAAA,CADA,UAAA,CADA,aAAA,CAIA,0CAAA,CAAA,kCAAA,CACA,4BAAA,CAAA,oBAAA,CACA,6BAAA,CAAA,qBAAA,CACA,yBAAA,CAAA,iBAAA,CAVA,iBAAA,CACA,SAAA,CACA,Y1Cu8IJ,C0C37IE,gEACE,wBzB2Wa,CyB1Wb,mDAAA,CAAA,2C1C67IJ,C2C7+IA,QACE,8DAAA,CAGA,+CAAA,CACA,iEAAA,CACA,oDAAA,CACA,sDAAA,CACA,mDAAA,CAGA,qEAAA,CACA,qEAAA,CACA,wEAAA,CACA,0EAAA,CACA,wEAAA,CACA,yEAAA,CACA,kEAAA,CACA,+DAAA,CACA,oEAAA,CACA,oEAAA,CACA,mEAAA,CACA,gEAAA,CACA,uEAAA,CACA,mEAAA,CACA,qEAAA,CACA,oEAAA,CACA,gEAAA,CACA,wEAAA,CACA,qEAAA,CACA,+D3C4+IF,C2Ct+IA,SAEE,kBAAA,CADA,Y3C0+IF,CKz2II,mCuChKA,8BACE,U5CihJJ,C4ClhJE,8BACE,W5CihJJ,C4ClhJE,8BAGE,kB5C+gJJ,C4ClhJE,8BAGE,iB5C+gJJ,C4ClhJE,oBAKE,mBAAA,CADA,YAAA,CAFA,a5CghJJ,C4C1gJI,kCACE,W5C6gJN,C4C9gJI,kCACE,U5C6gJN,C4C9gJI,kCAEE,iBAAA,CAAA,c5C4gJN,C4C9gJI,kCAEE,aAAA,CAAA,kB5C4gJN,CACF","file":"main.css"} \ No newline at end of file diff --git a/assets/stylesheets/main.6a10b989.min.css b/assets/stylesheets/main.6a10b989.min.css deleted file mode 100644 index a794f345..00000000 --- a/assets/stylesheets/main.6a10b989.min.css +++ /dev/null @@ -1 +0,0 @@ -@charset "UTF-8";html{-webkit-text-size-adjust:none;-moz-text-size-adjust:none;text-size-adjust:none;box-sizing:border-box}*,:after,:before{box-sizing:inherit}@media (prefers-reduced-motion){*,:after,:before{transition:none!important}}body{margin:0}a,button,input,label{-webkit-tap-highlight-color:transparent}a{color:inherit;text-decoration:none}hr{border:0;box-sizing:initial;display:block;height:.05rem;overflow:visible;padding:0}small{font-size:80%}sub,sup{line-height:1em}img{border-style:none}table{border-collapse:initial;border-spacing:0}td,th{font-weight:400;vertical-align:top}button{background:#0000;border:0;font-family:inherit;font-size:inherit;margin:0;padding:0}input{border:0;outline:none}:root{--md-primary-fg-color:#4051b5;--md-primary-fg-color--light:#5d6cc0;--md-primary-fg-color--dark:#303fa1;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3;--md-accent-fg-color:#526cfe;--md-accent-fg-color--transparent:#526cfe1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-scheme=default]{color-scheme:light}[data-md-color-scheme=default] img[src$="#gh-dark-mode-only"],[data-md-color-scheme=default] img[src$="#only-dark"]{display:none}:root,[data-md-color-scheme=default]{--md-hue:225deg;--md-default-fg-color:#000000de;--md-default-fg-color--light:#0000008a;--md-default-fg-color--lighter:#00000052;--md-default-fg-color--lightest:#00000012;--md-default-bg-color:#fff;--md-default-bg-color--light:#ffffffb3;--md-default-bg-color--lighter:#ffffff4d;--md-default-bg-color--lightest:#ffffff1f;--md-code-fg-color:#36464e;--md-code-bg-color:#f5f5f5;--md-code-hl-color:#4287ff;--md-code-hl-color--light:#4287ff1a;--md-code-hl-number-color:#d52a2a;--md-code-hl-special-color:#db1457;--md-code-hl-function-color:#a846b9;--md-code-hl-constant-color:#6e59d9;--md-code-hl-keyword-color:#3f6ec6;--md-code-hl-string-color:#1c7d4d;--md-code-hl-name-color:var(--md-code-fg-color);--md-code-hl-operator-color:var(--md-default-fg-color--light);--md-code-hl-punctuation-color:var(--md-default-fg-color--light);--md-code-hl-comment-color:var(--md-default-fg-color--light);--md-code-hl-generic-color:var(--md-default-fg-color--light);--md-code-hl-variable-color:var(--md-default-fg-color--light);--md-typeset-color:var(--md-default-fg-color);--md-typeset-a-color:var(--md-primary-fg-color);--md-typeset-del-color:#f5503d26;--md-typeset-ins-color:#0bd57026;--md-typeset-kbd-color:#fafafa;--md-typeset-kbd-accent-color:#fff;--md-typeset-kbd-border-color:#b8b8b8;--md-typeset-mark-color:#ffff0080;--md-typeset-table-color:#0000001f;--md-typeset-table-color--light:rgba(0,0,0,.035);--md-admonition-fg-color:var(--md-default-fg-color);--md-admonition-bg-color:var(--md-default-bg-color);--md-warning-fg-color:#000000de;--md-warning-bg-color:#ff9;--md-footer-fg-color:#fff;--md-footer-fg-color--light:#ffffffb3;--md-footer-fg-color--lighter:#ffffff73;--md-footer-bg-color:#000000de;--md-footer-bg-color--dark:#00000052;--md-shadow-z1:0 0.2rem 0.5rem #0000000d,0 0 0.05rem #0000001a;--md-shadow-z2:0 0.2rem 0.5rem #0000001a,0 0 0.05rem #00000040;--md-shadow-z3:0 0.2rem 0.5rem #0003,0 0 0.05rem #00000059}.md-icon svg{fill:currentcolor;display:block;height:1.2rem;width:1.2rem}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;--md-text-font-family:var(--md-text-font,_),-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;--md-code-font-family:var(--md-code-font,_),SFMono-Regular,Consolas,Menlo,monospace}aside,body,input{font-feature-settings:"kern","liga";color:var(--md-typeset-color);font-family:var(--md-text-font-family)}code,kbd,pre{font-feature-settings:"kern";font-family:var(--md-code-font-family)}:root{--md-typeset-table-sort-icon:url('data:image/svg+xml;charset=utf-8,');--md-typeset-table-sort-icon--asc:url('data:image/svg+xml;charset=utf-8,');--md-typeset-table-sort-icon--desc:url('data:image/svg+xml;charset=utf-8,')}.md-typeset{-webkit-print-color-adjust:exact;color-adjust:exact;font-size:.8rem;line-height:1.6}@media print{.md-typeset{font-size:.68rem}}.md-typeset blockquote,.md-typeset dl,.md-typeset figure,.md-typeset ol,.md-typeset pre,.md-typeset ul{margin-bottom:1em;margin-top:1em}.md-typeset h1{color:var(--md-default-fg-color--light);font-size:2em;line-height:1.3;margin:0 0 1.25em}.md-typeset h1,.md-typeset h2{font-weight:300;letter-spacing:-.01em}.md-typeset h2{font-size:1.5625em;line-height:1.4;margin:1.6em 0 .64em}.md-typeset h3{font-size:1.25em;font-weight:400;letter-spacing:-.01em;line-height:1.5;margin:1.6em 0 .8em}.md-typeset h2+h3{margin-top:.8em}.md-typeset h4{font-weight:700;letter-spacing:-.01em;margin:1em 0}.md-typeset h5,.md-typeset h6{color:var(--md-default-fg-color--light);font-size:.8em;font-weight:700;letter-spacing:-.01em;margin:1.25em 0}.md-typeset h5{text-transform:uppercase}.md-typeset hr{border-bottom:.05rem solid var(--md-default-fg-color--lightest);display:flow-root;margin:1.5em 0}.md-typeset a{color:var(--md-typeset-a-color);word-break:break-word}.md-typeset a,.md-typeset a:before{transition:color 125ms}.md-typeset a:focus,.md-typeset a:hover{color:var(--md-accent-fg-color)}.md-typeset a:focus code,.md-typeset a:hover code{background-color:var(--md-accent-fg-color--transparent)}.md-typeset a code{color:currentcolor;transition:background-color 125ms}.md-typeset a.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-typeset code,.md-typeset kbd,.md-typeset pre{color:var(--md-code-fg-color);direction:ltr;font-variant-ligatures:none}@media print{.md-typeset code,.md-typeset kbd,.md-typeset pre{white-space:pre-wrap}}.md-typeset code{background-color:var(--md-code-bg-color);border-radius:.1rem;-webkit-box-decoration-break:clone;box-decoration-break:clone;font-size:.85em;padding:0 .2941176471em;word-break:break-word}.md-typeset code:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}.md-typeset pre{display:flow-root;line-height:1.4;position:relative}.md-typeset pre>code{-webkit-box-decoration-break:slice;box-decoration-break:slice;box-shadow:none;display:block;margin:0;outline-color:var(--md-accent-fg-color);overflow:auto;padding:.7720588235em 1.1764705882em;scrollbar-color:var(--md-default-fg-color--lighter) #0000;scrollbar-width:thin;touch-action:auto;word-break:normal}.md-typeset pre>code:hover{scrollbar-color:var(--md-accent-fg-color) #0000}.md-typeset pre>code::-webkit-scrollbar{height:.2rem;width:.2rem}.md-typeset pre>code::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-typeset pre>code::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}.md-typeset kbd{background-color:var(--md-typeset-kbd-color);border-radius:.1rem;box-shadow:0 .1rem 0 .05rem var(--md-typeset-kbd-border-color),0 .1rem 0 var(--md-typeset-kbd-border-color),0 -.1rem .2rem var(--md-typeset-kbd-accent-color) inset;color:var(--md-default-fg-color);display:inline-block;font-size:.75em;padding:0 .6666666667em;vertical-align:text-top;word-break:break-word}.md-typeset mark{background-color:var(--md-typeset-mark-color);-webkit-box-decoration-break:clone;box-decoration-break:clone;color:inherit;word-break:break-word}.md-typeset abbr{border-bottom:.05rem dotted var(--md-default-fg-color--light);cursor:help;text-decoration:none}@media (hover:none){.md-typeset abbr[title]:focus:after,.md-typeset abbr[title]:hover:after{background-color:var(--md-default-fg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z3);color:var(--md-default-bg-color);content:attr(title);font-size:.7rem;left:.8rem;margin-top:2em;padding:.2rem .3rem;position:absolute;right:.8rem}}.md-typeset small{opacity:.75}[dir=ltr] .md-typeset sub,[dir=ltr] .md-typeset sup{margin-left:.078125em}[dir=rtl] .md-typeset sub,[dir=rtl] .md-typeset sup{margin-right:.078125em}[dir=ltr] .md-typeset blockquote{padding-left:.6rem}[dir=rtl] .md-typeset blockquote{padding-right:.6rem}[dir=ltr] .md-typeset blockquote{border-left:.2rem solid var(--md-default-fg-color--lighter)}[dir=rtl] .md-typeset blockquote{border-right:.2rem solid var(--md-default-fg-color--lighter)}.md-typeset blockquote{color:var(--md-default-fg-color--light);margin-left:0;margin-right:0}.md-typeset ul{list-style-type:disc}[dir=ltr] .md-typeset ol,[dir=ltr] .md-typeset ul{margin-left:.625em}[dir=rtl] .md-typeset ol,[dir=rtl] .md-typeset ul{margin-right:.625em}.md-typeset ol,.md-typeset ul{padding:0}.md-typeset ol:not([hidden]),.md-typeset ul:not([hidden]){display:flow-root}.md-typeset ol ol,.md-typeset ul ol{list-style-type:lower-alpha}.md-typeset ol ol ol,.md-typeset ul ol ol{list-style-type:lower-roman}[dir=ltr] .md-typeset ol li,[dir=ltr] .md-typeset ul li{margin-left:1.25em}[dir=rtl] .md-typeset ol li,[dir=rtl] .md-typeset ul li{margin-right:1.25em}.md-typeset ol li,.md-typeset ul li{margin-bottom:.5em}.md-typeset ol li blockquote,.md-typeset ol li p,.md-typeset ul li blockquote,.md-typeset ul li p{margin:.5em 0}.md-typeset ol li:last-child,.md-typeset ul li:last-child{margin-bottom:0}[dir=ltr] .md-typeset ol li ol,[dir=ltr] .md-typeset ol li ul,[dir=ltr] .md-typeset ul li ol,[dir=ltr] .md-typeset ul li ul{margin-left:.625em}[dir=rtl] .md-typeset ol li ol,[dir=rtl] .md-typeset ol li ul,[dir=rtl] .md-typeset ul li ol,[dir=rtl] .md-typeset ul li ul{margin-right:.625em}.md-typeset ol li ol,.md-typeset ol li ul,.md-typeset ul li ol,.md-typeset ul li ul{margin-bottom:.5em;margin-top:.5em}[dir=ltr] .md-typeset dd{margin-left:1.875em}[dir=rtl] .md-typeset dd{margin-right:1.875em}.md-typeset dd{margin-bottom:1.5em;margin-top:1em}.md-typeset img,.md-typeset svg,.md-typeset video{height:auto;max-width:100%}.md-typeset img[align=left]{margin:1em 1em 1em 0}.md-typeset img[align=right]{margin:1em 0 1em 1em}.md-typeset img[align]:only-child{margin-top:0}.md-typeset figure{display:flow-root;margin:1em auto;max-width:100%;text-align:center;width:-webkit-fit-content;width:-moz-fit-content;width:fit-content}.md-typeset figure img{display:block}.md-typeset figcaption{font-style:italic;margin:1em auto;max-width:24rem}.md-typeset iframe{max-width:100%}.md-typeset table:not([class]){background-color:var(--md-default-bg-color);border:.05rem solid var(--md-typeset-table-color);border-radius:.1rem;display:inline-block;font-size:.64rem;max-width:100%;overflow:auto;touch-action:auto}@media print{.md-typeset table:not([class]){display:table}}.md-typeset table:not([class])+*{margin-top:1.5em}.md-typeset table:not([class]) td>:first-child,.md-typeset table:not([class]) th>:first-child{margin-top:0}.md-typeset table:not([class]) td>:last-child,.md-typeset table:not([class]) th>:last-child{margin-bottom:0}.md-typeset table:not([class]) td:not([align]),.md-typeset table:not([class]) th:not([align]){text-align:left}[dir=rtl] .md-typeset table:not([class]) td:not([align]),[dir=rtl] .md-typeset table:not([class]) th:not([align]){text-align:right}.md-typeset table:not([class]) th{font-weight:700;min-width:5rem;padding:.9375em 1.25em;vertical-align:top}.md-typeset table:not([class]) td{border-top:.05rem solid var(--md-typeset-table-color);padding:.9375em 1.25em;vertical-align:top}.md-typeset table:not([class]) tbody tr{transition:background-color 125ms}.md-typeset table:not([class]) tbody tr:hover{background-color:var(--md-typeset-table-color--light);box-shadow:0 .05rem 0 var(--md-default-bg-color) inset}.md-typeset table:not([class]) a{word-break:normal}.md-typeset table th[role=columnheader]{cursor:pointer}[dir=ltr] .md-typeset table th[role=columnheader]:after{margin-left:.5em}[dir=rtl] .md-typeset table th[role=columnheader]:after{margin-right:.5em}.md-typeset table th[role=columnheader]:after{content:"";display:inline-block;height:1.2em;-webkit-mask-image:var(--md-typeset-table-sort-icon);mask-image:var(--md-typeset-table-sort-icon);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color 125ms;vertical-align:text-bottom;width:1.2em}.md-typeset table th[role=columnheader]:hover:after{background-color:var(--md-default-fg-color--lighter)}.md-typeset table th[role=columnheader][aria-sort=ascending]:after{background-color:var(--md-default-fg-color--light);-webkit-mask-image:var(--md-typeset-table-sort-icon--asc);mask-image:var(--md-typeset-table-sort-icon--asc)}.md-typeset table th[role=columnheader][aria-sort=descending]:after{background-color:var(--md-default-fg-color--light);-webkit-mask-image:var(--md-typeset-table-sort-icon--desc);mask-image:var(--md-typeset-table-sort-icon--desc)}.md-typeset__scrollwrap{margin:1em -.8rem;overflow-x:auto;touch-action:auto}.md-typeset__table{display:inline-block;margin-bottom:.5em;padding:0 .8rem}@media print{.md-typeset__table{display:block}}html .md-typeset__table table{display:table;margin:0;overflow:hidden;width:100%}@media screen and (max-width:44.984375em){.md-content__inner>pre{margin:1em -.8rem}.md-content__inner>pre code{border-radius:0}}.md-typeset .md-author{display:block;flex-shrink:0;height:1.6rem;overflow:hidden;position:relative;transition:color 125ms,transform 125ms;width:1.6rem}.md-typeset .md-author img{border-radius:100%;display:block}.md-typeset .md-author--more{background:var(--md-default-fg-color--lightest);color:var(--md-default-fg-color--lighter);font-size:.6rem;font-weight:700;line-height:1.6rem;text-align:center}.md-typeset .md-author--long{height:2.4rem;width:2.4rem}.md-typeset a.md-author{transform:scale(1)}.md-typeset a.md-author img{filter:grayscale(100%) opacity(75%);transition:filter 125ms}.md-typeset a.md-author:focus,.md-typeset a.md-author:hover{transform:scale(1.1);z-index:1}.md-typeset a.md-author:focus img,.md-typeset a.md-author:hover img{filter:grayscale(0)}.md-banner{background-color:var(--md-footer-bg-color);color:var(--md-footer-fg-color);overflow:auto}@media print{.md-banner{display:none}}.md-banner--warning{background-color:var(--md-warning-bg-color);color:var(--md-warning-fg-color)}.md-banner__inner{font-size:.7rem;margin:.6rem auto;padding:0 .8rem}[dir=ltr] .md-banner__button{float:right}[dir=rtl] .md-banner__button{float:left}.md-banner__button{color:inherit;cursor:pointer;transition:opacity .25s}.no-js .md-banner__button{display:none}.md-banner__button:hover{opacity:.7}html{font-size:125%;height:100%;overflow-x:hidden}@media screen and (min-width:100em){html{font-size:137.5%}}@media screen and (min-width:125em){html{font-size:150%}}body{background-color:var(--md-default-bg-color);display:flex;flex-direction:column;font-size:.5rem;min-height:100%;position:relative;width:100%}@media print{body{display:block}}@media screen and (max-width:59.984375em){body[data-md-scrolllock]{position:fixed}}.md-grid{margin-left:auto;margin-right:auto;max-width:61rem}.md-container{display:flex;flex-direction:column;flex-grow:1}@media print{.md-container{display:block}}.md-main{flex-grow:1}.md-main__inner{display:flex;height:100%;margin-top:1.5rem}.md-ellipsis{overflow:hidden;text-overflow:ellipsis}.md-toggle{display:none}.md-option{height:0;opacity:0;position:absolute;width:0}.md-option:checked+label:not([hidden]){display:block}.md-option.focus-visible+label{outline-color:var(--md-accent-fg-color);outline-style:auto}.md-skip{background-color:var(--md-default-fg-color);border-radius:.1rem;color:var(--md-default-bg-color);font-size:.64rem;margin:.5rem;opacity:0;outline-color:var(--md-accent-fg-color);padding:.3rem .5rem;position:fixed;transform:translateY(.4rem);z-index:-1}.md-skip:focus{opacity:1;transform:translateY(0);transition:transform .25s cubic-bezier(.4,0,.2,1),opacity 175ms 75ms;z-index:10}@page{margin:25mm}:root{--md-clipboard-icon:url('data:image/svg+xml;charset=utf-8,')}.md-clipboard{border-radius:.1rem;color:var(--md-default-fg-color--lightest);cursor:pointer;height:1.5em;outline-color:var(--md-accent-fg-color);outline-offset:.1rem;position:absolute;right:.5em;top:.5em;transition:color .25s;width:1.5em;z-index:1}@media print{.md-clipboard{display:none}}.md-clipboard:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}:hover>.md-clipboard{color:var(--md-default-fg-color--light)}.md-clipboard:focus,.md-clipboard:hover{color:var(--md-accent-fg-color)}.md-clipboard:after{background-color:currentcolor;content:"";display:block;height:1.125em;margin:0 auto;-webkit-mask-image:var(--md-clipboard-icon);mask-image:var(--md-clipboard-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:1.125em}.md-clipboard--inline{cursor:pointer}.md-clipboard--inline code{transition:color .25s,background-color .25s}.md-clipboard--inline:focus code,.md-clipboard--inline:hover code{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}@keyframes consent{0%{opacity:0;transform:translateY(100%)}to{opacity:1;transform:translateY(0)}}@keyframes overlay{0%{opacity:0}to{opacity:1}}.md-consent__overlay{animation:overlay .25s both;-webkit-backdrop-filter:blur(.1rem);backdrop-filter:blur(.1rem);background-color:#0000008a;height:100%;opacity:1;position:fixed;top:0;width:100%;z-index:5}.md-consent__inner{animation:consent .5s cubic-bezier(.1,.7,.1,1) both;background-color:var(--md-default-bg-color);border:0;border-radius:.1rem;bottom:0;box-shadow:0 0 .2rem #0000001a,0 .2rem .4rem #0003;max-height:100%;overflow:auto;padding:0;position:fixed;width:100%;z-index:5}.md-consent__form{padding:.8rem}.md-consent__settings{display:none;margin:1em 0}input:checked+.md-consent__settings{display:block}.md-consent__controls{margin-bottom:.8rem}.md-typeset .md-consent__controls .md-button{display:inline}@media screen and (max-width:44.984375em){.md-typeset .md-consent__controls .md-button{display:block;margin-top:.4rem;text-align:center;width:100%}}.md-consent label{cursor:pointer}.md-content{flex-grow:1;min-width:0}.md-content__inner{margin:0 .8rem 1.2rem;padding-top:.6rem}@media screen and (min-width:76.25em){[dir=ltr] .md-sidebar--primary:not([hidden])~.md-content>.md-content__inner{margin-left:1.2rem}[dir=ltr] .md-sidebar--secondary:not([hidden])~.md-content>.md-content__inner,[dir=rtl] .md-sidebar--primary:not([hidden])~.md-content>.md-content__inner{margin-right:1.2rem}[dir=rtl] .md-sidebar--secondary:not([hidden])~.md-content>.md-content__inner{margin-left:1.2rem}}.md-content__inner:before{content:"";display:block;height:.4rem}.md-content__inner>:last-child{margin-bottom:0}[dir=ltr] .md-content__button{float:right}[dir=rtl] .md-content__button{float:left}[dir=ltr] .md-content__button{margin-left:.4rem}[dir=rtl] .md-content__button{margin-right:.4rem}.md-content__button{margin:.4rem 0;padding:0}@media print{.md-content__button{display:none}}.md-typeset .md-content__button{color:var(--md-default-fg-color--lighter)}.md-content__button svg{display:inline;vertical-align:top}[dir=rtl] .md-content__button svg{transform:scaleX(-1)}[dir=ltr] .md-dialog{right:.8rem}[dir=rtl] .md-dialog{left:.8rem}.md-dialog{background-color:var(--md-default-fg-color);border-radius:.1rem;bottom:.8rem;box-shadow:var(--md-shadow-z3);min-width:11.1rem;opacity:0;padding:.4rem .6rem;pointer-events:none;position:fixed;transform:translateY(100%);transition:transform 0ms .4s,opacity .4s;z-index:4}@media print{.md-dialog{display:none}}.md-dialog--active{opacity:1;pointer-events:auto;transform:translateY(0);transition:transform .4s cubic-bezier(.075,.85,.175,1),opacity .4s}.md-dialog__inner{color:var(--md-default-bg-color);font-size:.7rem}.md-feedback{margin:2em 0 1em;text-align:center}.md-feedback fieldset{border:none;margin:0;padding:0}.md-feedback__title{font-weight:700;margin:1em auto}.md-feedback__inner{position:relative}.md-feedback__list{align-content:baseline;display:flex;flex-wrap:wrap;justify-content:center;position:relative}.md-feedback__list:hover .md-icon:not(:disabled){color:var(--md-default-fg-color--lighter)}:disabled .md-feedback__list{min-height:1.8rem}.md-feedback__icon{color:var(--md-default-fg-color--light);cursor:pointer;flex-shrink:0;margin:0 .1rem;transition:color 125ms}.md-feedback__icon:not(:disabled).md-icon:hover{color:var(--md-accent-fg-color)}.md-feedback__icon:disabled{color:var(--md-default-fg-color--lightest);pointer-events:none}.md-feedback__note{opacity:0;position:relative;transform:translateY(.4rem);transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .15s}.md-feedback__note>*{margin:0 auto;max-width:16rem}:disabled .md-feedback__note{opacity:1;transform:translateY(0)}.md-footer{background-color:var(--md-footer-bg-color);color:var(--md-footer-fg-color)}@media print{.md-footer{display:none}}.md-footer__inner{justify-content:space-between;overflow:auto;padding:.2rem}.md-footer__inner:not([hidden]){display:flex}.md-footer__link{align-items:end;display:flex;flex-grow:0.01;margin-bottom:.4rem;margin-top:1rem;max-width:100%;outline-color:var(--md-accent-fg-color);overflow:hidden;transition:opacity .25s}.md-footer__link:focus,.md-footer__link:hover{opacity:.7}[dir=rtl] .md-footer__link svg{transform:scaleX(-1)}@media screen and (max-width:44.984375em){.md-footer__link--prev{flex-shrink:0}.md-footer__link--prev .md-footer__title{display:none}}[dir=ltr] .md-footer__link--next{margin-left:auto}[dir=rtl] .md-footer__link--next{margin-right:auto}.md-footer__link--next{text-align:right}[dir=rtl] .md-footer__link--next{text-align:left}.md-footer__title{flex-grow:1;font-size:.9rem;margin-bottom:.7rem;max-width:calc(100% - 2.4rem);padding:0 1rem;white-space:nowrap}.md-footer__button{margin:.2rem;padding:.4rem}.md-footer__direction{font-size:.64rem;opacity:.7}.md-footer-meta{background-color:var(--md-footer-bg-color--dark)}.md-footer-meta__inner{display:flex;flex-wrap:wrap;justify-content:space-between;padding:.2rem}html .md-footer-meta.md-typeset a{color:var(--md-footer-fg-color--light)}html .md-footer-meta.md-typeset a:focus,html .md-footer-meta.md-typeset a:hover{color:var(--md-footer-fg-color)}.md-copyright{color:var(--md-footer-fg-color--lighter);font-size:.64rem;margin:auto .6rem;padding:.4rem 0;width:100%}@media screen and (min-width:45em){.md-copyright{width:auto}}.md-copyright__highlight{color:var(--md-footer-fg-color--light)}.md-social{display:inline-flex;gap:.2rem;margin:0 .4rem;padding:.2rem 0 .6rem}@media screen and (min-width:45em){.md-social{padding:.6rem 0}}.md-social__link{display:inline-block;height:1.6rem;text-align:center;width:1.6rem}.md-social__link:before{line-height:1.9}.md-social__link svg{fill:currentcolor;max-height:.8rem;vertical-align:-25%}.md-typeset .md-button{border:.1rem solid;border-radius:.1rem;color:var(--md-primary-fg-color);cursor:pointer;display:inline-block;font-weight:700;padding:.625em 2em;transition:color 125ms,background-color 125ms,border-color 125ms}.md-typeset .md-button--primary{background-color:var(--md-primary-fg-color);border-color:var(--md-primary-fg-color);color:var(--md-primary-bg-color)}.md-typeset .md-button:focus,.md-typeset .md-button:hover{background-color:var(--md-accent-fg-color);border-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}[dir=ltr] .md-typeset .md-input{border-top-left-radius:.1rem}[dir=ltr] .md-typeset .md-input,[dir=rtl] .md-typeset .md-input{border-top-right-radius:.1rem}[dir=rtl] .md-typeset .md-input{border-top-left-radius:.1rem}.md-typeset .md-input{border-bottom:.1rem solid var(--md-default-fg-color--lighter);box-shadow:var(--md-shadow-z1);font-size:.8rem;height:1.8rem;padding:0 .6rem;transition:border .25s,box-shadow .25s}.md-typeset .md-input:focus,.md-typeset .md-input:hover{border-bottom-color:var(--md-accent-fg-color);box-shadow:var(--md-shadow-z2)}.md-typeset .md-input--stretch{width:100%}.md-header{background-color:var(--md-primary-fg-color);box-shadow:0 0 .2rem #0000,0 .2rem .4rem #0000;color:var(--md-primary-bg-color);display:block;left:0;position:sticky;right:0;top:0;z-index:4}@media print{.md-header{display:none}}.md-header[hidden]{transform:translateY(-100%);transition:transform .25s cubic-bezier(.8,0,.6,1),box-shadow .25s}.md-header--shadow{box-shadow:0 0 .2rem #0000001a,0 .2rem .4rem #0003;transition:transform .25s cubic-bezier(.1,.7,.1,1),box-shadow .25s}.md-header__inner{align-items:center;display:flex;padding:0 .2rem}.md-header__button{color:currentcolor;cursor:pointer;margin:.2rem;outline-color:var(--md-accent-fg-color);padding:.4rem;position:relative;transition:opacity .25s;vertical-align:middle;z-index:1}.md-header__button:hover{opacity:.7}.md-header__button:not([hidden]){display:inline-block}.md-header__button:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}.md-header__button.md-logo{margin:.2rem;padding:.4rem}@media screen and (max-width:76.234375em){.md-header__button.md-logo{display:none}}.md-header__button.md-logo img,.md-header__button.md-logo svg{fill:currentcolor;display:block;height:1.2rem;width:auto}@media screen and (min-width:60em){.md-header__button[for=__search]{display:none}}.no-js .md-header__button[for=__search]{display:none}[dir=rtl] .md-header__button[for=__search] svg{transform:scaleX(-1)}@media screen and (min-width:76.25em){.md-header__button[for=__drawer]{display:none}}.md-header__topic{display:flex;max-width:100%;position:absolute;transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .15s;white-space:nowrap}.md-header__topic+.md-header__topic{opacity:0;pointer-events:none;transform:translateX(1.25rem);transition:transform .4s cubic-bezier(1,.7,.1,.1),opacity .15s;z-index:-1}[dir=rtl] .md-header__topic+.md-header__topic{transform:translateX(-1.25rem)}.md-header__topic:first-child{font-weight:700}[dir=ltr] .md-header__title{margin-left:1rem}[dir=rtl] .md-header__title{margin-right:1rem}[dir=ltr] .md-header__title{margin-right:.4rem}[dir=rtl] .md-header__title{margin-left:.4rem}.md-header__title{flex-grow:1;font-size:.9rem;height:2.4rem;line-height:2.4rem}.md-header__title--active .md-header__topic{opacity:0;pointer-events:none;transform:translateX(-1.25rem);transition:transform .4s cubic-bezier(1,.7,.1,.1),opacity .15s;z-index:-1}[dir=rtl] .md-header__title--active .md-header__topic{transform:translateX(1.25rem)}.md-header__title--active .md-header__topic+.md-header__topic{opacity:1;pointer-events:auto;transform:translateX(0);transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .15s;z-index:0}.md-header__title>.md-header__ellipsis{height:100%;position:relative;width:100%}.md-header__option{display:flex;flex-shrink:0;max-width:100%;transition:max-width 0ms .25s,opacity .25s .25s;white-space:nowrap}[data-md-toggle=search]:checked~.md-header .md-header__option{max-width:0;opacity:0;transition:max-width 0ms,opacity 0ms}.md-header__option>input{bottom:0}.md-header__source{display:none}@media screen and (min-width:60em){[dir=ltr] .md-header__source{margin-left:1rem}[dir=rtl] .md-header__source{margin-right:1rem}.md-header__source{display:block;max-width:11.7rem;width:11.7rem}}@media screen and (min-width:76.25em){[dir=ltr] .md-header__source{margin-left:1.4rem}[dir=rtl] .md-header__source{margin-right:1.4rem}}.md-meta{color:var(--md-default-fg-color--light);font-size:.7rem;line-height:1.3}.md-meta__list{display:inline-flex;flex-wrap:wrap;list-style:none;margin:0;padding:0}.md-meta__item:not(:last-child):after{content:"·";margin-left:.2rem;margin-right:.2rem}.md-meta__link{color:var(--md-typeset-a-color)}.md-meta__link:focus,.md-meta__link:hover{color:var(--md-accent-fg-color)}.md-draft{background-color:#ff1744;border-radius:.125em;color:#fff;display:inline-block;font-weight:700;padding-left:.5714285714em;padding-right:.5714285714em}:root{--md-nav-icon--prev:url('data:image/svg+xml;charset=utf-8,');--md-nav-icon--next:url('data:image/svg+xml;charset=utf-8,');--md-toc-icon:url('data:image/svg+xml;charset=utf-8,')}.md-nav{font-size:.7rem;line-height:1.3}.md-nav__title{color:var(--md-default-fg-color--light);display:block;font-weight:700;overflow:hidden;padding:0 .6rem;text-overflow:ellipsis}.md-nav__title .md-nav__button{display:none}.md-nav__title .md-nav__button img{height:100%;width:auto}.md-nav__title .md-nav__button.md-logo img,.md-nav__title .md-nav__button.md-logo svg{fill:currentcolor;display:block;height:2.4rem;max-width:100%;object-fit:contain;width:auto}.md-nav__list{list-style:none;margin:0;padding:0}.md-nav__link{align-items:flex-start;display:flex;gap:.4rem;margin-top:.625em;scroll-snap-align:start;transition:color 125ms}.md-nav__link--passed{color:var(--md-default-fg-color--light)}.md-nav__item .md-nav__link--active,.md-nav__item .md-nav__link--active code{color:var(--md-typeset-a-color)}.md-nav__link .md-ellipsis{position:relative}[dir=ltr] .md-nav__link .md-icon:last-child{margin-left:auto}[dir=rtl] .md-nav__link .md-icon:last-child{margin-right:auto}.md-nav__link svg{fill:currentcolor;flex-shrink:0;height:1.3em}.md-nav__link[for]:focus,.md-nav__link[for]:hover,.md-nav__link[href]:focus,.md-nav__link[href]:hover{color:var(--md-accent-fg-color);cursor:pointer}.md-nav__link.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-nav--primary .md-nav__link[for=__toc]{display:none}.md-nav--primary .md-nav__link[for=__toc] .md-icon:after{background-color:currentcolor;display:block;height:100%;-webkit-mask-image:var(--md-toc-icon);mask-image:var(--md-toc-icon);width:100%}.md-nav--primary .md-nav__link[for=__toc]~.md-nav{display:none}.md-nav__container>.md-nav__link{margin-top:0}.md-nav__container>.md-nav__link:first-child{flex-grow:1;min-width:0}.md-nav__icon{flex-shrink:0}.md-nav__source{display:none}@media screen and (max-width:76.234375em){.md-nav--primary,.md-nav--primary .md-nav{background-color:var(--md-default-bg-color);display:flex;flex-direction:column;height:100%;left:0;position:absolute;right:0;top:0;z-index:1}.md-nav--primary .md-nav__item,.md-nav--primary .md-nav__title{font-size:.8rem;line-height:1.5}.md-nav--primary .md-nav__title{background-color:var(--md-default-fg-color--lightest);color:var(--md-default-fg-color--light);cursor:pointer;height:5.6rem;line-height:2.4rem;padding:3rem .8rem .2rem;position:relative;white-space:nowrap}[dir=ltr] .md-nav--primary .md-nav__title .md-nav__icon{left:.4rem}[dir=rtl] .md-nav--primary .md-nav__title .md-nav__icon{right:.4rem}.md-nav--primary .md-nav__title .md-nav__icon{display:block;height:1.2rem;margin:.2rem;position:absolute;top:.4rem;width:1.2rem}.md-nav--primary .md-nav__title .md-nav__icon:after{background-color:currentcolor;content:"";display:block;height:100%;-webkit-mask-image:var(--md-nav-icon--prev);mask-image:var(--md-nav-icon--prev);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:100%}.md-nav--primary .md-nav__title~.md-nav__list{background-color:var(--md-default-bg-color);box-shadow:0 .05rem 0 var(--md-default-fg-color--lightest) inset;overflow-y:auto;scroll-snap-type:y mandatory;touch-action:pan-y}.md-nav--primary .md-nav__title~.md-nav__list>:first-child{border-top:0}.md-nav--primary .md-nav__title[for=__drawer]{background-color:var(--md-primary-fg-color);color:var(--md-primary-bg-color);font-weight:700}.md-nav--primary .md-nav__title .md-logo{display:block;left:.2rem;margin:.2rem;padding:.4rem;position:absolute;right:.2rem;top:.2rem}.md-nav--primary .md-nav__list{flex:1}.md-nav--primary .md-nav__item{border-top:.05rem solid var(--md-default-fg-color--lightest)}.md-nav--primary .md-nav__item--active>.md-nav__link{color:var(--md-typeset-a-color)}.md-nav--primary .md-nav__item--active>.md-nav__link:focus,.md-nav--primary .md-nav__item--active>.md-nav__link:hover{color:var(--md-accent-fg-color)}.md-nav--primary .md-nav__link{margin-top:0;padding:.6rem .8rem}.md-nav--primary .md-nav__link svg{margin-top:.1em}.md-nav--primary .md-nav__link>.md-nav__link{padding:0}[dir=ltr] .md-nav--primary .md-nav__link .md-nav__icon{margin-right:-.2rem}[dir=rtl] .md-nav--primary .md-nav__link .md-nav__icon{margin-left:-.2rem}.md-nav--primary .md-nav__link .md-nav__icon{font-size:1.2rem;height:1.2rem;width:1.2rem}.md-nav--primary .md-nav__link .md-nav__icon:after{background-color:currentcolor;content:"";display:block;height:100%;-webkit-mask-image:var(--md-nav-icon--next);mask-image:var(--md-nav-icon--next);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:100%}[dir=rtl] .md-nav--primary .md-nav__icon:after{transform:scale(-1)}.md-nav--primary .md-nav--secondary .md-nav{background-color:initial;position:static}[dir=ltr] .md-nav--primary .md-nav--secondary .md-nav .md-nav__link{padding-left:1.4rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav__link{padding-right:1.4rem}[dir=ltr] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav__link{padding-left:2rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav__link{padding-right:2rem}[dir=ltr] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav__link{padding-left:2.6rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav__link{padding-right:2.6rem}[dir=ltr] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav .md-nav__link{padding-left:3.2rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav .md-nav__link{padding-right:3.2rem}.md-nav--secondary{background-color:initial}.md-nav__toggle~.md-nav{display:flex;opacity:0;transform:translateX(100%);transition:transform .25s cubic-bezier(.8,0,.6,1),opacity 125ms 50ms}[dir=rtl] .md-nav__toggle~.md-nav{transform:translateX(-100%)}.md-nav__toggle:checked~.md-nav{opacity:1;transform:translateX(0);transition:transform .25s cubic-bezier(.4,0,.2,1),opacity 125ms 125ms}.md-nav__toggle:checked~.md-nav>.md-nav__list{-webkit-backface-visibility:hidden;backface-visibility:hidden}}@media screen and (max-width:59.984375em){.md-nav--primary .md-nav__link[for=__toc]{display:flex}.md-nav--primary .md-nav__link[for=__toc] .md-icon:after{content:""}.md-nav--primary .md-nav__link[for=__toc]+.md-nav__link{display:none}.md-nav--primary .md-nav__link[for=__toc]~.md-nav{display:flex}.md-nav__source{background-color:var(--md-primary-fg-color--dark);color:var(--md-primary-bg-color);display:block;padding:0 .2rem}}@media screen and (min-width:60em) and (max-width:76.234375em){.md-nav--integrated .md-nav__link[for=__toc]{display:flex}.md-nav--integrated .md-nav__link[for=__toc] .md-icon:after{content:""}.md-nav--integrated .md-nav__link[for=__toc]+.md-nav__link{display:none}.md-nav--integrated .md-nav__link[for=__toc]~.md-nav{display:flex}}@media screen and (min-width:60em){.md-nav{margin-bottom:-.4rem}.md-nav--secondary .md-nav__title{background:var(--md-default-bg-color);box-shadow:0 0 .4rem .4rem var(--md-default-bg-color);position:sticky;top:0;z-index:1}.md-nav--secondary .md-nav__title[for=__toc]{scroll-snap-align:start}.md-nav--secondary .md-nav__title .md-nav__icon{display:none}[dir=ltr] .md-nav--secondary .md-nav__list{padding-left:.6rem}[dir=rtl] .md-nav--secondary .md-nav__list{padding-right:.6rem}.md-nav--secondary .md-nav__list{padding-bottom:.4rem}[dir=ltr] .md-nav--secondary .md-nav__item>.md-nav__link{margin-right:.4rem}[dir=rtl] .md-nav--secondary .md-nav__item>.md-nav__link{margin-left:.4rem}}@media screen and (min-width:76.25em){.md-nav{margin-bottom:-.4rem;transition:max-height .25s cubic-bezier(.86,0,.07,1)}.md-nav--primary .md-nav__title{background:var(--md-default-bg-color);box-shadow:0 0 .4rem .4rem var(--md-default-bg-color);position:sticky;top:0;z-index:1}.md-nav--primary .md-nav__title[for=__drawer]{scroll-snap-align:start}.md-nav--primary .md-nav__title .md-nav__icon{display:none}[dir=ltr] .md-nav--primary .md-nav__list{padding-left:.6rem}[dir=rtl] .md-nav--primary .md-nav__list{padding-right:.6rem}.md-nav--primary .md-nav__list{padding-bottom:.4rem}[dir=ltr] .md-nav--primary .md-nav__item>.md-nav__link{margin-right:.4rem}[dir=rtl] .md-nav--primary .md-nav__item>.md-nav__link{margin-left:.4rem}.md-nav__toggle~.md-nav{display:grid;grid-template-rows:0fr;opacity:0;transition:grid-template-rows .25s cubic-bezier(.86,0,.07,1),opacity .25s,visibility 0ms .25s;visibility:collapse}.md-nav__toggle~.md-nav>.md-nav__list{overflow:hidden}.md-nav__toggle:checked~.md-nav,.md-nav__toggle:indeterminate~.md-nav{grid-template-rows:1fr;opacity:1;transition:grid-template-rows .25s cubic-bezier(.86,0,.07,1),opacity .15s .1s,visibility 0ms;visibility:visible}.md-nav__item--nested>.md-nav>.md-nav__title{display:none}.md-nav__item--section{display:block;margin:1.25em 0}.md-nav__item--section:last-child{margin-bottom:0}.md-nav__item--section>.md-nav__link{font-weight:700}.md-nav__item--section>.md-nav__link[for]{color:var(--md-default-fg-color--light)}.md-nav__item--section>.md-nav__link:not(.md-nav__container){pointer-events:none}.md-nav__item--section>.md-nav__link .md-icon,.md-nav__item--section>.md-nav__link>[for]{display:none}[dir=ltr] .md-nav__item--section>.md-nav{margin-left:-.6rem}[dir=rtl] .md-nav__item--section>.md-nav{margin-right:-.6rem}.md-nav__item--section>.md-nav{display:block;opacity:1;visibility:visible}.md-nav__item--section>.md-nav>.md-nav__list>.md-nav__item{padding:0}.md-nav__icon{border-radius:100%;height:.9rem;transition:background-color .25s;width:.9rem}.md-nav__icon:hover{background-color:var(--md-accent-fg-color--transparent)}.md-nav__icon:after{background-color:currentcolor;border-radius:100%;content:"";display:inline-block;height:100%;-webkit-mask-image:var(--md-nav-icon--next);mask-image:var(--md-nav-icon--next);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:transform .25s;vertical-align:-.1rem;width:100%}[dir=rtl] .md-nav__icon:after{transform:rotate(180deg)}.md-nav__item--nested .md-nav__toggle:checked~.md-nav__link .md-nav__icon:after,.md-nav__item--nested .md-nav__toggle:indeterminate~.md-nav__link .md-nav__icon:after{transform:rotate(90deg)}.md-nav--lifted>.md-nav__list>.md-nav__item,.md-nav--lifted>.md-nav__title{display:none}.md-nav--lifted>.md-nav__list>.md-nav__item--active{display:block}.md-nav--lifted>.md-nav__list>.md-nav__item--active>.md-nav__link{background:var(--md-default-bg-color);box-shadow:0 0 .4rem .4rem var(--md-default-bg-color);margin-top:0;position:sticky;top:0;z-index:1}.md-nav--lifted>.md-nav__list>.md-nav__item--active>.md-nav__link:not(.md-nav__container){pointer-events:none}.md-nav--lifted>.md-nav__list>.md-nav__item--active.md-nav__item--section{margin:0}[dir=ltr] .md-nav--lifted>.md-nav__list>.md-nav__item>.md-nav{margin-left:-.6rem}[dir=rtl] .md-nav--lifted>.md-nav__list>.md-nav__item>.md-nav{margin-right:-.6rem}.md-nav--lifted>.md-nav__list>.md-nav__item>[for]{color:var(--md-default-fg-color--light)}.md-nav--lifted .md-nav[data-md-level="1"]{grid-template-rows:1fr;opacity:1;visibility:visible}.md-nav--integrated>.md-nav__list>.md-nav__item--active:not(.md-nav__item--nested){padding:0 .6rem}.md-nav--integrated>.md-nav__list>.md-nav__item--active:not(.md-nav__item--nested)>.md-nav__link{padding:0}[dir=ltr] .md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{border-left:.05rem solid var(--md-primary-fg-color)}[dir=rtl] .md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{border-right:.05rem solid var(--md-primary-fg-color)}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{display:block;margin-bottom:1.25em;opacity:1;visibility:visible}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary>.md-nav__list{overflow:visible;padding-bottom:0}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary>.md-nav__title{display:none}}.md-pagination{font-size:.8rem;font-weight:700;gap:.4rem}.md-pagination,.md-pagination>*{align-items:center;display:flex;justify-content:center}.md-pagination>*{border-radius:.2rem;height:1.8rem;min-width:1.8rem;text-align:center}.md-pagination__current{background-color:var(--md-default-fg-color--lightest);color:var(--md-default-fg-color--light)}.md-pagination__link{transition:color 125ms,background-color 125ms}.md-pagination__link:focus,.md-pagination__link:hover{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-pagination__link:focus svg,.md-pagination__link:hover svg{color:var(--md-accent-fg-color)}.md-pagination__link.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-pagination__link svg{fill:currentcolor;color:var(--md-default-fg-color--lighter);display:block;max-height:100%;width:1.2rem}.md-post__back{border-bottom:.05rem solid var(--md-default-fg-color--lightest);margin-bottom:1.2rem;padding-bottom:1.2rem}@media screen and (max-width:76.234375em){.md-post__back{display:none}}[dir=rtl] .md-post__back svg{transform:scaleX(-1)}.md-post__authors{display:flex;flex-direction:column;gap:.6rem;margin:0 .6rem 1.2rem}.md-post .md-post__meta a{transition:color 125ms}.md-post .md-post__meta a:focus,.md-post .md-post__meta a:hover{color:var(--md-accent-fg-color)}.md-post__title{color:var(--md-default-fg-color--light);font-weight:700}.md-post--excerpt{margin-bottom:3.2rem}.md-post--excerpt .md-post__header{align-items:center;display:flex;gap:.6rem;min-height:1.6rem}.md-post--excerpt .md-post__authors{align-items:center;display:inline-flex;flex-direction:row;gap:.2rem;margin:0;min-height:2.4rem}[dir=ltr] .md-post--excerpt .md-post__meta .md-meta__list{margin-right:.4rem}[dir=rtl] .md-post--excerpt .md-post__meta .md-meta__list{margin-left:.4rem}.md-post--excerpt .md-post__content>:first-child{--md-scroll-margin:6rem;margin-top:0}.md-post>.md-nav--secondary{margin:1em 0}.md-profile{align-items:center;display:flex;font-size:.7rem;gap:.6rem;line-height:1.4;width:100%}.md-profile__description{flex-grow:1}.md-content--post{display:flex}@media screen and (max-width:76.234375em){.md-content--post{flex-flow:column-reverse}}.md-content--post>.md-content__inner{min-width:0}@media screen and (min-width:76.25em){[dir=ltr] .md-content--post>.md-content__inner{margin-left:1.2rem}[dir=rtl] .md-content--post>.md-content__inner{margin-right:1.2rem}}@media screen and (max-width:76.234375em){.md-sidebar.md-sidebar--post{padding:0;position:static;width:100%}.md-sidebar.md-sidebar--post .md-sidebar__inner{padding:0}.md-sidebar.md-sidebar--post .md-post__meta{margin-left:.6rem;margin-right:.6rem}.md-sidebar.md-sidebar--post .md-nav__item{border:none;display:inline}.md-sidebar.md-sidebar--post .md-nav__list{display:inline-flex;flex-wrap:wrap;gap:.6rem;padding-bottom:.6rem;padding-top:.6rem}.md-sidebar.md-sidebar--post .md-nav__link{padding:0}.md-sidebar.md-sidebar--post .md-nav{position:static}}:root{--md-progress-value:0;--md-progress-delay:400ms}.md-progress{background:var(--md-primary-bg-color);height:.075rem;opacity:min(clamp(0,var(--md-progress-value),1),clamp(0,100 - var(--md-progress-value),1));position:fixed;top:0;transform:scaleX(calc(var(--md-progress-value)*1%));transform-origin:left;transition:transform .5s cubic-bezier(.19,1,.22,1),opacity .25s var(--md-progress-delay);width:100%;z-index:4}:root{--md-search-result-icon:url('data:image/svg+xml;charset=utf-8,')}.md-search{position:relative}@media screen and (min-width:60em){.md-search{padding:.2rem 0}}.no-js .md-search{display:none}.md-search__overlay{opacity:0;z-index:1}@media screen and (max-width:59.984375em){[dir=ltr] .md-search__overlay{left:-2.2rem}[dir=rtl] .md-search__overlay{right:-2.2rem}.md-search__overlay{background-color:var(--md-default-bg-color);border-radius:1rem;height:2rem;overflow:hidden;pointer-events:none;position:absolute;top:-1rem;transform-origin:center;transition:transform .3s .1s,opacity .2s .2s;width:2rem}[data-md-toggle=search]:checked~.md-header .md-search__overlay{opacity:1;transition:transform .4s,opacity .1s}}@media screen and (min-width:60em){[dir=ltr] .md-search__overlay{left:0}[dir=rtl] .md-search__overlay{right:0}.md-search__overlay{background-color:#0000008a;cursor:pointer;height:0;position:fixed;top:0;transition:width 0ms .25s,height 0ms .25s,opacity .25s;width:0}[data-md-toggle=search]:checked~.md-header .md-search__overlay{height:200vh;opacity:1;transition:width 0ms,height 0ms,opacity .25s;width:100%}}@media screen and (max-width:29.984375em){[data-md-toggle=search]:checked~.md-header .md-search__overlay{transform:scale(45)}}@media screen and (min-width:30em) and (max-width:44.984375em){[data-md-toggle=search]:checked~.md-header .md-search__overlay{transform:scale(60)}}@media screen and (min-width:45em) and (max-width:59.984375em){[data-md-toggle=search]:checked~.md-header .md-search__overlay{transform:scale(75)}}.md-search__inner{-webkit-backface-visibility:hidden;backface-visibility:hidden}@media screen and (max-width:59.984375em){[dir=ltr] .md-search__inner{left:0}[dir=rtl] .md-search__inner{right:0}.md-search__inner{height:0;opacity:0;overflow:hidden;position:fixed;top:0;transform:translateX(5%);transition:width 0ms .3s,height 0ms .3s,transform .15s cubic-bezier(.4,0,.2,1) .15s,opacity .15s .15s;width:0;z-index:2}[dir=rtl] .md-search__inner{transform:translateX(-5%)}[data-md-toggle=search]:checked~.md-header .md-search__inner{height:100%;opacity:1;transform:translateX(0);transition:width 0ms 0ms,height 0ms 0ms,transform .15s cubic-bezier(.1,.7,.1,1) .15s,opacity .15s .15s;width:100%}}@media screen and (min-width:60em){[dir=ltr] .md-search__inner{float:right}[dir=rtl] .md-search__inner{float:left}.md-search__inner{padding:.1rem 0;position:relative;transition:width .25s cubic-bezier(.1,.7,.1,1);width:11.7rem}}@media screen and (min-width:60em) and (max-width:76.234375em){[data-md-toggle=search]:checked~.md-header .md-search__inner{width:23.4rem}}@media screen and (min-width:76.25em){[data-md-toggle=search]:checked~.md-header .md-search__inner{width:34.4rem}}.md-search__form{background-color:var(--md-default-bg-color);box-shadow:0 0 .6rem #0000;height:2.4rem;position:relative;transition:color .25s,background-color .25s;z-index:2}@media screen and (min-width:60em){.md-search__form{background-color:#00000042;border-radius:.1rem;height:1.8rem}.md-search__form:hover{background-color:#ffffff1f}}[data-md-toggle=search]:checked~.md-header .md-search__form{background-color:var(--md-default-bg-color);border-radius:.1rem .1rem 0 0;box-shadow:0 0 .6rem #00000012;color:var(--md-default-fg-color)}[dir=ltr] .md-search__input{padding-left:3.6rem;padding-right:2.2rem}[dir=rtl] .md-search__input{padding-left:2.2rem;padding-right:3.6rem}.md-search__input{background:#0000;font-size:.9rem;height:100%;position:relative;text-overflow:ellipsis;width:100%;z-index:2}.md-search__input::placeholder{transition:color .25s}.md-search__input::placeholder,.md-search__input~.md-search__icon{color:var(--md-default-fg-color--light)}.md-search__input::-ms-clear{display:none}@media screen and (max-width:59.984375em){.md-search__input{font-size:.9rem;height:2.4rem;width:100%}}@media screen and (min-width:60em){[dir=ltr] .md-search__input{padding-left:2.2rem}[dir=rtl] .md-search__input{padding-right:2.2rem}.md-search__input{color:inherit;font-size:.8rem}.md-search__input::placeholder{color:var(--md-primary-bg-color--light)}.md-search__input+.md-search__icon{color:var(--md-primary-bg-color)}[data-md-toggle=search]:checked~.md-header .md-search__input{text-overflow:clip}[data-md-toggle=search]:checked~.md-header .md-search__input+.md-search__icon{color:var(--md-default-fg-color--light)}[data-md-toggle=search]:checked~.md-header .md-search__input::placeholder{color:#0000}}.md-search__icon{cursor:pointer;display:inline-block;height:1.2rem;transition:color .25s,opacity .25s;width:1.2rem}.md-search__icon:hover{opacity:.7}[dir=ltr] .md-search__icon[for=__search]{left:.5rem}[dir=rtl] .md-search__icon[for=__search]{right:.5rem}.md-search__icon[for=__search]{position:absolute;top:.3rem;z-index:2}[dir=rtl] .md-search__icon[for=__search] svg{transform:scaleX(-1)}@media screen and (max-width:59.984375em){[dir=ltr] .md-search__icon[for=__search]{left:.8rem}[dir=rtl] .md-search__icon[for=__search]{right:.8rem}.md-search__icon[for=__search]{top:.6rem}.md-search__icon[for=__search] svg:first-child{display:none}}@media screen and (min-width:60em){.md-search__icon[for=__search]{pointer-events:none}.md-search__icon[for=__search] svg:last-child{display:none}}[dir=ltr] .md-search__options{right:.5rem}[dir=rtl] .md-search__options{left:.5rem}.md-search__options{pointer-events:none;position:absolute;top:.3rem;z-index:2}@media screen and (max-width:59.984375em){[dir=ltr] .md-search__options{right:.8rem}[dir=rtl] .md-search__options{left:.8rem}.md-search__options{top:.6rem}}[dir=ltr] .md-search__options>.md-icon{margin-left:.2rem}[dir=rtl] .md-search__options>.md-icon{margin-right:.2rem}.md-search__options>.md-icon{color:var(--md-default-fg-color--light);opacity:0;transform:scale(.75);transition:transform .15s cubic-bezier(.1,.7,.1,1),opacity .15s}.md-search__options>.md-icon:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}[data-md-toggle=search]:checked~.md-header .md-search__input:valid~.md-search__options>.md-icon{opacity:1;pointer-events:auto;transform:scale(1)}[data-md-toggle=search]:checked~.md-header .md-search__input:valid~.md-search__options>.md-icon:hover{opacity:.7}[dir=ltr] .md-search__suggest{padding-left:3.6rem;padding-right:2.2rem}[dir=rtl] .md-search__suggest{padding-left:2.2rem;padding-right:3.6rem}.md-search__suggest{align-items:center;color:var(--md-default-fg-color--lighter);display:flex;font-size:.9rem;height:100%;opacity:0;position:absolute;top:0;transition:opacity 50ms;white-space:nowrap;width:100%}@media screen and (min-width:60em){[dir=ltr] .md-search__suggest{padding-left:2.2rem}[dir=rtl] .md-search__suggest{padding-right:2.2rem}.md-search__suggest{font-size:.8rem}}[data-md-toggle=search]:checked~.md-header .md-search__suggest{opacity:1;transition:opacity .3s .1s}[dir=ltr] .md-search__output{border-bottom-left-radius:.1rem}[dir=ltr] .md-search__output,[dir=rtl] .md-search__output{border-bottom-right-radius:.1rem}[dir=rtl] .md-search__output{border-bottom-left-radius:.1rem}.md-search__output{overflow:hidden;position:absolute;width:100%;z-index:1}@media screen and (max-width:59.984375em){.md-search__output{bottom:0;top:2.4rem}}@media screen and (min-width:60em){.md-search__output{opacity:0;top:1.9rem;transition:opacity .4s}[data-md-toggle=search]:checked~.md-header .md-search__output{box-shadow:var(--md-shadow-z3);opacity:1}}.md-search__scrollwrap{-webkit-backface-visibility:hidden;backface-visibility:hidden;background-color:var(--md-default-bg-color);height:100%;overflow-y:auto;touch-action:pan-y}@media (-webkit-max-device-pixel-ratio:1),(max-resolution:1dppx){.md-search__scrollwrap{transform:translateZ(0)}}@media screen and (min-width:60em) and (max-width:76.234375em){.md-search__scrollwrap{width:23.4rem}}@media screen and (min-width:76.25em){.md-search__scrollwrap{width:34.4rem}}@media screen and (min-width:60em){.md-search__scrollwrap{max-height:0;scrollbar-color:var(--md-default-fg-color--lighter) #0000;scrollbar-width:thin}[data-md-toggle=search]:checked~.md-header .md-search__scrollwrap{max-height:75vh}.md-search__scrollwrap:hover{scrollbar-color:var(--md-accent-fg-color) #0000}.md-search__scrollwrap::-webkit-scrollbar{height:.2rem;width:.2rem}.md-search__scrollwrap::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-search__scrollwrap::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}}.md-search-result{color:var(--md-default-fg-color);word-break:break-word}.md-search-result__meta{background-color:var(--md-default-fg-color--lightest);color:var(--md-default-fg-color--light);font-size:.64rem;line-height:1.8rem;padding:0 .8rem;scroll-snap-align:start}@media screen and (min-width:60em){[dir=ltr] .md-search-result__meta{padding-left:2.2rem}[dir=rtl] .md-search-result__meta{padding-right:2.2rem}}.md-search-result__list{list-style:none;margin:0;padding:0;-webkit-user-select:none;user-select:none}.md-search-result__item{box-shadow:0 -.05rem var(--md-default-fg-color--lightest)}.md-search-result__item:first-child{box-shadow:none}.md-search-result__link{display:block;outline:none;scroll-snap-align:start;transition:background-color .25s}.md-search-result__link:focus,.md-search-result__link:hover{background-color:var(--md-accent-fg-color--transparent)}.md-search-result__link:last-child p:last-child{margin-bottom:.6rem}.md-search-result__more>summary{cursor:pointer;display:block;outline:none;position:sticky;scroll-snap-align:start;top:0;z-index:1}.md-search-result__more>summary::marker{display:none}.md-search-result__more>summary::-webkit-details-marker{display:none}.md-search-result__more>summary>div{color:var(--md-typeset-a-color);font-size:.64rem;padding:.75em .8rem;transition:color .25s,background-color .25s}@media screen and (min-width:60em){[dir=ltr] .md-search-result__more>summary>div{padding-left:2.2rem}[dir=rtl] .md-search-result__more>summary>div{padding-right:2.2rem}}.md-search-result__more>summary:focus>div,.md-search-result__more>summary:hover>div{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-search-result__more[open]>summary{background-color:var(--md-default-bg-color)}.md-search-result__article{overflow:hidden;padding:0 .8rem;position:relative}@media screen and (min-width:60em){[dir=ltr] .md-search-result__article{padding-left:2.2rem}[dir=rtl] .md-search-result__article{padding-right:2.2rem}}[dir=ltr] .md-search-result__icon{left:0}[dir=rtl] .md-search-result__icon{right:0}.md-search-result__icon{color:var(--md-default-fg-color--light);height:1.2rem;margin:.5rem;position:absolute;width:1.2rem}@media screen and (max-width:59.984375em){.md-search-result__icon{display:none}}.md-search-result__icon:after{background-color:currentcolor;content:"";display:inline-block;height:100%;-webkit-mask-image:var(--md-search-result-icon);mask-image:var(--md-search-result-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:100%}[dir=rtl] .md-search-result__icon:after{transform:scaleX(-1)}.md-search-result .md-typeset{color:var(--md-default-fg-color--light);font-size:.64rem;line-height:1.6}.md-search-result .md-typeset h1{color:var(--md-default-fg-color);font-size:.8rem;font-weight:400;line-height:1.4;margin:.55rem 0}.md-search-result .md-typeset h1 mark{text-decoration:none}.md-search-result .md-typeset h2{color:var(--md-default-fg-color);font-size:.64rem;font-weight:700;line-height:1.6;margin:.5em 0}.md-search-result .md-typeset h2 mark{text-decoration:none}.md-search-result__terms{color:var(--md-default-fg-color);display:block;font-size:.64rem;font-style:italic;margin:.5em 0}.md-search-result mark{background-color:initial;color:var(--md-accent-fg-color);text-decoration:underline}.md-select{position:relative;z-index:1}.md-select__inner{background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color);left:50%;margin-top:.2rem;max-height:0;opacity:0;position:absolute;top:calc(100% - .2rem);transform:translate3d(-50%,.3rem,0);transition:transform .25s 375ms,opacity .25s .25s,max-height 0ms .5s}.md-select:focus-within .md-select__inner,.md-select:hover .md-select__inner{max-height:10rem;opacity:1;transform:translate3d(-50%,0,0);transition:transform .25s cubic-bezier(.1,.7,.1,1),opacity .25s,max-height 0ms}.md-select__inner:after{border-bottom:.2rem solid #0000;border-bottom-color:var(--md-default-bg-color);border-left:.2rem solid #0000;border-right:.2rem solid #0000;border-top:0;content:"";height:0;left:50%;margin-left:-.2rem;margin-top:-.2rem;position:absolute;top:0;width:0}.md-select__list{border-radius:.1rem;font-size:.8rem;list-style-type:none;margin:0;max-height:inherit;overflow:auto;padding:0}.md-select__item{line-height:1.8rem}[dir=ltr] .md-select__link{padding-left:.6rem;padding-right:1.2rem}[dir=rtl] .md-select__link{padding-left:1.2rem;padding-right:.6rem}.md-select__link{cursor:pointer;display:block;outline:none;scroll-snap-align:start;transition:background-color .25s,color .25s;width:100%}.md-select__link:focus,.md-select__link:hover{color:var(--md-accent-fg-color)}.md-select__link:focus{background-color:var(--md-default-fg-color--lightest)}.md-sidebar{align-self:flex-start;flex-shrink:0;padding:1.2rem 0;position:sticky;top:2.4rem;width:12.1rem}@media print{.md-sidebar{display:none}}@media screen and (max-width:76.234375em){[dir=ltr] .md-sidebar--primary{left:-12.1rem}[dir=rtl] .md-sidebar--primary{right:-12.1rem}.md-sidebar--primary{background-color:var(--md-default-bg-color);display:block;height:100%;position:fixed;top:0;transform:translateX(0);transition:transform .25s cubic-bezier(.4,0,.2,1),box-shadow .25s;width:12.1rem;z-index:5}[data-md-toggle=drawer]:checked~.md-container .md-sidebar--primary{box-shadow:var(--md-shadow-z3);transform:translateX(12.1rem)}[dir=rtl] [data-md-toggle=drawer]:checked~.md-container .md-sidebar--primary{transform:translateX(-12.1rem)}.md-sidebar--primary .md-sidebar__scrollwrap{bottom:0;left:0;margin:0;overflow:hidden;position:absolute;right:0;scroll-snap-type:none;top:0}}@media screen and (min-width:76.25em){.md-sidebar{height:0}.no-js .md-sidebar{height:auto}.md-header--lifted~.md-container .md-sidebar{top:4.8rem}}.md-sidebar--secondary{display:none;order:2}@media screen and (min-width:60em){.md-sidebar--secondary{height:0}.no-js .md-sidebar--secondary{height:auto}.md-sidebar--secondary:not([hidden]){display:block}.md-sidebar--secondary .md-sidebar__scrollwrap{touch-action:pan-y}}.md-sidebar__scrollwrap{scrollbar-gutter:stable;-webkit-backface-visibility:hidden;backface-visibility:hidden;margin:0 .2rem;overflow-y:auto;scrollbar-color:var(--md-default-fg-color--lighter) #0000;scrollbar-width:thin}.md-sidebar__scrollwrap::-webkit-scrollbar{height:.2rem;width:.2rem}.md-sidebar__scrollwrap:focus-within,.md-sidebar__scrollwrap:hover{scrollbar-color:var(--md-accent-fg-color) #0000}.md-sidebar__scrollwrap:focus-within::-webkit-scrollbar-thumb,.md-sidebar__scrollwrap:hover::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-sidebar__scrollwrap:focus-within::-webkit-scrollbar-thumb:hover,.md-sidebar__scrollwrap:hover::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}@supports selector(::-webkit-scrollbar){.md-sidebar__scrollwrap{scrollbar-gutter:auto}[dir=ltr] .md-sidebar__inner{padding-right:calc(100% - 11.5rem)}[dir=rtl] .md-sidebar__inner{padding-left:calc(100% - 11.5rem)}}@media screen and (max-width:76.234375em){.md-overlay{background-color:#0000008a;height:0;opacity:0;position:fixed;top:0;transition:width 0ms .25s,height 0ms .25s,opacity .25s;width:0;z-index:5}[data-md-toggle=drawer]:checked~.md-overlay{height:100%;opacity:1;transition:width 0ms,height 0ms,opacity .25s;width:100%}}@keyframes facts{0%{height:0}to{height:.65rem}}@keyframes fact{0%{opacity:0;transform:translateY(100%)}50%{opacity:0}to{opacity:1;transform:translateY(0)}}:root{--md-source-forks-icon:url('data:image/svg+xml;charset=utf-8,');--md-source-repositories-icon:url('data:image/svg+xml;charset=utf-8,');--md-source-stars-icon:url('data:image/svg+xml;charset=utf-8,');--md-source-version-icon:url('data:image/svg+xml;charset=utf-8,')}.md-source{-webkit-backface-visibility:hidden;backface-visibility:hidden;display:block;font-size:.65rem;line-height:1.2;outline-color:var(--md-accent-fg-color);transition:opacity .25s;white-space:nowrap}.md-source:hover{opacity:.7}.md-source__icon{display:inline-block;height:2.4rem;vertical-align:middle;width:2rem}[dir=ltr] .md-source__icon svg{margin-left:.6rem}[dir=rtl] .md-source__icon svg{margin-right:.6rem}.md-source__icon svg{margin-top:.6rem}[dir=ltr] .md-source__icon+.md-source__repository{padding-left:2rem}[dir=rtl] .md-source__icon+.md-source__repository{padding-right:2rem}[dir=ltr] .md-source__icon+.md-source__repository{margin-left:-2rem}[dir=rtl] .md-source__icon+.md-source__repository{margin-right:-2rem}[dir=ltr] .md-source__repository{margin-left:.6rem}[dir=rtl] .md-source__repository{margin-right:.6rem}.md-source__repository{display:inline-block;max-width:calc(100% - 1.2rem);overflow:hidden;text-overflow:ellipsis;vertical-align:middle}.md-source__facts{display:flex;font-size:.55rem;gap:.4rem;list-style-type:none;margin:.1rem 0 0;opacity:.75;overflow:hidden;padding:0;width:100%}.md-source__repository--active .md-source__facts{animation:facts .25s ease-in}.md-source__fact{overflow:hidden;text-overflow:ellipsis}.md-source__repository--active .md-source__fact{animation:fact .4s ease-out}[dir=ltr] .md-source__fact:before{margin-right:.1rem}[dir=rtl] .md-source__fact:before{margin-left:.1rem}.md-source__fact:before{background-color:currentcolor;content:"";display:inline-block;height:.6rem;-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;vertical-align:text-top;width:.6rem}.md-source__fact:nth-child(1n+2){flex-shrink:0}.md-source__fact--version:before{-webkit-mask-image:var(--md-source-version-icon);mask-image:var(--md-source-version-icon)}.md-source__fact--stars:before{-webkit-mask-image:var(--md-source-stars-icon);mask-image:var(--md-source-stars-icon)}.md-source__fact--forks:before{-webkit-mask-image:var(--md-source-forks-icon);mask-image:var(--md-source-forks-icon)}.md-source__fact--repositories:before{-webkit-mask-image:var(--md-source-repositories-icon);mask-image:var(--md-source-repositories-icon)}:root{--md-status:url('data:image/svg+xml;charset=utf-8,');--md-status--new:url('data:image/svg+xml;charset=utf-8,');--md-status--deprecated:url('data:image/svg+xml;charset=utf-8,');--md-status--encrypted:url('data:image/svg+xml;charset=utf-8,')}.md-status:after{background-color:var(--md-default-fg-color--light);content:"";display:inline-block;height:1.125em;-webkit-mask-image:var(--md-status);mask-image:var(--md-status);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;vertical-align:text-bottom;width:1.125em}.md-status:hover:after{background-color:currentcolor}.md-status--new:after{-webkit-mask-image:var(--md-status--new);mask-image:var(--md-status--new)}.md-status--deprecated:after{-webkit-mask-image:var(--md-status--deprecated);mask-image:var(--md-status--deprecated)}.md-status--encrypted:after{-webkit-mask-image:var(--md-status--encrypted);mask-image:var(--md-status--encrypted)}.md-tabs{background-color:var(--md-primary-fg-color);color:var(--md-primary-bg-color);display:block;line-height:1.3;overflow:auto;width:100%;z-index:3}@media print{.md-tabs{display:none}}@media screen and (max-width:76.234375em){.md-tabs{display:none}}.md-tabs[hidden]{pointer-events:none}[dir=ltr] .md-tabs__list{margin-left:.2rem}[dir=rtl] .md-tabs__list{margin-right:.2rem}.md-tabs__list{contain:content;display:flex;list-style:none;margin:0;overflow:auto;padding:0;scrollbar-width:none;white-space:nowrap}.md-tabs__list::-webkit-scrollbar{display:none}.md-tabs__item{height:2.4rem;padding-left:.6rem;padding-right:.6rem}.md-tabs__item--active .md-tabs__link{color:inherit;opacity:1}.md-tabs__link{-webkit-backface-visibility:hidden;backface-visibility:hidden;display:flex;font-size:.7rem;margin-top:.8rem;opacity:.7;outline-color:var(--md-accent-fg-color);outline-offset:.2rem;transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .25s}.md-tabs__link:focus,.md-tabs__link:hover{color:inherit;opacity:1}[dir=ltr] .md-tabs__link svg{margin-right:.4rem}[dir=rtl] .md-tabs__link svg{margin-left:.4rem}.md-tabs__link svg{fill:currentcolor;height:1.3em}.md-tabs__item:nth-child(2) .md-tabs__link{transition-delay:20ms}.md-tabs__item:nth-child(3) .md-tabs__link{transition-delay:40ms}.md-tabs__item:nth-child(4) .md-tabs__link{transition-delay:60ms}.md-tabs__item:nth-child(5) .md-tabs__link{transition-delay:80ms}.md-tabs__item:nth-child(6) .md-tabs__link{transition-delay:.1s}.md-tabs__item:nth-child(7) .md-tabs__link{transition-delay:.12s}.md-tabs__item:nth-child(8) .md-tabs__link{transition-delay:.14s}.md-tabs__item:nth-child(9) .md-tabs__link{transition-delay:.16s}.md-tabs__item:nth-child(10) .md-tabs__link{transition-delay:.18s}.md-tabs__item:nth-child(11) .md-tabs__link{transition-delay:.2s}.md-tabs__item:nth-child(12) .md-tabs__link{transition-delay:.22s}.md-tabs__item:nth-child(13) .md-tabs__link{transition-delay:.24s}.md-tabs__item:nth-child(14) .md-tabs__link{transition-delay:.26s}.md-tabs__item:nth-child(15) .md-tabs__link{transition-delay:.28s}.md-tabs__item:nth-child(16) .md-tabs__link{transition-delay:.3s}.md-tabs[hidden] .md-tabs__link{opacity:0;transform:translateY(50%);transition:transform 0ms .1s,opacity .1s}:root{--md-tag-icon:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .md-tags:not([hidden]){display:inline-flex;flex-wrap:wrap;gap:.5em;margin-bottom:.75em;margin-top:-.125em}.md-typeset .md-tag{align-items:center;background:var(--md-default-fg-color--lightest);border-radius:2.4rem;display:inline-flex;font-size:.64rem;font-size:min(.8em,.64rem);font-weight:700;gap:.5em;letter-spacing:normal;line-height:1.6;padding:.3125em .78125em}.md-typeset .md-tag[href]{-webkit-tap-highlight-color:transparent;color:inherit;outline:none;transition:color 125ms,background-color 125ms}.md-typeset .md-tag[href]:focus,.md-typeset .md-tag[href]:hover{background-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}[id]>.md-typeset .md-tag{vertical-align:text-top}.md-typeset .md-tag-icon:before{background-color:var(--md-default-fg-color--lighter);content:"";display:inline-block;height:1.2em;-webkit-mask-image:var(--md-tag-icon);mask-image:var(--md-tag-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color 125ms;vertical-align:text-bottom;width:1.2em}.md-typeset .md-tag-icon[href]:focus:before,.md-typeset .md-tag-icon[href]:hover:before{background-color:var(--md-accent-bg-color)}@keyframes pulse{0%{transform:scale(.95)}75%{transform:scale(1)}to{transform:scale(.95)}}:root{--md-annotation-bg-icon:url('data:image/svg+xml;charset=utf-8,');--md-annotation-icon:url('data:image/svg+xml;charset=utf-8,');--md-tooltip-width:20rem}.md-tooltip{-webkit-backface-visibility:hidden;backface-visibility:hidden;background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color);font-family:var(--md-text-font-family);left:clamp(var(--md-tooltip-0,0rem) + .8rem,var(--md-tooltip-x),100vw + var(--md-tooltip-0,0rem) + .8rem - var(--md-tooltip-width) - 2 * .8rem);max-width:calc(100vw - 1.6rem);opacity:0;position:absolute;top:var(--md-tooltip-y);transform:translateY(-.4rem);transition:transform 0ms .25s,opacity .25s,z-index .25s;width:var(--md-tooltip-width);z-index:0}.md-tooltip--active{opacity:1;transform:translateY(0);transition:transform .25s cubic-bezier(.1,.7,.1,1),opacity .25s,z-index 0ms;z-index:2}.focus-visible>.md-tooltip,.md-tooltip:target{outline:var(--md-accent-fg-color) auto}.md-tooltip__inner{font-size:.64rem;padding:.8rem}.md-tooltip__inner.md-typeset>:first-child{margin-top:0}.md-tooltip__inner.md-typeset>:last-child{margin-bottom:0}.md-annotation{font-weight:400;outline:none;vertical-align:text-bottom;white-space:normal}[dir=rtl] .md-annotation{direction:rtl}code .md-annotation{font-family:var(--md-code-font-family);font-size:inherit}.md-annotation:not([hidden]){display:inline-block;line-height:1.25}.md-annotation__index{border-radius:.01px;cursor:pointer;display:inline-block;margin-left:.4ch;margin-right:.4ch;outline:none;overflow:hidden;position:relative;-webkit-user-select:none;user-select:none;vertical-align:text-top;z-index:0}.md-annotation .md-annotation__index{transition:z-index .25s}@media screen{.md-annotation__index{width:2.2ch}[data-md-visible]>.md-annotation__index{animation:pulse 2s infinite}.md-annotation__index:before{background:var(--md-default-bg-color);-webkit-mask-image:var(--md-annotation-bg-icon);mask-image:var(--md-annotation-bg-icon)}.md-annotation__index:after,.md-annotation__index:before{content:"";height:2.2ch;-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:-.1ch;width:2.2ch;z-index:-1}.md-annotation__index:after{background-color:var(--md-default-fg-color--lighter);-webkit-mask-image:var(--md-annotation-icon);mask-image:var(--md-annotation-icon);transform:scale(1.0001);transition:background-color .25s,transform .25s}.md-tooltip--active+.md-annotation__index:after{transform:rotate(45deg)}.md-tooltip--active+.md-annotation__index:after,:hover>.md-annotation__index:after{background-color:var(--md-accent-fg-color)}}.md-tooltip--active+.md-annotation__index{animation-play-state:paused;transition-duration:0ms;z-index:2}.md-annotation__index [data-md-annotation-id]{display:inline-block}@media print{.md-annotation__index [data-md-annotation-id]{background:var(--md-default-fg-color--lighter);border-radius:2ch;color:var(--md-default-bg-color);font-weight:700;padding:0 .6ch;white-space:nowrap}.md-annotation__index [data-md-annotation-id]:after{content:attr(data-md-annotation-id)}}.md-typeset .md-annotation-list{counter-reset:xxx;list-style:none}.md-typeset .md-annotation-list li{position:relative}[dir=ltr] .md-typeset .md-annotation-list li:before{left:-2.125em}[dir=rtl] .md-typeset .md-annotation-list li:before{right:-2.125em}.md-typeset .md-annotation-list li:before{background:var(--md-default-fg-color--lighter);border-radius:2ch;color:var(--md-default-bg-color);content:counter(xxx);counter-increment:xxx;font-size:.8875em;font-weight:700;height:2ch;line-height:1.25;min-width:2ch;padding:0 .6ch;position:absolute;text-align:center;top:.25em}[dir=ltr] .md-top{margin-left:50%}[dir=rtl] .md-top{margin-right:50%}.md-top{background-color:var(--md-default-bg-color);border-radius:1.6rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color--light);cursor:pointer;display:block;font-size:.7rem;outline:none;padding:.4rem .8rem;position:fixed;top:3.2rem;transform:translate(-50%);transition:color 125ms,background-color 125ms,transform 125ms cubic-bezier(.4,0,.2,1),opacity 125ms;z-index:2}@media print{.md-top{display:none}}[dir=rtl] .md-top{transform:translate(50%)}.md-top[hidden]{opacity:0;pointer-events:none;transform:translate(-50%,.2rem);transition-duration:0ms}[dir=rtl] .md-top[hidden]{transform:translate(50%,.2rem)}.md-top:focus,.md-top:hover{background-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}.md-top svg{display:inline-block;vertical-align:-.5em}@keyframes hoverfix{0%{pointer-events:none}}:root{--md-version-icon:url('data:image/svg+xml;charset=utf-8,')}.md-version{flex-shrink:0;font-size:.8rem;height:2.4rem}[dir=ltr] .md-version__current{margin-left:1.4rem;margin-right:.4rem}[dir=rtl] .md-version__current{margin-left:.4rem;margin-right:1.4rem}.md-version__current{color:inherit;cursor:pointer;outline:none;position:relative;top:.05rem}[dir=ltr] .md-version__current:after{margin-left:.4rem}[dir=rtl] .md-version__current:after{margin-right:.4rem}.md-version__current:after{background-color:currentcolor;content:"";display:inline-block;height:.6rem;-webkit-mask-image:var(--md-version-icon);mask-image:var(--md-version-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:.4rem}.md-version__list{background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color);list-style-type:none;margin:.2rem .8rem;max-height:0;opacity:0;overflow:auto;padding:0;position:absolute;scroll-snap-type:y mandatory;top:.15rem;transition:max-height 0ms .5s,opacity .25s .25s;z-index:3}.md-version:focus-within .md-version__list,.md-version:hover .md-version__list{max-height:10rem;opacity:1;transition:max-height 0ms,opacity .25s}@media (hover:none),(pointer:coarse){.md-version:hover .md-version__list{animation:hoverfix .25s forwards}.md-version:focus-within .md-version__list{animation:none}}.md-version__item{line-height:1.8rem}[dir=ltr] .md-version__link{padding-left:.6rem;padding-right:1.2rem}[dir=rtl] .md-version__link{padding-left:1.2rem;padding-right:.6rem}.md-version__link{cursor:pointer;display:block;outline:none;scroll-snap-align:start;transition:color .25s,background-color .25s;white-space:nowrap;width:100%}.md-version__link:focus,.md-version__link:hover{color:var(--md-accent-fg-color)}.md-version__link:focus{background-color:var(--md-default-fg-color--lightest)}:root{--md-admonition-icon--note:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--abstract:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--info:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--tip:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--success:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--question:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--warning:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--failure:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--danger:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--bug:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--example:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--quote:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .admonition,.md-typeset details{background-color:var(--md-admonition-bg-color);border:.075rem solid #448aff;border-radius:.2rem;box-shadow:var(--md-shadow-z1);color:var(--md-admonition-fg-color);display:flow-root;font-size:.64rem;margin:1.5625em 0;padding:0 .6rem;page-break-inside:avoid;transition:box-shadow 125ms}@media print{.md-typeset .admonition,.md-typeset details{box-shadow:none}}.md-typeset .admonition:focus-within,.md-typeset details:focus-within{box-shadow:0 0 0 .2rem #448aff1a}.md-typeset .admonition>*,.md-typeset details>*{box-sizing:border-box}.md-typeset .admonition .admonition,.md-typeset .admonition details,.md-typeset details .admonition,.md-typeset details details{margin-bottom:1em;margin-top:1em}.md-typeset .admonition .md-typeset__scrollwrap,.md-typeset details .md-typeset__scrollwrap{margin:1em -.6rem}.md-typeset .admonition .md-typeset__table,.md-typeset details .md-typeset__table{padding:0 .6rem}.md-typeset .admonition>.tabbed-set:only-child,.md-typeset details>.tabbed-set:only-child{margin-top:0}html .md-typeset .admonition>:last-child,html .md-typeset details>:last-child{margin-bottom:.6rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary{padding-left:2rem;padding-right:.6rem}[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{padding-left:.6rem;padding-right:2rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary{border-left-width:.2rem}[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{border-right-width:.2rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary{border-top-left-radius:.1rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary,[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{border-top-right-radius:.1rem}[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{border-top-left-radius:.1rem}.md-typeset .admonition-title,.md-typeset summary{background-color:#448aff1a;border:none;font-weight:700;margin:0 -.6rem;padding-bottom:.4rem;padding-top:.4rem;position:relative}html .md-typeset .admonition-title:last-child,html .md-typeset summary:last-child{margin-bottom:0}[dir=ltr] .md-typeset .admonition-title:before,[dir=ltr] .md-typeset summary:before{left:.6rem}[dir=rtl] .md-typeset .admonition-title:before,[dir=rtl] .md-typeset summary:before{right:.6rem}.md-typeset .admonition-title:before,.md-typeset summary:before{background-color:#448aff;content:"";height:1rem;-webkit-mask-image:var(--md-admonition-icon--note);mask-image:var(--md-admonition-icon--note);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.625em;width:1rem}.md-typeset .admonition-title code,.md-typeset summary code{box-shadow:0 0 0 .05rem var(--md-default-fg-color--lightest)}.md-typeset .admonition.note,.md-typeset details.note{border-color:#448aff}.md-typeset .admonition.note:focus-within,.md-typeset details.note:focus-within{box-shadow:0 0 0 .2rem #448aff1a}.md-typeset .note>.admonition-title,.md-typeset .note>summary{background-color:#448aff1a}.md-typeset .note>.admonition-title:before,.md-typeset .note>summary:before{background-color:#448aff;-webkit-mask-image:var(--md-admonition-icon--note);mask-image:var(--md-admonition-icon--note)}.md-typeset .note>.admonition-title:after,.md-typeset .note>summary:after{color:#448aff}.md-typeset .admonition.abstract,.md-typeset details.abstract{border-color:#00b0ff}.md-typeset .admonition.abstract:focus-within,.md-typeset details.abstract:focus-within{box-shadow:0 0 0 .2rem #00b0ff1a}.md-typeset .abstract>.admonition-title,.md-typeset .abstract>summary{background-color:#00b0ff1a}.md-typeset .abstract>.admonition-title:before,.md-typeset .abstract>summary:before{background-color:#00b0ff;-webkit-mask-image:var(--md-admonition-icon--abstract);mask-image:var(--md-admonition-icon--abstract)}.md-typeset .abstract>.admonition-title:after,.md-typeset .abstract>summary:after{color:#00b0ff}.md-typeset .admonition.info,.md-typeset details.info{border-color:#00b8d4}.md-typeset .admonition.info:focus-within,.md-typeset details.info:focus-within{box-shadow:0 0 0 .2rem #00b8d41a}.md-typeset .info>.admonition-title,.md-typeset .info>summary{background-color:#00b8d41a}.md-typeset .info>.admonition-title:before,.md-typeset .info>summary:before{background-color:#00b8d4;-webkit-mask-image:var(--md-admonition-icon--info);mask-image:var(--md-admonition-icon--info)}.md-typeset .info>.admonition-title:after,.md-typeset .info>summary:after{color:#00b8d4}.md-typeset .admonition.tip,.md-typeset details.tip{border-color:#00bfa5}.md-typeset .admonition.tip:focus-within,.md-typeset details.tip:focus-within{box-shadow:0 0 0 .2rem #00bfa51a}.md-typeset .tip>.admonition-title,.md-typeset .tip>summary{background-color:#00bfa51a}.md-typeset .tip>.admonition-title:before,.md-typeset .tip>summary:before{background-color:#00bfa5;-webkit-mask-image:var(--md-admonition-icon--tip);mask-image:var(--md-admonition-icon--tip)}.md-typeset .tip>.admonition-title:after,.md-typeset .tip>summary:after{color:#00bfa5}.md-typeset .admonition.success,.md-typeset details.success{border-color:#00c853}.md-typeset .admonition.success:focus-within,.md-typeset details.success:focus-within{box-shadow:0 0 0 .2rem #00c8531a}.md-typeset .success>.admonition-title,.md-typeset .success>summary{background-color:#00c8531a}.md-typeset .success>.admonition-title:before,.md-typeset .success>summary:before{background-color:#00c853;-webkit-mask-image:var(--md-admonition-icon--success);mask-image:var(--md-admonition-icon--success)}.md-typeset .success>.admonition-title:after,.md-typeset .success>summary:after{color:#00c853}.md-typeset .admonition.question,.md-typeset details.question{border-color:#64dd17}.md-typeset .admonition.question:focus-within,.md-typeset details.question:focus-within{box-shadow:0 0 0 .2rem #64dd171a}.md-typeset .question>.admonition-title,.md-typeset .question>summary{background-color:#64dd171a}.md-typeset .question>.admonition-title:before,.md-typeset .question>summary:before{background-color:#64dd17;-webkit-mask-image:var(--md-admonition-icon--question);mask-image:var(--md-admonition-icon--question)}.md-typeset .question>.admonition-title:after,.md-typeset .question>summary:after{color:#64dd17}.md-typeset .admonition.warning,.md-typeset details.warning{border-color:#ff9100}.md-typeset .admonition.warning:focus-within,.md-typeset details.warning:focus-within{box-shadow:0 0 0 .2rem #ff91001a}.md-typeset .warning>.admonition-title,.md-typeset .warning>summary{background-color:#ff91001a}.md-typeset .warning>.admonition-title:before,.md-typeset .warning>summary:before{background-color:#ff9100;-webkit-mask-image:var(--md-admonition-icon--warning);mask-image:var(--md-admonition-icon--warning)}.md-typeset .warning>.admonition-title:after,.md-typeset .warning>summary:after{color:#ff9100}.md-typeset .admonition.failure,.md-typeset details.failure{border-color:#ff5252}.md-typeset .admonition.failure:focus-within,.md-typeset details.failure:focus-within{box-shadow:0 0 0 .2rem #ff52521a}.md-typeset .failure>.admonition-title,.md-typeset .failure>summary{background-color:#ff52521a}.md-typeset .failure>.admonition-title:before,.md-typeset .failure>summary:before{background-color:#ff5252;-webkit-mask-image:var(--md-admonition-icon--failure);mask-image:var(--md-admonition-icon--failure)}.md-typeset .failure>.admonition-title:after,.md-typeset .failure>summary:after{color:#ff5252}.md-typeset .admonition.danger,.md-typeset details.danger{border-color:#ff1744}.md-typeset .admonition.danger:focus-within,.md-typeset details.danger:focus-within{box-shadow:0 0 0 .2rem #ff17441a}.md-typeset .danger>.admonition-title,.md-typeset .danger>summary{background-color:#ff17441a}.md-typeset .danger>.admonition-title:before,.md-typeset .danger>summary:before{background-color:#ff1744;-webkit-mask-image:var(--md-admonition-icon--danger);mask-image:var(--md-admonition-icon--danger)}.md-typeset .danger>.admonition-title:after,.md-typeset .danger>summary:after{color:#ff1744}.md-typeset .admonition.bug,.md-typeset details.bug{border-color:#f50057}.md-typeset .admonition.bug:focus-within,.md-typeset details.bug:focus-within{box-shadow:0 0 0 .2rem #f500571a}.md-typeset .bug>.admonition-title,.md-typeset .bug>summary{background-color:#f500571a}.md-typeset .bug>.admonition-title:before,.md-typeset .bug>summary:before{background-color:#f50057;-webkit-mask-image:var(--md-admonition-icon--bug);mask-image:var(--md-admonition-icon--bug)}.md-typeset .bug>.admonition-title:after,.md-typeset .bug>summary:after{color:#f50057}.md-typeset .admonition.example,.md-typeset details.example{border-color:#7c4dff}.md-typeset .admonition.example:focus-within,.md-typeset details.example:focus-within{box-shadow:0 0 0 .2rem #7c4dff1a}.md-typeset .example>.admonition-title,.md-typeset .example>summary{background-color:#7c4dff1a}.md-typeset .example>.admonition-title:before,.md-typeset .example>summary:before{background-color:#7c4dff;-webkit-mask-image:var(--md-admonition-icon--example);mask-image:var(--md-admonition-icon--example)}.md-typeset .example>.admonition-title:after,.md-typeset .example>summary:after{color:#7c4dff}.md-typeset .admonition.quote,.md-typeset details.quote{border-color:#9e9e9e}.md-typeset .admonition.quote:focus-within,.md-typeset details.quote:focus-within{box-shadow:0 0 0 .2rem #9e9e9e1a}.md-typeset .quote>.admonition-title,.md-typeset .quote>summary{background-color:#9e9e9e1a}.md-typeset .quote>.admonition-title:before,.md-typeset .quote>summary:before{background-color:#9e9e9e;-webkit-mask-image:var(--md-admonition-icon--quote);mask-image:var(--md-admonition-icon--quote)}.md-typeset .quote>.admonition-title:after,.md-typeset .quote>summary:after{color:#9e9e9e}:root{--md-footnotes-icon:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .footnote{color:var(--md-default-fg-color--light);font-size:.64rem}[dir=ltr] .md-typeset .footnote>ol{margin-left:0}[dir=rtl] .md-typeset .footnote>ol{margin-right:0}.md-typeset .footnote>ol>li{transition:color 125ms}.md-typeset .footnote>ol>li:target{color:var(--md-default-fg-color)}.md-typeset .footnote>ol>li:focus-within .footnote-backref{opacity:1;transform:translateX(0);transition:none}.md-typeset .footnote>ol>li:hover .footnote-backref,.md-typeset .footnote>ol>li:target .footnote-backref{opacity:1;transform:translateX(0)}.md-typeset .footnote>ol>li>:first-child{margin-top:0}.md-typeset .footnote-ref{font-size:.75em;font-weight:700}html .md-typeset .footnote-ref{outline-offset:.1rem}.md-typeset [id^="fnref:"]:target>.footnote-ref{outline:auto}.md-typeset .footnote-backref{color:var(--md-typeset-a-color);display:inline-block;font-size:0;opacity:0;transform:translateX(.25rem);transition:color .25s,transform .25s .25s,opacity 125ms .25s;vertical-align:text-bottom}@media print{.md-typeset .footnote-backref{color:var(--md-typeset-a-color);opacity:1;transform:translateX(0)}}[dir=rtl] .md-typeset .footnote-backref{transform:translateX(-.25rem)}.md-typeset .footnote-backref:hover{color:var(--md-accent-fg-color)}.md-typeset .footnote-backref:before{background-color:currentcolor;content:"";display:inline-block;height:.8rem;-webkit-mask-image:var(--md-footnotes-icon);mask-image:var(--md-footnotes-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:.8rem}[dir=rtl] .md-typeset .footnote-backref:before svg{transform:scaleX(-1)}[dir=ltr] .md-typeset .headerlink{margin-left:.5rem}[dir=rtl] .md-typeset .headerlink{margin-right:.5rem}.md-typeset .headerlink{color:var(--md-default-fg-color--lighter);display:inline-block;opacity:0;transition:color .25s,opacity 125ms}@media print{.md-typeset .headerlink{display:none}}.md-typeset .headerlink:focus,.md-typeset :hover>.headerlink,.md-typeset :target>.headerlink{opacity:1;transition:color .25s,opacity 125ms}.md-typeset .headerlink:focus,.md-typeset .headerlink:hover,.md-typeset :target>.headerlink{color:var(--md-accent-fg-color)}.md-typeset :target{--md-scroll-margin:3.6rem;--md-scroll-offset:0rem;scroll-margin-top:calc(var(--md-scroll-margin) - var(--md-scroll-offset))}@media screen and (min-width:76.25em){.md-header--lifted~.md-container .md-typeset :target{--md-scroll-margin:6rem}}.md-typeset h1:target,.md-typeset h2:target,.md-typeset h3:target{--md-scroll-offset:0.2rem}.md-typeset h4:target{--md-scroll-offset:0.15rem}.md-typeset div.arithmatex{overflow:auto}@media screen and (max-width:44.984375em){.md-typeset div.arithmatex{margin:0 -.8rem}}.md-typeset div.arithmatex>*{margin-left:auto!important;margin-right:auto!important;padding:0 .8rem;touch-action:auto;width:-webkit-min-content;width:min-content}.md-typeset div.arithmatex>* mjx-container{margin:0!important}.md-typeset del.critic{background-color:var(--md-typeset-del-color)}.md-typeset del.critic,.md-typeset ins.critic{-webkit-box-decoration-break:clone;box-decoration-break:clone}.md-typeset ins.critic{background-color:var(--md-typeset-ins-color)}.md-typeset .critic.comment{-webkit-box-decoration-break:clone;box-decoration-break:clone;color:var(--md-code-hl-comment-color)}.md-typeset .critic.comment:before{content:"/* "}.md-typeset .critic.comment:after{content:" */"}.md-typeset .critic.block{box-shadow:none;display:block;margin:1em 0;overflow:auto;padding-left:.8rem;padding-right:.8rem}.md-typeset .critic.block>:first-child{margin-top:.5em}.md-typeset .critic.block>:last-child{margin-bottom:.5em}:root{--md-details-icon:url('data:image/svg+xml;charset=utf-8,')}.md-typeset details{display:flow-root;overflow:visible;padding-top:0}.md-typeset details[open]>summary:after{transform:rotate(90deg)}.md-typeset details:not([open]){box-shadow:none;padding-bottom:0}.md-typeset details:not([open])>summary{border-radius:.1rem}[dir=ltr] .md-typeset summary{padding-right:1.8rem}[dir=rtl] .md-typeset summary{padding-left:1.8rem}[dir=ltr] .md-typeset summary{border-top-left-radius:.1rem}[dir=ltr] .md-typeset summary,[dir=rtl] .md-typeset summary{border-top-right-radius:.1rem}[dir=rtl] .md-typeset summary{border-top-left-radius:.1rem}.md-typeset summary{cursor:pointer;display:block;min-height:1rem}.md-typeset summary.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-typeset summary:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}[dir=ltr] .md-typeset summary:after{right:.4rem}[dir=rtl] .md-typeset summary:after{left:.4rem}.md-typeset summary:after{background-color:currentcolor;content:"";height:1rem;-webkit-mask-image:var(--md-details-icon);mask-image:var(--md-details-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.625em;transform:rotate(0deg);transition:transform .25s;width:1rem}[dir=rtl] .md-typeset summary:after{transform:rotate(180deg)}.md-typeset summary::marker{display:none}.md-typeset summary::-webkit-details-marker{display:none}.md-typeset .emojione,.md-typeset .gemoji,.md-typeset .twemoji{display:inline-flex;height:1.125em;vertical-align:text-top}.md-typeset .emojione svg,.md-typeset .gemoji svg,.md-typeset .twemoji svg{fill:currentcolor;max-height:100%;width:1.125em}.highlight .o,.highlight .ow{color:var(--md-code-hl-operator-color)}.highlight .p{color:var(--md-code-hl-punctuation-color)}.highlight .cpf,.highlight .l,.highlight .s,.highlight .s1,.highlight .s2,.highlight .sb,.highlight .sc,.highlight .si,.highlight .ss{color:var(--md-code-hl-string-color)}.highlight .cp,.highlight .se,.highlight .sh,.highlight .sr,.highlight .sx{color:var(--md-code-hl-special-color)}.highlight .il,.highlight .m,.highlight .mb,.highlight .mf,.highlight .mh,.highlight .mi,.highlight .mo{color:var(--md-code-hl-number-color)}.highlight .k,.highlight .kd,.highlight .kn,.highlight .kp,.highlight .kr,.highlight .kt{color:var(--md-code-hl-keyword-color)}.highlight .kc,.highlight .n{color:var(--md-code-hl-name-color)}.highlight .bp,.highlight .nb,.highlight .no{color:var(--md-code-hl-constant-color)}.highlight .nc,.highlight .ne,.highlight .nf,.highlight .nn{color:var(--md-code-hl-function-color)}.highlight .nd,.highlight .ni,.highlight .nl,.highlight .nt{color:var(--md-code-hl-keyword-color)}.highlight .c,.highlight .c1,.highlight .ch,.highlight .cm,.highlight .cs,.highlight .sd{color:var(--md-code-hl-comment-color)}.highlight .na,.highlight .nv,.highlight .vc,.highlight .vg,.highlight .vi{color:var(--md-code-hl-variable-color)}.highlight .ge,.highlight .gh,.highlight .go,.highlight .gp,.highlight .gr,.highlight .gs,.highlight .gt,.highlight .gu{color:var(--md-code-hl-generic-color)}.highlight .gd,.highlight .gi{border-radius:.1rem;margin:0 -.125em;padding:0 .125em}.highlight .gd{background-color:var(--md-typeset-del-color)}.highlight .gi{background-color:var(--md-typeset-ins-color)}.highlight .hll{background-color:var(--md-code-hl-color--light);box-shadow:2px 0 0 0 var(--md-code-hl-color) inset;display:block;margin:0 -1.1764705882em;padding:0 1.1764705882em}.highlight span.filename{background-color:var(--md-code-bg-color);border-bottom:.05rem solid var(--md-default-fg-color--lightest);border-top-left-radius:.1rem;border-top-right-radius:.1rem;display:flow-root;font-size:.85em;font-weight:700;margin-top:1em;padding:.6617647059em 1.1764705882em;position:relative}.highlight span.filename+pre{margin-top:0}.highlight span.filename+pre>code{border-top-left-radius:0;border-top-right-radius:0}.highlight [data-linenos]:before{background-color:var(--md-code-bg-color);box-shadow:-.05rem 0 var(--md-default-fg-color--lightest) inset;color:var(--md-default-fg-color--light);content:attr(data-linenos);float:left;left:-1.1764705882em;margin-left:-1.1764705882em;margin-right:1.1764705882em;padding-left:1.1764705882em;position:sticky;-webkit-user-select:none;user-select:none;z-index:3}.highlight code a[id]{position:absolute;visibility:hidden}.highlight code[data-md-copying] .hll{display:contents}.highlight code[data-md-copying] .md-annotation{display:none}.highlighttable{display:flow-root}.highlighttable tbody,.highlighttable td{display:block;padding:0}.highlighttable tr{display:flex}.highlighttable pre{margin:0}.highlighttable th.filename{flex-grow:1;padding:0;text-align:left}.highlighttable th.filename span.filename{margin-top:0}.highlighttable .linenos{background-color:var(--md-code-bg-color);border-bottom-left-radius:.1rem;border-top-left-radius:.1rem;font-size:.85em;padding:.7720588235em 0 .7720588235em 1.1764705882em;-webkit-user-select:none;user-select:none}.highlighttable .linenodiv{box-shadow:-.05rem 0 var(--md-default-fg-color--lightest) inset;padding-right:.5882352941em}.highlighttable .linenodiv pre{color:var(--md-default-fg-color--light);text-align:right}.highlighttable .code{flex:1;min-width:0}.linenodiv a{color:inherit}.md-typeset .highlighttable{direction:ltr;margin:1em 0}.md-typeset .highlighttable>tbody>tr>.code>div>pre>code{border-bottom-left-radius:0;border-top-left-radius:0}.md-typeset .highlight+.result{border:.05rem solid var(--md-code-bg-color);border-bottom-left-radius:.1rem;border-bottom-right-radius:.1rem;border-top-width:.1rem;margin-top:-1.125em;overflow:visible;padding:0 1em}.md-typeset .highlight+.result:after{clear:both;content:"";display:block}@media screen and (max-width:44.984375em){.md-content__inner>.highlight{margin:1em -.8rem}.md-content__inner>.highlight>.filename,.md-content__inner>.highlight>.highlighttable>tbody>tr>.code>div>pre>code,.md-content__inner>.highlight>.highlighttable>tbody>tr>.filename span.filename,.md-content__inner>.highlight>.highlighttable>tbody>tr>.linenos,.md-content__inner>.highlight>pre>code{border-radius:0}.md-content__inner>.highlight+.result{border-left-width:0;border-radius:0;border-right-width:0;margin-left:-.8rem;margin-right:-.8rem}}.md-typeset .keys kbd:after,.md-typeset .keys kbd:before{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;color:inherit;margin:0;position:relative}.md-typeset .keys span{color:var(--md-default-fg-color--light);padding:0 .2em}.md-typeset .keys .key-alt:before,.md-typeset .keys .key-left-alt:before,.md-typeset .keys .key-right-alt:before{content:"⎇";padding-right:.4em}.md-typeset .keys .key-command:before,.md-typeset .keys .key-left-command:before,.md-typeset .keys .key-right-command:before{content:"⌘";padding-right:.4em}.md-typeset .keys .key-control:before,.md-typeset .keys .key-left-control:before,.md-typeset .keys .key-right-control:before{content:"⌃";padding-right:.4em}.md-typeset .keys .key-left-meta:before,.md-typeset .keys .key-meta:before,.md-typeset .keys .key-right-meta:before{content:"◆";padding-right:.4em}.md-typeset .keys .key-left-option:before,.md-typeset .keys .key-option:before,.md-typeset .keys .key-right-option:before{content:"⌥";padding-right:.4em}.md-typeset .keys .key-left-shift:before,.md-typeset .keys .key-right-shift:before,.md-typeset .keys .key-shift:before{content:"⇧";padding-right:.4em}.md-typeset .keys .key-left-super:before,.md-typeset .keys .key-right-super:before,.md-typeset .keys .key-super:before{content:"❖";padding-right:.4em}.md-typeset .keys .key-left-windows:before,.md-typeset .keys .key-right-windows:before,.md-typeset .keys .key-windows:before{content:"⊞";padding-right:.4em}.md-typeset .keys .key-arrow-down:before{content:"↓";padding-right:.4em}.md-typeset .keys .key-arrow-left:before{content:"←";padding-right:.4em}.md-typeset .keys .key-arrow-right:before{content:"→";padding-right:.4em}.md-typeset .keys .key-arrow-up:before{content:"↑";padding-right:.4em}.md-typeset .keys .key-backspace:before{content:"⌫";padding-right:.4em}.md-typeset .keys .key-backtab:before{content:"⇤";padding-right:.4em}.md-typeset .keys .key-caps-lock:before{content:"⇪";padding-right:.4em}.md-typeset .keys .key-clear:before{content:"⌧";padding-right:.4em}.md-typeset .keys .key-context-menu:before{content:"☰";padding-right:.4em}.md-typeset .keys .key-delete:before{content:"⌦";padding-right:.4em}.md-typeset .keys .key-eject:before{content:"⏏";padding-right:.4em}.md-typeset .keys .key-end:before{content:"⤓";padding-right:.4em}.md-typeset .keys .key-escape:before{content:"⎋";padding-right:.4em}.md-typeset .keys .key-home:before{content:"⤒";padding-right:.4em}.md-typeset .keys .key-insert:before{content:"⎀";padding-right:.4em}.md-typeset .keys .key-page-down:before{content:"⇟";padding-right:.4em}.md-typeset .keys .key-page-up:before{content:"⇞";padding-right:.4em}.md-typeset .keys .key-print-screen:before{content:"⎙";padding-right:.4em}.md-typeset .keys .key-tab:after{content:"⇥";padding-left:.4em}.md-typeset .keys .key-num-enter:after{content:"⌤";padding-left:.4em}.md-typeset .keys .key-enter:after{content:"⏎";padding-left:.4em}:root{--md-tabbed-icon--prev:url('data:image/svg+xml;charset=utf-8,');--md-tabbed-icon--next:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .tabbed-set{border-radius:.1rem;display:flex;flex-flow:column wrap;margin:1em 0;position:relative}.md-typeset .tabbed-set>input{height:0;opacity:0;position:absolute;width:0}.md-typeset .tabbed-set>input:target{--md-scroll-offset:0.625em}.md-typeset .tabbed-set>input.focus-visible~.tabbed-labels:before{background-color:var(--md-accent-fg-color)}.md-typeset .tabbed-labels{-ms-overflow-style:none;box-shadow:0 -.05rem var(--md-default-fg-color--lightest) inset;display:flex;max-width:100%;overflow:auto;scrollbar-width:none}@media print{.md-typeset .tabbed-labels{display:contents}}@media screen{.js .md-typeset .tabbed-labels{position:relative}.js .md-typeset .tabbed-labels:before{background:var(--md-default-fg-color);bottom:0;content:"";display:block;height:2px;left:0;position:absolute;transform:translateX(var(--md-indicator-x));transition:width 225ms,background-color .25s,transform .25s;transition-timing-function:cubic-bezier(.4,0,.2,1);width:var(--md-indicator-width)}}.md-typeset .tabbed-labels::-webkit-scrollbar{display:none}.md-typeset .tabbed-labels>label{border-bottom:.1rem solid #0000;border-radius:.1rem .1rem 0 0;color:var(--md-default-fg-color--light);cursor:pointer;flex-shrink:0;font-size:.64rem;font-weight:700;padding:.78125em 1.25em .625em;scroll-margin-inline-start:1rem;transition:background-color .25s,color .25s;white-space:nowrap;width:auto}@media print{.md-typeset .tabbed-labels>label:first-child{order:1}.md-typeset .tabbed-labels>label:nth-child(2){order:2}.md-typeset .tabbed-labels>label:nth-child(3){order:3}.md-typeset .tabbed-labels>label:nth-child(4){order:4}.md-typeset .tabbed-labels>label:nth-child(5){order:5}.md-typeset .tabbed-labels>label:nth-child(6){order:6}.md-typeset .tabbed-labels>label:nth-child(7){order:7}.md-typeset .tabbed-labels>label:nth-child(8){order:8}.md-typeset .tabbed-labels>label:nth-child(9){order:9}.md-typeset .tabbed-labels>label:nth-child(10){order:10}.md-typeset .tabbed-labels>label:nth-child(11){order:11}.md-typeset .tabbed-labels>label:nth-child(12){order:12}.md-typeset .tabbed-labels>label:nth-child(13){order:13}.md-typeset .tabbed-labels>label:nth-child(14){order:14}.md-typeset .tabbed-labels>label:nth-child(15){order:15}.md-typeset .tabbed-labels>label:nth-child(16){order:16}.md-typeset .tabbed-labels>label:nth-child(17){order:17}.md-typeset .tabbed-labels>label:nth-child(18){order:18}.md-typeset .tabbed-labels>label:nth-child(19){order:19}.md-typeset .tabbed-labels>label:nth-child(20){order:20}}.md-typeset .tabbed-labels>label:hover{color:var(--md-default-fg-color)}.md-typeset .tabbed-content{width:100%}@media print{.md-typeset .tabbed-content{display:contents}}.md-typeset .tabbed-block{display:none}@media print{.md-typeset .tabbed-block{display:block}.md-typeset .tabbed-block:first-child{order:1}.md-typeset .tabbed-block:nth-child(2){order:2}.md-typeset .tabbed-block:nth-child(3){order:3}.md-typeset .tabbed-block:nth-child(4){order:4}.md-typeset .tabbed-block:nth-child(5){order:5}.md-typeset .tabbed-block:nth-child(6){order:6}.md-typeset .tabbed-block:nth-child(7){order:7}.md-typeset .tabbed-block:nth-child(8){order:8}.md-typeset .tabbed-block:nth-child(9){order:9}.md-typeset .tabbed-block:nth-child(10){order:10}.md-typeset .tabbed-block:nth-child(11){order:11}.md-typeset .tabbed-block:nth-child(12){order:12}.md-typeset .tabbed-block:nth-child(13){order:13}.md-typeset .tabbed-block:nth-child(14){order:14}.md-typeset .tabbed-block:nth-child(15){order:15}.md-typeset .tabbed-block:nth-child(16){order:16}.md-typeset .tabbed-block:nth-child(17){order:17}.md-typeset .tabbed-block:nth-child(18){order:18}.md-typeset .tabbed-block:nth-child(19){order:19}.md-typeset .tabbed-block:nth-child(20){order:20}}.md-typeset .tabbed-block>.highlight:first-child>pre,.md-typeset .tabbed-block>pre:first-child{margin:0}.md-typeset .tabbed-block>.highlight:first-child>pre>code,.md-typeset .tabbed-block>pre:first-child>code{border-top-left-radius:0;border-top-right-radius:0}.md-typeset .tabbed-block>.highlight:first-child>.filename{border-top-left-radius:0;border-top-right-radius:0;margin:0}.md-typeset .tabbed-block>.highlight:first-child>.highlighttable{margin:0}.md-typeset .tabbed-block>.highlight:first-child>.highlighttable>tbody>tr>.filename span.filename,.md-typeset .tabbed-block>.highlight:first-child>.highlighttable>tbody>tr>.linenos{border-top-left-radius:0;border-top-right-radius:0;margin:0}.md-typeset .tabbed-block>.highlight:first-child>.highlighttable>tbody>tr>.code>div>pre>code{border-top-left-radius:0;border-top-right-radius:0}.md-typeset .tabbed-block>.highlight:first-child+.result{margin-top:-.125em}.md-typeset .tabbed-block>.tabbed-set{margin:0}.md-typeset .tabbed-button{align-self:center;border-radius:100%;color:var(--md-default-fg-color--light);cursor:pointer;display:block;height:.9rem;margin-top:.1rem;pointer-events:auto;transition:background-color .25s;width:.9rem}.md-typeset .tabbed-button:hover{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-typeset .tabbed-button:after{background-color:currentcolor;content:"";display:block;height:100%;-webkit-mask-image:var(--md-tabbed-icon--prev);mask-image:var(--md-tabbed-icon--prev);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color .25s,transform .25s;width:100%}.md-typeset .tabbed-control{background:linear-gradient(to right,var(--md-default-bg-color) 60%,#0000);display:flex;height:1.9rem;justify-content:start;pointer-events:none;position:absolute;transition:opacity 125ms;width:1.2rem}[dir=rtl] .md-typeset .tabbed-control{transform:rotate(180deg)}.md-typeset .tabbed-control[hidden]{opacity:0}.md-typeset .tabbed-control--next{background:linear-gradient(to left,var(--md-default-bg-color) 60%,#0000);justify-content:end;right:0}.md-typeset .tabbed-control--next .tabbed-button:after{-webkit-mask-image:var(--md-tabbed-icon--next);mask-image:var(--md-tabbed-icon--next)}@media screen and (max-width:44.984375em){[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels{padding-left:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels{padding-right:.8rem}.md-content__inner>.tabbed-set .tabbed-labels{margin:0 -.8rem;max-width:100vw;scroll-padding-inline-start:.8rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels:after{padding-right:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels:after{padding-left:.8rem}.md-content__inner>.tabbed-set .tabbed-labels:after{content:""}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{padding-left:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{padding-right:.8rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{margin-left:-.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{margin-right:-.8rem}.md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{width:2rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{padding-right:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{padding-left:.8rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{margin-right:-.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{margin-left:-.8rem}.md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{width:2rem}}@media screen{.md-typeset .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9){color:var(--md-default-fg-color)}.md-typeset .no-js .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.md-typeset .no-js .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.md-typeset .no-js .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.md-typeset .no-js .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.md-typeset .no-js .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.md-typeset .no-js .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.md-typeset .no-js .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.md-typeset .no-js .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.md-typeset .no-js .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.md-typeset .no-js .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.md-typeset .no-js .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.md-typeset .no-js .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.md-typeset .no-js .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.md-typeset .no-js .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.md-typeset .no-js .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.md-typeset .no-js .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.md-typeset .no-js .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.md-typeset .no-js .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.md-typeset .no-js .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.md-typeset .no-js .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9),.no-js .md-typeset .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.no-js .md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.no-js .md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.no-js .md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.no-js .md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.no-js .md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.no-js .md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.no-js .md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.no-js .md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.no-js .md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.no-js .md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.no-js .md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.no-js .md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.no-js .md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.no-js .md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.no-js .md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.no-js .md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.no-js .md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.no-js .md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.no-js .md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9){border-color:var(--md-default-fg-color)}}.md-typeset .tabbed-set>input:first-child.focus-visible~.tabbed-labels>:first-child,.md-typeset .tabbed-set>input:nth-child(10).focus-visible~.tabbed-labels>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11).focus-visible~.tabbed-labels>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12).focus-visible~.tabbed-labels>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13).focus-visible~.tabbed-labels>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14).focus-visible~.tabbed-labels>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15).focus-visible~.tabbed-labels>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16).focus-visible~.tabbed-labels>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17).focus-visible~.tabbed-labels>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18).focus-visible~.tabbed-labels>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19).focus-visible~.tabbed-labels>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2).focus-visible~.tabbed-labels>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20).focus-visible~.tabbed-labels>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3).focus-visible~.tabbed-labels>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4).focus-visible~.tabbed-labels>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5).focus-visible~.tabbed-labels>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6).focus-visible~.tabbed-labels>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7).focus-visible~.tabbed-labels>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8).focus-visible~.tabbed-labels>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9).focus-visible~.tabbed-labels>:nth-child(9){color:var(--md-accent-fg-color)}.md-typeset .tabbed-set>input:first-child:checked~.tabbed-content>:first-child,.md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-content>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-content>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-content>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-content>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-content>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-content>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-content>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-content>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-content>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-content>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-content>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-content>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-content>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-content>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-content>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-content>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-content>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-content>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-content>:nth-child(9){display:block}:root{--md-tasklist-icon:url('data:image/svg+xml;charset=utf-8,');--md-tasklist-icon--checked:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .task-list-item{list-style-type:none;position:relative}[dir=ltr] .md-typeset .task-list-item [type=checkbox]{left:-2em}[dir=rtl] .md-typeset .task-list-item [type=checkbox]{right:-2em}.md-typeset .task-list-item [type=checkbox]{position:absolute;top:.45em}.md-typeset .task-list-control [type=checkbox]{opacity:0;z-index:-1}[dir=ltr] .md-typeset .task-list-indicator:before{left:-1.5em}[dir=rtl] .md-typeset .task-list-indicator:before{right:-1.5em}.md-typeset .task-list-indicator:before{background-color:var(--md-default-fg-color--lightest);content:"";height:1.25em;-webkit-mask-image:var(--md-tasklist-icon);mask-image:var(--md-tasklist-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.15em;width:1.25em}.md-typeset [type=checkbox]:checked+.task-list-indicator:before{background-color:#00e676;-webkit-mask-image:var(--md-tasklist-icon--checked);mask-image:var(--md-tasklist-icon--checked)}:root>*{--md-mermaid-font-family:var(--md-text-font-family),sans-serif;--md-mermaid-edge-color:var(--md-code-fg-color);--md-mermaid-node-bg-color:var(--md-accent-fg-color--transparent);--md-mermaid-node-fg-color:var(--md-accent-fg-color);--md-mermaid-label-bg-color:var(--md-default-bg-color);--md-mermaid-label-fg-color:var(--md-code-fg-color);--md-mermaid-sequence-actor-bg-color:var(--md-mermaid-label-bg-color);--md-mermaid-sequence-actor-fg-color:var(--md-mermaid-label-fg-color);--md-mermaid-sequence-actor-border-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-actor-line-color:var(--md-default-fg-color--lighter);--md-mermaid-sequence-actorman-bg-color:var(--md-mermaid-label-bg-color);--md-mermaid-sequence-actorman-line-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-box-bg-color:var(--md-mermaid-node-bg-color);--md-mermaid-sequence-box-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-label-bg-color:var(--md-mermaid-node-bg-color);--md-mermaid-sequence-label-fg-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-loop-bg-color:var(--md-mermaid-node-bg-color);--md-mermaid-sequence-loop-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-loop-border-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-message-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-message-line-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-note-bg-color:var(--md-mermaid-label-bg-color);--md-mermaid-sequence-note-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-note-border-color:var(--md-mermaid-label-fg-color);--md-mermaid-sequence-number-bg-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-number-fg-color:var(--md-accent-bg-color)}.mermaid{line-height:normal;margin:1em 0}@media screen and (min-width:45em){[dir=ltr] .md-typeset .inline{float:left}[dir=rtl] .md-typeset .inline{float:right}[dir=ltr] .md-typeset .inline{margin-right:.8rem}[dir=rtl] .md-typeset .inline{margin-left:.8rem}.md-typeset .inline{margin-bottom:.8rem;margin-top:0;width:11.7rem}[dir=ltr] .md-typeset .inline.end{float:right}[dir=rtl] .md-typeset .inline.end{float:left}[dir=ltr] .md-typeset .inline.end{margin-left:.8rem;margin-right:0}[dir=rtl] .md-typeset .inline.end{margin-left:0;margin-right:.8rem}} \ No newline at end of file diff --git a/assets/stylesheets/main.6a10b989.min.css.map b/assets/stylesheets/main.6a10b989.min.css.map deleted file mode 100644 index e9c64423..00000000 --- a/assets/stylesheets/main.6a10b989.min.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["src/templates/assets/stylesheets/main/components/_meta.scss","../../../../src/templates/assets/stylesheets/main.scss","src/templates/assets/stylesheets/main/_resets.scss","src/templates/assets/stylesheets/main/_colors.scss","src/templates/assets/stylesheets/main/_icons.scss","src/templates/assets/stylesheets/main/_typeset.scss","src/templates/assets/stylesheets/utilities/_break.scss","src/templates/assets/stylesheets/main/components/_author.scss","src/templates/assets/stylesheets/main/components/_banner.scss","src/templates/assets/stylesheets/main/components/_base.scss","src/templates/assets/stylesheets/main/components/_clipboard.scss","src/templates/assets/stylesheets/main/components/_consent.scss","src/templates/assets/stylesheets/main/components/_content.scss","src/templates/assets/stylesheets/main/components/_dialog.scss","src/templates/assets/stylesheets/main/components/_feedback.scss","src/templates/assets/stylesheets/main/components/_footer.scss","src/templates/assets/stylesheets/main/components/_form.scss","src/templates/assets/stylesheets/main/components/_header.scss","node_modules/material-design-color/material-color.scss","src/templates/assets/stylesheets/main/components/_nav.scss","src/templates/assets/stylesheets/main/components/_pagination.scss","src/templates/assets/stylesheets/main/components/_post.scss","src/templates/assets/stylesheets/main/components/_progress.scss","src/templates/assets/stylesheets/main/components/_search.scss","src/templates/assets/stylesheets/main/components/_select.scss","src/templates/assets/stylesheets/main/components/_sidebar.scss","src/templates/assets/stylesheets/main/components/_source.scss","src/templates/assets/stylesheets/main/components/_status.scss","src/templates/assets/stylesheets/main/components/_tabs.scss","src/templates/assets/stylesheets/main/components/_tag.scss","src/templates/assets/stylesheets/main/components/_tooltip.scss","src/templates/assets/stylesheets/main/components/_top.scss","src/templates/assets/stylesheets/main/components/_version.scss","src/templates/assets/stylesheets/main/extensions/markdown/_admonition.scss","src/templates/assets/stylesheets/main/extensions/markdown/_footnotes.scss","src/templates/assets/stylesheets/main/extensions/markdown/_toc.scss","src/templates/assets/stylesheets/main/extensions/pymdownx/_arithmatex.scss","src/templates/assets/stylesheets/main/extensions/pymdownx/_critic.scss","src/templates/assets/stylesheets/main/extensions/pymdownx/_details.scss","src/templates/assets/stylesheets/main/extensions/pymdownx/_emoji.scss","src/templates/assets/stylesheets/main/extensions/pymdownx/_highlight.scss","src/templates/assets/stylesheets/main/extensions/pymdownx/_keys.scss","src/templates/assets/stylesheets/main/extensions/pymdownx/_tabbed.scss","src/templates/assets/stylesheets/main/extensions/pymdownx/_tasklist.scss","src/templates/assets/stylesheets/main/integrations/_mermaid.scss","src/templates/assets/stylesheets/main/_modifiers.scss"],"names":[],"mappings":"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","file":"main.css"} \ No newline at end of file diff --git a/building-api/cheshire/index.html b/building-api/cheshire/index.html index 467a857c..2dd057d7 100644 --- a/building-api/cheshire/index.html +++ b/building-api/cheshire/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/building-api/compojure-api-template/index.html b/building-api/compojure-api-template/index.html index 3ac2ee78..143d3754 100644 --- a/building-api/compojure-api-template/index.html +++ b/building-api/compojure-api-template/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/building-api/create-compojure-api-project/index.html b/building-api/create-compojure-api-project/index.html index 3ab071c7..fcac1c12 100644 --- a/building-api/create-compojure-api-project/index.html +++ b/building-api/create-compojure-api-project/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/building-api/end-to-end-testing/curl/index.html b/building-api/end-to-end-testing/curl/index.html index b6d95c89..b038df18 100644 --- a/building-api/end-to-end-testing/curl/index.html +++ b/building-api/end-to-end-testing/curl/index.html @@ -25,7 +25,7 @@ - + @@ -33,7 +33,7 @@ - + diff --git a/building-api/end-to-end-testing/httpie/index.html b/building-api/end-to-end-testing/httpie/index.html index e2502236..1282ea4d 100644 --- a/building-api/end-to-end-testing/httpie/index.html +++ b/building-api/end-to-end-testing/httpie/index.html @@ -25,7 +25,7 @@ - + @@ -33,7 +33,7 @@ - + diff --git a/building-api/end-to-end-testing/index.html b/building-api/end-to-end-testing/index.html index 1e78b5af..d1b9c3f9 100644 --- a/building-api/end-to-end-testing/index.html +++ b/building-api/end-to-end-testing/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/building-api/end-to-end-testing/postman/index.html b/building-api/end-to-end-testing/postman/index.html index 9f81d76e..947ec8a9 100644 --- a/building-api/end-to-end-testing/postman/index.html +++ b/building-api/end-to-end-testing/postman/index.html @@ -25,7 +25,7 @@ - + @@ -33,7 +33,7 @@ - + diff --git a/building-api/end-to-end-testing/swagger/index.html b/building-api/end-to-end-testing/swagger/index.html index e2ae8640..7d34e3e9 100644 --- a/building-api/end-to-end-testing/swagger/index.html +++ b/building-api/end-to-end-testing/swagger/index.html @@ -25,7 +25,7 @@ - + @@ -33,7 +33,7 @@ - + diff --git a/building-api/index.html b/building-api/index.html index 7bcc151f..9224b716 100644 --- a/building-api/index.html +++ b/building-api/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/building-api/json-files/index.html b/building-api/json-files/index.html index 49b945fc..de6182b6 100644 --- a/building-api/json-files/index.html +++ b/building-api/json-files/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/building-api/plumatic-schema/index.html b/building-api/plumatic-schema/index.html index 30081695..c6114cb5 100644 --- a/building-api/plumatic-schema/index.html +++ b/building-api/plumatic-schema/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/building-api/projects/game-scoreboard-ui/create-project/index.html b/building-api/projects/game-scoreboard-ui/create-project/index.html index c9bd17ff..a2ba6d80 100644 --- a/building-api/projects/game-scoreboard-ui/create-project/index.html +++ b/building-api/projects/game-scoreboard-ui/create-project/index.html @@ -25,7 +25,7 @@ - + @@ -33,7 +33,7 @@ - + diff --git a/building-api/projects/game-scoreboard-ui/index.html b/building-api/projects/game-scoreboard-ui/index.html index 492cf0d5..cf34e6c3 100644 --- a/building-api/projects/game-scoreboard-ui/index.html +++ b/building-api/projects/game-scoreboard-ui/index.html @@ -25,7 +25,7 @@ - + @@ -33,7 +33,7 @@ - + diff --git a/building-api/projects/game-scoreboard/defining-scoreboard/index.html b/building-api/projects/game-scoreboard/defining-scoreboard/index.html index f7a8b934..643780df 100644 --- a/building-api/projects/game-scoreboard/defining-scoreboard/index.html +++ b/building-api/projects/game-scoreboard/defining-scoreboard/index.html @@ -25,7 +25,7 @@ - + @@ -33,7 +33,7 @@ - + diff --git a/building-api/projects/game-scoreboard/defining-scores/index.html b/building-api/projects/game-scoreboard/defining-scores/index.html index 7b017988..22c4fd12 100644 --- a/building-api/projects/game-scoreboard/defining-scores/index.html +++ b/building-api/projects/game-scoreboard/defining-scores/index.html @@ -25,7 +25,7 @@ - + @@ -33,7 +33,7 @@ - + diff --git a/building-api/projects/game-scoreboard/index.html b/building-api/projects/game-scoreboard/index.html index ee7b8e37..9bd7eac5 100644 --- a/building-api/projects/game-scoreboard/index.html +++ b/building-api/projects/game-scoreboard/index.html @@ -25,7 +25,7 @@ - + @@ -33,7 +33,7 @@ - + diff --git a/building-api/reitit/index.html b/building-api/reitit/index.html index a5f55182..cb287948 100644 --- a/building-api/reitit/index.html +++ b/building-api/reitit/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/building-api/ring-mock/index.html b/building-api/ring-mock/index.html index 789a52ca..628f7775 100644 --- a/building-api/ring-mock/index.html +++ b/building-api/ring-mock/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/building-api/ring-swagger/index.html b/building-api/ring-swagger/index.html index 53838101..8f12b268 100644 --- a/building-api/ring-swagger/index.html +++ b/building-api/ring-swagger/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/building-api/swagger/index.html b/building-api/swagger/index.html index 85dd8e10..bb2aef35 100644 --- a/building-api/swagger/index.html +++ b/building-api/swagger/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/building-api/terminology/index.html b/building-api/terminology/index.html index e9250ebb..21cf7ce8 100644 --- a/building-api/terminology/index.html +++ b/building-api/terminology/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/building-api/testing-api/index.html b/building-api/testing-api/index.html index e2aa4b99..92963d1e 100644 --- a/building-api/testing-api/index.html +++ b/building-api/testing-api/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/clojure-databases/crux/index.html b/clojure-databases/crux/index.html index 024df66b..dd41f11c 100644 --- a/clojure-databases/crux/index.html +++ b/clojure-databases/crux/index.html @@ -25,7 +25,7 @@ - + @@ -33,7 +33,7 @@ - + diff --git a/clojure-databases/index.html b/clojure-databases/index.html index 5624aeac..aaf4cba6 100644 --- a/clojure-databases/index.html +++ b/clojure-databases/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/full-app/index.html b/full-app/index.html index 4ded995e..aad75667 100644 --- a/full-app/index.html +++ b/full-app/index.html @@ -25,7 +25,7 @@ - + @@ -33,7 +33,7 @@ - + diff --git a/index.html b/index.html index b57f7eae..f07012c5 100644 --- a/index.html +++ b/index.html @@ -27,7 +27,7 @@ - + @@ -35,7 +35,7 @@ - + diff --git a/introduction/contributing/index.html b/introduction/contributing/index.html index b413851a..f78eed34 100644 --- a/introduction/contributing/index.html +++ b/introduction/contributing/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/introduction/overview/index.html b/introduction/overview/index.html index ac35626a..35d6f4fa 100644 --- a/introduction/overview/index.html +++ b/introduction/overview/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/introduction/repl-workflow/index.html b/introduction/repl-workflow/index.html index 866157f1..5034b83b 100644 --- a/introduction/repl-workflow/index.html +++ b/introduction/repl-workflow/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/introduction/requirements/index.html b/introduction/requirements/index.html index 8a0938ed..c6f7fd50 100644 --- a/introduction/requirements/index.html +++ b/introduction/requirements/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/introduction/writing-tips/index.html b/introduction/writing-tips/index.html index da2c808e..43c9a66f 100644 --- a/introduction/writing-tips/index.html +++ b/introduction/writing-tips/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + @@ -6700,52 +6700,7 @@

Embed external files Scheduled Version Check GitHub Workflow from source code file -
scheduled version check
---
-# ------------------------------------------
-# Scheduled check of versions
-# - use as non-urgent report on versions
-# - Uses POSIX Cron syntax
-#   - Minute [0,59]
-#   - Hour [0,23]
-#   - Day of the month [1,31]
-#   - Month of the year [1,12]
-#   - Day of the week ([0,6] with 0=Sunday)
-#
-# Using liquidz/anta to check:
-# - GitHub workflows
-# - deps.edn
-# ------------------------------------------
-
-name: "Scheduled Version Check"
-on:
-  schedule:
-    # - cron: "0 4 * * *" # at 04:04:04 ever day
-    # - cron: "0 4 * * 5" # at 04:04:04 ever Friday
-    - cron: "0 4 1 * *" # at 04:04:04 on first day of month
-  workflow_dispatch: # Run manually via GitHub Actions Workflow page
-
-jobs:
-  scheduled-version-check:
-    name: "Scheduled Version Check"
-    runs-on: ubuntu-latest
-    steps:
-      - run: echo "🚀 Job automatically triggered by ${{ github.event_name }}"
-      - run: echo "🐧 Job running on ${{ runner.os }} server"
-      - run: echo "🐙 Using ${{ github.ref }} branch from ${{ github.repository }} repository"
-
-      - name: "Checkout code"
-        uses: actions/checkout@v3
-      - run: echo "🐙 ${{ github.repository }} repository was cloned to the runner."
-
-      - name: "Antq Check versions"
-        uses: liquidz/antq-action@main
-        with:
-          excludes: ""
-          skips: "boot clojure-cli pom shadow-cljs leiningen"
-
-      # Summary
-      - run: echo "🎨 library versions checked with liquidz/antq"
-      - run: echo "🍏 Job status is ${{ job.status }}."
+
scheduled version check

 
diff --git a/libraries/reitit/constructing-routes/index.html b/libraries/reitit/constructing-routes/index.html index 9800b17a..e1cfe161 100644 --- a/libraries/reitit/constructing-routes/index.html +++ b/libraries/reitit/constructing-routes/index.html @@ -25,7 +25,7 @@ - + @@ -33,7 +33,7 @@ - + diff --git a/libraries/reitit/index.html b/libraries/reitit/index.html index 1484359f..8a5906ba 100644 --- a/libraries/reitit/index.html +++ b/libraries/reitit/index.html @@ -25,7 +25,7 @@ - + @@ -33,7 +33,7 @@ - + diff --git a/micro-framework/edge/index.html b/micro-framework/edge/index.html index 94a53fdc..9cb6873e 100644 --- a/micro-framework/edge/index.html +++ b/micro-framework/edge/index.html @@ -25,7 +25,7 @@ - + @@ -33,7 +33,7 @@ - + diff --git a/micro-framework/index.html b/micro-framework/index.html index 5a7db727..c2e5f2db 100644 --- a/micro-framework/index.html +++ b/micro-framework/index.html @@ -25,7 +25,7 @@ - + @@ -33,7 +33,7 @@ - + diff --git a/micro-framework/luminus/index.html b/micro-framework/luminus/index.html index 39c52651..3a69fc65 100644 --- a/micro-framework/luminus/index.html +++ b/micro-framework/luminus/index.html @@ -25,7 +25,7 @@ - + @@ -33,7 +33,7 @@ - + diff --git a/micro-framework/pedestal/index.html b/micro-framework/pedestal/index.html index a6f239e1..4a445159 100644 --- a/micro-framework/pedestal/index.html +++ b/micro-framework/pedestal/index.html @@ -25,7 +25,7 @@ - + @@ -33,7 +33,7 @@ - + diff --git a/micro-services/index.html b/micro-services/index.html index 3e8a600d..a1e0d906 100644 --- a/micro-services/index.html +++ b/micro-services/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/project-url-shortner/add-alias-to-database/index.html b/project-url-shortner/add-alias-to-database/index.html index d750aad5..fc4406a1 100644 --- a/project-url-shortner/add-alias-to-database/index.html +++ b/project-url-shortner/add-alias-to-database/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/project-url-shortner/add-static-resources/index.html b/project-url-shortner/add-static-resources/index.html index f7255340..2abda591 100644 --- a/project-url-shortner/add-static-resources/index.html +++ b/project-url-shortner/add-static-resources/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/project-url-shortner/alias-generator/index.html b/project-url-shortner/alias-generator/index.html index 3e93964b..28894940 100644 --- a/project-url-shortner/alias-generator/index.html +++ b/project-url-shortner/alias-generator/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/project-url-shortner/compojure-template/index.html b/project-url-shortner/compojure-template/index.html index f5903685..6f4a1ef9 100644 --- a/project-url-shortner/compojure-template/index.html +++ b/project-url-shortner/compojure-template/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/project-url-shortner/create-database/index.html b/project-url-shortner/create-database/index.html index 88e19403..2b0f3f22 100644 --- a/project-url-shortner/create-database/index.html +++ b/project-url-shortner/create-database/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/project-url-shortner/create-html-form/index.html b/project-url-shortner/create-html-form/index.html index 551e8d75..e23f0844 100644 --- a/project-url-shortner/create-html-form/index.html +++ b/project-url-shortner/create-html-form/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/project-url-shortner/create-project/index.html b/project-url-shortner/create-project/index.html index f640f244..4991cf49 100644 --- a/project-url-shortner/create-project/index.html +++ b/project-url-shortner/create-project/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/project-url-shortner/delete-alias-from-database/index.html b/project-url-shortner/delete-alias-from-database/index.html index 5f22fa77..48036467 100644 --- a/project-url-shortner/delete-alias-from-database/index.html +++ b/project-url-shortner/delete-alias-from-database/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/project-url-shortner/design-data-structure/index.html b/project-url-shortner/design-data-structure/index.html index c4c00de2..0d10ab59 100644 --- a/project-url-shortner/design-data-structure/index.html +++ b/project-url-shortner/design-data-structure/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/project-url-shortner/disable-anti-forgery-check/index.html b/project-url-shortner/disable-anti-forgery-check/index.html index 1043239e..3bfaf147 100644 --- a/project-url-shortner/disable-anti-forgery-check/index.html +++ b/project-url-shortner/disable-anti-forgery-check/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/project-url-shortner/get-alias-from-database/index.html b/project-url-shortner/get-alias-from-database/index.html index 9fd4f169..31b0fadd 100644 --- a/project-url-shortner/get-alias-from-database/index.html +++ b/project-url-shortner/get-alias-from-database/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/project-url-shortner/html-form/index.html b/project-url-shortner/html-form/index.html index 624fbac5..8b69c3db 100644 --- a/project-url-shortner/html-form/index.html +++ b/project-url-shortner/html-form/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/project-url-shortner/if-let-function/index.html b/project-url-shortner/if-let-function/index.html index 534b228d..67561144 100644 --- a/project-url-shortner/if-let-function/index.html +++ b/project-url-shortner/if-let-function/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/project-url-shortner/index.html b/project-url-shortner/index.html index 25e6893d..e1599d30 100644 --- a/project-url-shortner/index.html +++ b/project-url-shortner/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/project-url-shortner/named-alias-handler/index.html b/project-url-shortner/named-alias-handler/index.html index ed6df055..90c8af8c 100644 --- a/project-url-shortner/named-alias-handler/index.html +++ b/project-url-shortner/named-alias-handler/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/project-url-shortner/persist-aliases/index.html b/project-url-shortner/persist-aliases/index.html index d21e4acf..c35905ee 100644 --- a/project-url-shortner/persist-aliases/index.html +++ b/project-url-shortner/persist-aliases/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/project-url-shortner/postgres-setup/index.html b/project-url-shortner/postgres-setup/index.html index 7adcfb14..7f7495ac 100644 --- a/project-url-shortner/postgres-setup/index.html +++ b/project-url-shortner/postgres-setup/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/project-url-shortner/redirect-to-full-url/index.html b/project-url-shortner/redirect-to-full-url/index.html index 4b315b6e..80c4aa63 100644 --- a/project-url-shortner/redirect-to-full-url/index.html +++ b/project-url-shortner/redirect-to-full-url/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/project-url-shortner/redis-setup/index.html b/project-url-shortner/redis-setup/index.html index 5d22465b..f6e1c083 100644 --- a/project-url-shortner/redis-setup/index.html +++ b/project-url-shortner/redis-setup/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/project-url-shortner/refacor-hiccup-form/index.html b/project-url-shortner/refacor-hiccup-form/index.html index 6c84cd02..75b3dcc5 100644 --- a/project-url-shortner/refacor-hiccup-form/index.html +++ b/project-url-shortner/refacor-hiccup-form/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/project-url-shortner/return-short-url/index.html b/project-url-shortner/return-short-url/index.html index 6728c7e9..9d1fba29 100644 --- a/project-url-shortner/return-short-url/index.html +++ b/project-url-shortner/return-short-url/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/project-url-shortner/return-url-aliases/index.html b/project-url-shortner/return-url-aliases/index.html index 05a6eaf6..a9f88a67 100644 --- a/project-url-shortner/return-url-aliases/index.html +++ b/project-url-shortner/return-url-aliases/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/project-url-shortner/run-project/index.html b/project-url-shortner/run-project/index.html index 2ac08515..243b21ea 100644 --- a/project-url-shortner/run-project/index.html +++ b/project-url-shortner/run-project/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/project-url-shortner/test-app-reloading/index.html b/project-url-shortner/test-app-reloading/index.html index b0091d0a..0c7f9597 100644 --- a/project-url-shortner/test-app-reloading/index.html +++ b/project-url-shortner/test-app-reloading/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/project-url-shortner/using-ring-redirect/index.html b/project-url-shortner/using-ring-redirect/index.html index 5ab7165b..6cf227f7 100644 --- a/project-url-shortner/using-ring-redirect/index.html +++ b/project-url-shortner/using-ring-redirect/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/project-url-shortner/whats-in-a-request/index.html b/project-url-shortner/whats-in-a-request/index.html index 1100df11..e98381b1 100644 --- a/project-url-shortner/whats-in-a-request/index.html +++ b/project-url-shortner/whats-in-a-request/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/banking-on-clojure/account-overview-page/index.html b/projects/banking-on-clojure/account-overview-page/index.html index afc4b7c7..87e9e1c4 100644 --- a/projects/banking-on-clojure/account-overview-page/index.html +++ b/projects/banking-on-clojure/account-overview-page/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/banking-on-clojure/clojure-server-project/index.html b/projects/banking-on-clojure/clojure-server-project/index.html index aa9222be..737ab576 100644 --- a/projects/banking-on-clojure/clojure-server-project/index.html +++ b/projects/banking-on-clojure/clojure-server-project/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/banking-on-clojure/clojure-spec-generate-mock-data/index.html b/projects/banking-on-clojure/clojure-spec-generate-mock-data/index.html index b8189fdf..362179d9 100644 --- a/projects/banking-on-clojure/clojure-spec-generate-mock-data/index.html +++ b/projects/banking-on-clojure/clojure-spec-generate-mock-data/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/banking-on-clojure/continuous-integration/index.html b/projects/banking-on-clojure/continuous-integration/index.html index f228374d..6c331f33 100644 --- a/projects/banking-on-clojure/continuous-integration/index.html +++ b/projects/banking-on-clojure/continuous-integration/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/banking-on-clojure/create-records/index.html b/projects/banking-on-clojure/create-records/index.html index 9f95c578..f334545d 100644 --- a/projects/banking-on-clojure/create-records/index.html +++ b/projects/banking-on-clojure/create-records/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/banking-on-clojure/cyclic-load-dependency/index.html b/projects/banking-on-clojure/cyclic-load-dependency/index.html index 53dc574c..6ff2e477 100644 --- a/projects/banking-on-clojure/cyclic-load-dependency/index.html +++ b/projects/banking-on-clojure/cyclic-load-dependency/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/banking-on-clojure/database-queries/index.html b/projects/banking-on-clojure/database-queries/index.html index a51e71eb..48aa7707 100644 --- a/projects/banking-on-clojure/database-queries/index.html +++ b/projects/banking-on-clojure/database-queries/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/banking-on-clojure/database-tables/index.html b/projects/banking-on-clojure/database-tables/index.html index 282f8ac5..31cf1c2f 100644 --- a/projects/banking-on-clojure/database-tables/index.html +++ b/projects/banking-on-clojure/database-tables/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/banking-on-clojure/delete-records/index.html b/projects/banking-on-clojure/delete-records/index.html index 4528f7d7..60ce4d47 100644 --- a/projects/banking-on-clojure/delete-records/index.html +++ b/projects/banking-on-clojure/delete-records/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/banking-on-clojure/deployment-pipeline/index.html b/projects/banking-on-clojure/deployment-pipeline/index.html index 798165b7..dd333e73 100644 --- a/projects/banking-on-clojure/deployment-pipeline/index.html +++ b/projects/banking-on-clojure/deployment-pipeline/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/banking-on-clojure/deployment-via-ci/index.html b/projects/banking-on-clojure/deployment-via-ci/index.html index 4de4b7ec..de4bacd4 100644 --- a/projects/banking-on-clojure/deployment-via-ci/index.html +++ b/projects/banking-on-clojure/deployment-via-ci/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/banking-on-clojure/development-database/index.html b/projects/banking-on-clojure/development-database/index.html index de947de3..979994b9 100644 --- a/projects/banking-on-clojure/development-database/index.html +++ b/projects/banking-on-clojure/development-database/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/banking-on-clojure/generate-data-from-specs/index.html b/projects/banking-on-clojure/generate-data-from-specs/index.html index 9683e9fb..640b451b 100644 --- a/projects/banking-on-clojure/generate-data-from-specs/index.html +++ b/projects/banking-on-clojure/generate-data-from-specs/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/banking-on-clojure/honeysql/index.html b/projects/banking-on-clojure/honeysql/index.html index 903a11c2..02ba9b2c 100644 --- a/projects/banking-on-clojure/honeysql/index.html +++ b/projects/banking-on-clojure/honeysql/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/banking-on-clojure/index.html b/projects/banking-on-clojure/index.html index bb83fe09..6d270821 100644 --- a/projects/banking-on-clojure/index.html +++ b/projects/banking-on-clojure/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/banking-on-clojure/instrument-next-jdbc-functions/index.html b/projects/banking-on-clojure/instrument-next-jdbc-functions/index.html index 2562c343..61cc753e 100644 --- a/projects/banking-on-clojure/instrument-next-jdbc-functions/index.html +++ b/projects/banking-on-clojure/instrument-next-jdbc-functions/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/banking-on-clojure/namespace-design/index.html b/projects/banking-on-clojure/namespace-design/index.html index 8cdaa260..8980f023 100644 --- a/projects/banking-on-clojure/namespace-design/index.html +++ b/projects/banking-on-clojure/namespace-design/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/banking-on-clojure/production-database/index.html b/projects/banking-on-clojure/production-database/index.html index 5d8667bc..74f36207 100644 --- a/projects/banking-on-clojure/production-database/index.html +++ b/projects/banking-on-clojure/production-database/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/banking-on-clojure/read-records/index.html b/projects/banking-on-clojure/read-records/index.html index 69fb9215..2acbcd69 100644 --- a/projects/banking-on-clojure/read-records/index.html +++ b/projects/banking-on-clojure/read-records/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/banking-on-clojure/refactor-handler/index.html b/projects/banking-on-clojure/refactor-handler/index.html index fe0323a9..9aad6c3a 100644 --- a/projects/banking-on-clojure/refactor-handler/index.html +++ b/projects/banking-on-clojure/refactor-handler/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/banking-on-clojure/spec-generative-testing/index.html b/projects/banking-on-clojure/spec-generative-testing/index.html index 52758eb9..2ae19392 100644 --- a/projects/banking-on-clojure/spec-generative-testing/index.html +++ b/projects/banking-on-clojure/spec-generative-testing/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/banking-on-clojure/ui-handler-functions/index.html b/projects/banking-on-clojure/ui-handler-functions/index.html index dad3deae..91888a19 100644 --- a/projects/banking-on-clojure/ui-handler-functions/index.html +++ b/projects/banking-on-clojure/ui-handler-functions/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/banking-on-clojure/unit-testing-the-database/index.html b/projects/banking-on-clojure/unit-testing-the-database/index.html index de0de4e6..49c96679 100644 --- a/projects/banking-on-clojure/unit-testing-the-database/index.html +++ b/projects/banking-on-clojure/unit-testing-the-database/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/banking-on-clojure/unit-tests/index.html b/projects/banking-on-clojure/unit-tests/index.html index d86a3e00..41f30929 100644 --- a/projects/banking-on-clojure/unit-tests/index.html +++ b/projects/banking-on-clojure/unit-tests/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/banking-on-clojure/update-records/index.html b/projects/banking-on-clojure/update-records/index.html index 29e4eade..fa76ec51 100644 --- a/projects/banking-on-clojure/update-records/index.html +++ b/projects/banking-on-clojure/update-records/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/game-scoreboard-api/index.html b/projects/game-scoreboard-api/index.html index fd518048..805b9902 100644 --- a/projects/game-scoreboard-api/index.html +++ b/projects/game-scoreboard-api/index.html @@ -25,7 +25,7 @@ - + @@ -33,7 +33,7 @@ - + diff --git a/projects/index.html b/projects/index.html index dd46c4a7..8bf4df4b 100644 --- a/projects/index.html +++ b/projects/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/compojure/about/index.html b/projects/leiningen/todo-app/compojure/about/index.html index 0f17f441..e69c1c4c 100644 --- a/projects/leiningen/todo-app/compojure/about/index.html +++ b/projects/leiningen/todo-app/compojure/about/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/compojure/adding-dependency/index.html b/projects/leiningen/todo-app/compojure/adding-dependency/index.html index 397d233f..96ecc273 100644 --- a/projects/leiningen/todo-app/compojure/adding-dependency/index.html +++ b/projects/leiningen/todo-app/compojure/adding-dependency/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/compojure/adding-goodbye-route/index.html b/projects/leiningen/todo-app/compojure/adding-goodbye-route/index.html index c5b1e0e5..1ffa6783 100644 --- a/projects/leiningen/todo-app/compojure/adding-goodbye-route/index.html +++ b/projects/leiningen/todo-app/compojure/adding-goodbye-route/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/compojure/code-so-far/index.html b/projects/leiningen/todo-app/compojure/code-so-far/index.html index dca3a6ca..68067df5 100644 --- a/projects/leiningen/todo-app/compojure/code-so-far/index.html +++ b/projects/leiningen/todo-app/compojure/code-so-far/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/compojure/defroutes/index.html b/projects/leiningen/todo-app/compojure/defroutes/index.html index 56500b9f..973e31de 100644 --- a/projects/leiningen/todo-app/compojure/defroutes/index.html +++ b/projects/leiningen/todo-app/compojure/defroutes/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/compojure/index.html b/projects/leiningen/todo-app/compojure/index.html index 220cc8e9..a8498c50 100644 --- a/projects/leiningen/todo-app/compojure/index.html +++ b/projects/leiningen/todo-app/compojure/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/compojure/lisp-calculator/index.html b/projects/leiningen/todo-app/compojure/lisp-calculator/index.html index 859c4a90..690ac001 100644 --- a/projects/leiningen/todo-app/compojure/lisp-calculator/index.html +++ b/projects/leiningen/todo-app/compojure/lisp-calculator/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/compojure/show-request-info/index.html b/projects/leiningen/todo-app/compojure/show-request-info/index.html index 281497b4..7b7b7dab 100644 --- a/projects/leiningen/todo-app/compojure/show-request-info/index.html +++ b/projects/leiningen/todo-app/compojure/show-request-info/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/compojure/theory-local-name-bindings/index.html b/projects/leiningen/todo-app/compojure/theory-local-name-bindings/index.html index 4e51e5a9..a9e0f82b 100644 --- a/projects/leiningen/todo-app/compojure/theory-local-name-bindings/index.html +++ b/projects/leiningen/todo-app/compojure/theory-local-name-bindings/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/compojure/theory-routing/index.html b/projects/leiningen/todo-app/compojure/theory-routing/index.html index 6eeed641..ea87493f 100644 --- a/projects/leiningen/todo-app/compojure/theory-routing/index.html +++ b/projects/leiningen/todo-app/compojure/theory-routing/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/compojure/theory-using-hash-maps/index.html b/projects/leiningen/todo-app/compojure/theory-using-hash-maps/index.html index b77d9c45..071551f3 100644 --- a/projects/leiningen/todo-app/compojure/theory-using-hash-maps/index.html +++ b/projects/leiningen/todo-app/compojure/theory-using-hash-maps/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/compojure/using-compojure/index.html b/projects/leiningen/todo-app/compojure/using-compojure/index.html index dc72de48..7069cfe1 100644 --- a/projects/leiningen/todo-app/compojure/using-compojure/index.html +++ b/projects/leiningen/todo-app/compojure/using-compojure/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/compojure/variable-path-elements/index.html b/projects/leiningen/todo-app/compojure/variable-path-elements/index.html index 23c00430..74886bd9 100644 --- a/projects/leiningen/todo-app/compojure/variable-path-elements/index.html +++ b/projects/leiningen/todo-app/compojure/variable-path-elements/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/connect-to-postgres/add-database-dependencies/index.html b/projects/leiningen/todo-app/connect-to-postgres/add-database-dependencies/index.html index 3c00b007..1ac0ab57 100644 --- a/projects/leiningen/todo-app/connect-to-postgres/add-database-dependencies/index.html +++ b/projects/leiningen/todo-app/connect-to-postgres/add-database-dependencies/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/connect-to-postgres/define-db-connection/index.html b/projects/leiningen/todo-app/connect-to-postgres/define-db-connection/index.html index 5a86ca12..941a53f0 100644 --- a/projects/leiningen/todo-app/connect-to-postgres/define-db-connection/index.html +++ b/projects/leiningen/todo-app/connect-to-postgres/define-db-connection/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/connect-to-postgres/index.html b/projects/leiningen/todo-app/connect-to-postgres/index.html index e76c5788..30925937 100644 --- a/projects/leiningen/todo-app/connect-to-postgres/index.html +++ b/projects/leiningen/todo-app/connect-to-postgres/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/create-a-handler-function/add-not-found/index.html b/projects/leiningen/todo-app/create-a-handler-function/add-not-found/index.html index 9c4ce05c..85da9fcf 100644 --- a/projects/leiningen/todo-app/create-a-handler-function/add-not-found/index.html +++ b/projects/leiningen/todo-app/create-a-handler-function/add-not-found/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/create-a-handler-function/code-so-far/index.html b/projects/leiningen/todo-app/create-a-handler-function/code-so-far/index.html index 17b6b20c..c7c3b650 100644 --- a/projects/leiningen/todo-app/create-a-handler-function/code-so-far/index.html +++ b/projects/leiningen/todo-app/create-a-handler-function/code-so-far/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/create-a-handler-function/if-function/index.html b/projects/leiningen/todo-app/create-a-handler-function/if-function/index.html index 985e241d..35dcd515 100644 --- a/projects/leiningen/todo-app/create-a-handler-function/if-function/index.html +++ b/projects/leiningen/todo-app/create-a-handler-function/if-function/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/create-a-handler-function/index.html b/projects/leiningen/todo-app/create-a-handler-function/index.html index cc0147ec..85bd7db1 100644 --- a/projects/leiningen/todo-app/create-a-handler-function/index.html +++ b/projects/leiningen/todo-app/create-a-handler-function/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/create-a-handler-function/maps-and-keywords/index.html b/projects/leiningen/todo-app/create-a-handler-function/maps-and-keywords/index.html index c0ed084e..cb8c1a01 100644 --- a/projects/leiningen/todo-app/create-a-handler-function/maps-and-keywords/index.html +++ b/projects/leiningen/todo-app/create-a-handler-function/maps-and-keywords/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/create-a-project/code-so-far/index.html b/projects/leiningen/todo-app/create-a-project/code-so-far/index.html index 463b011e..20d8e781 100644 --- a/projects/leiningen/todo-app/create-a-project/code-so-far/index.html +++ b/projects/leiningen/todo-app/create-a-project/code-so-far/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/create-a-project/index.html b/projects/leiningen/todo-app/create-a-project/index.html index fc8d1dcd..6ea545cc 100644 --- a/projects/leiningen/todo-app/create-a-project/index.html +++ b/projects/leiningen/todo-app/create-a-project/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/create-a-project/update-project-details/index.html b/projects/leiningen/todo-app/create-a-project/update-project-details/index.html index 4abb1617..751a15b5 100644 --- a/projects/leiningen/todo-app/create-a-project/update-project-details/index.html +++ b/projects/leiningen/todo-app/create-a-project/update-project-details/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/create-a-webserver-with-ring/add-a-jetty-webserver/index.html b/projects/leiningen/todo-app/create-a-webserver-with-ring/add-a-jetty-webserver/index.html index 6d45325e..d917de06 100644 --- a/projects/leiningen/todo-app/create-a-webserver-with-ring/add-a-jetty-webserver/index.html +++ b/projects/leiningen/todo-app/create-a-webserver-with-ring/add-a-jetty-webserver/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/create-a-webserver-with-ring/add-ring-dependency/index.html b/projects/leiningen/todo-app/create-a-webserver-with-ring/add-ring-dependency/index.html index 19f44dbf..ac7f700e 100644 --- a/projects/leiningen/todo-app/create-a-webserver-with-ring/add-ring-dependency/index.html +++ b/projects/leiningen/todo-app/create-a-webserver-with-ring/add-ring-dependency/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/create-a-webserver-with-ring/code-so-far/index.html b/projects/leiningen/todo-app/create-a-webserver-with-ring/code-so-far/index.html index 70ecc06a..ad7cc44c 100644 --- a/projects/leiningen/todo-app/create-a-webserver-with-ring/code-so-far/index.html +++ b/projects/leiningen/todo-app/create-a-webserver-with-ring/code-so-far/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/create-a-webserver-with-ring/coersing-types-and-java-lang/index.html b/projects/leiningen/todo-app/create-a-webserver-with-ring/coersing-types-and-java-lang/index.html index 68aac39c..013a4afc 100644 --- a/projects/leiningen/todo-app/create-a-webserver-with-ring/coersing-types-and-java-lang/index.html +++ b/projects/leiningen/todo-app/create-a-webserver-with-ring/coersing-types-and-java-lang/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/create-a-webserver-with-ring/configure-main-namespace/index.html b/projects/leiningen/todo-app/create-a-webserver-with-ring/configure-main-namespace/index.html index 38bf7341..fa1ccd34 100644 --- a/projects/leiningen/todo-app/create-a-webserver-with-ring/configure-main-namespace/index.html +++ b/projects/leiningen/todo-app/create-a-webserver-with-ring/configure-main-namespace/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/create-a-webserver-with-ring/include-ring-library/index.html b/projects/leiningen/todo-app/create-a-webserver-with-ring/include-ring-library/index.html index fd2848d0..41f3848e 100644 --- a/projects/leiningen/todo-app/create-a-webserver-with-ring/include-ring-library/index.html +++ b/projects/leiningen/todo-app/create-a-webserver-with-ring/include-ring-library/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/create-a-webserver-with-ring/index.html b/projects/leiningen/todo-app/create-a-webserver-with-ring/index.html index 066a3851..db2c2c87 100644 --- a/projects/leiningen/todo-app/create-a-webserver-with-ring/index.html +++ b/projects/leiningen/todo-app/create-a-webserver-with-ring/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/create-a-webserver-with-ring/namespaces/index.html b/projects/leiningen/todo-app/create-a-webserver-with-ring/namespaces/index.html index b3f8b227..cb180f89 100644 --- a/projects/leiningen/todo-app/create-a-webserver-with-ring/namespaces/index.html +++ b/projects/leiningen/todo-app/create-a-webserver-with-ring/namespaces/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/create-a-webserver-with-ring/run-webserver/index.html b/projects/leiningen/todo-app/create-a-webserver-with-ring/run-webserver/index.html index 331a8156..0e099ae7 100644 --- a/projects/leiningen/todo-app/create-a-webserver-with-ring/run-webserver/index.html +++ b/projects/leiningen/todo-app/create-a-webserver-with-ring/run-webserver/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/database-model/alternative-approaches/index.html b/projects/leiningen/todo-app/database-model/alternative-approaches/index.html index e6e5b1eb..dcfae455 100644 --- a/projects/leiningen/todo-app/database-model/alternative-approaches/index.html +++ b/projects/leiningen/todo-app/database-model/alternative-approaches/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/database-model/create-table/index.html b/projects/leiningen/todo-app/database-model/create-table/index.html index a008d20d..3b9a4cfd 100644 --- a/projects/leiningen/todo-app/database-model/create-table/index.html +++ b/projects/leiningen/todo-app/database-model/create-table/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/database-model/create-task/index.html b/projects/leiningen/todo-app/database-model/create-task/index.html index e0af8363..6564f95d 100644 --- a/projects/leiningen/todo-app/database-model/create-task/index.html +++ b/projects/leiningen/todo-app/database-model/create-task/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/database-model/delete-task/index.html b/projects/leiningen/todo-app/database-model/delete-task/index.html index 891a491f..8b6b53c5 100644 --- a/projects/leiningen/todo-app/database-model/delete-task/index.html +++ b/projects/leiningen/todo-app/database-model/delete-task/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/database-model/index.html b/projects/leiningen/todo-app/database-model/index.html index 68f6acb7..e7295848 100644 --- a/projects/leiningen/todo-app/database-model/index.html +++ b/projects/leiningen/todo-app/database-model/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/database-model/show-all-task/index.html b/projects/leiningen/todo-app/database-model/show-all-task/index.html index 7b349f80..cc6bf23b 100644 --- a/projects/leiningen/todo-app/database-model/show-all-task/index.html +++ b/projects/leiningen/todo-app/database-model/show-all-task/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/heroku/code-so-far/index.html b/projects/leiningen/todo-app/heroku/code-so-far/index.html index edd6cbda..89bcc01c 100644 --- a/projects/leiningen/todo-app/heroku/code-so-far/index.html +++ b/projects/leiningen/todo-app/heroku/code-so-far/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/heroku/deploy/index.html b/projects/leiningen/todo-app/heroku/deploy/index.html index 904bc8df..9d274501 100644 --- a/projects/leiningen/todo-app/heroku/deploy/index.html +++ b/projects/leiningen/todo-app/heroku/deploy/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/heroku/index.html b/projects/leiningen/todo-app/heroku/index.html index 84b90646..e32c3073 100644 --- a/projects/leiningen/todo-app/heroku/index.html +++ b/projects/leiningen/todo-app/heroku/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/heroku/procfile/index.html b/projects/leiningen/todo-app/heroku/procfile/index.html index 33f568ae..ac7b0a04 100644 --- a/projects/leiningen/todo-app/heroku/procfile/index.html +++ b/projects/leiningen/todo-app/heroku/procfile/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/heroku/update-project/index.html b/projects/leiningen/todo-app/heroku/update-project/index.html index 220625b3..66afab13 100644 --- a/projects/leiningen/todo-app/heroku/update-project/index.html +++ b/projects/leiningen/todo-app/heroku/update-project/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/hiccup/code-so-far/index.html b/projects/leiningen/todo-app/hiccup/code-so-far/index.html index e0efff8b..eed120a8 100644 --- a/projects/leiningen/todo-app/hiccup/code-so-far/index.html +++ b/projects/leiningen/todo-app/hiccup/code-so-far/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/hiccup/create-new-handler/index.html b/projects/leiningen/todo-app/hiccup/create-new-handler/index.html index 5d69a6e7..480fb5bd 100644 --- a/projects/leiningen/todo-app/hiccup/create-new-handler/index.html +++ b/projects/leiningen/todo-app/hiccup/create-new-handler/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/hiccup/index.html b/projects/leiningen/todo-app/hiccup/index.html index 27022e3d..db7a76f9 100644 --- a/projects/leiningen/todo-app/hiccup/index.html +++ b/projects/leiningen/todo-app/hiccup/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/hiccup/updating-handlers-with-hiccup/index.html b/projects/leiningen/todo-app/hiccup/updating-handlers-with-hiccup/index.html index 9a72163a..265aa4a3 100644 --- a/projects/leiningen/todo-app/hiccup/updating-handlers-with-hiccup/index.html +++ b/projects/leiningen/todo-app/hiccup/updating-handlers-with-hiccup/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/index.html b/projects/leiningen/todo-app/index.html index 76f1d54c..a055b455 100644 --- a/projects/leiningen/todo-app/index.html +++ b/projects/leiningen/todo-app/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/introducing-ring/index.html b/projects/leiningen/todo-app/introducing-ring/index.html index 23b896ae..66cc8f5b 100644 --- a/projects/leiningen/todo-app/introducing-ring/index.html +++ b/projects/leiningen/todo-app/introducing-ring/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/postgres/connect-to-heroku-postgres-from-clients/index.html b/projects/leiningen/todo-app/postgres/connect-to-heroku-postgres-from-clients/index.html index e304f454..d68637fa 100644 --- a/projects/leiningen/todo-app/postgres/connect-to-heroku-postgres-from-clients/index.html +++ b/projects/leiningen/todo-app/postgres/connect-to-heroku-postgres-from-clients/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/postgres/dataclips/index.html b/projects/leiningen/todo-app/postgres/dataclips/index.html index 1651896b..6f4a3a1a 100644 --- a/projects/leiningen/todo-app/postgres/dataclips/index.html +++ b/projects/leiningen/todo-app/postgres/dataclips/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/postgres/environment-variables/index.html b/projects/leiningen/todo-app/postgres/environment-variables/index.html index 581eaf0a..435adda6 100644 --- a/projects/leiningen/todo-app/postgres/environment-variables/index.html +++ b/projects/leiningen/todo-app/postgres/environment-variables/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/postgres/index.html b/projects/leiningen/todo-app/postgres/index.html index 7242a046..b104c657 100644 --- a/projects/leiningen/todo-app/postgres/index.html +++ b/projects/leiningen/todo-app/postgres/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/postgres/install/index.html b/projects/leiningen/todo-app/postgres/install/index.html index 1eb10f9a..52adcbfa 100644 --- a/projects/leiningen/todo-app/postgres/install/index.html +++ b/projects/leiningen/todo-app/postgres/install/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/postgres/jira-ticket/index.html b/projects/leiningen/todo-app/postgres/jira-ticket/index.html index 6892d37d..0aa812fa 100644 --- a/projects/leiningen/todo-app/postgres/jira-ticket/index.html +++ b/projects/leiningen/todo-app/postgres/jira-ticket/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/postgres/lobo-table-creation/index.html b/projects/leiningen/todo-app/postgres/lobo-table-creation/index.html index a2b99d05..22bceba6 100644 --- a/projects/leiningen/todo-app/postgres/lobo-table-creation/index.html +++ b/projects/leiningen/todo-app/postgres/lobo-table-creation/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/postgres/pg-admin/index.html b/projects/leiningen/todo-app/postgres/pg-admin/index.html index 91db6c9d..4af4bef0 100644 --- a/projects/leiningen/todo-app/postgres/pg-admin/index.html +++ b/projects/leiningen/todo-app/postgres/pg-admin/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/postgres/postgres-cli/index.html b/projects/leiningen/todo-app/postgres/postgres-cli/index.html index 029e64ff..a5ebd4ac 100644 --- a/projects/leiningen/todo-app/postgres/postgres-cli/index.html +++ b/projects/leiningen/todo-app/postgres/postgres-cli/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/postgres/postgres-commands/index.html b/projects/leiningen/todo-app/postgres/postgres-commands/index.html index 7804dae2..372f5b77 100644 --- a/projects/leiningen/todo-app/postgres/postgres-commands/index.html +++ b/projects/leiningen/todo-app/postgres/postgres-commands/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/postgres/postgres-performance-analytics/index.html b/projects/leiningen/todo-app/postgres/postgres-performance-analytics/index.html index 47def321..d2f08d34 100644 --- a/projects/leiningen/todo-app/postgres/postgres-performance-analytics/index.html +++ b/projects/leiningen/todo-app/postgres/postgres-performance-analytics/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/postgres/postgres-toolbelt-commands/index.html b/projects/leiningen/todo-app/postgres/postgres-toolbelt-commands/index.html index 8cc6c1e9..f508b4c2 100644 --- a/projects/leiningen/todo-app/postgres/postgres-toolbelt-commands/index.html +++ b/projects/leiningen/todo-app/postgres/postgres-toolbelt-commands/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/refactor-namespace/base-routes/index.html b/projects/leiningen/todo-app/refactor-namespace/base-routes/index.html index 901fdfcc..149c242b 100644 --- a/projects/leiningen/todo-app/refactor-namespace/base-routes/index.html +++ b/projects/leiningen/todo-app/refactor-namespace/base-routes/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/refactor-namespace/code-so-far/index.html b/projects/leiningen/todo-app/refactor-namespace/code-so-far/index.html index 4c659849..dcfc87a9 100644 --- a/projects/leiningen/todo-app/refactor-namespace/code-so-far/index.html +++ b/projects/leiningen/todo-app/refactor-namespace/code-so-far/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/refactor-namespace/core/index.html b/projects/leiningen/todo-app/refactor-namespace/core/index.html index d33032f8..30ce46dc 100644 --- a/projects/leiningen/todo-app/refactor-namespace/core/index.html +++ b/projects/leiningen/todo-app/refactor-namespace/core/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/refactor-namespace/index.html b/projects/leiningen/todo-app/refactor-namespace/index.html index 28847525..0712c375 100644 --- a/projects/leiningen/todo-app/refactor-namespace/index.html +++ b/projects/leiningen/todo-app/refactor-namespace/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/refactor-namespace/play-routes/index.html b/projects/leiningen/todo-app/refactor-namespace/play-routes/index.html index d265104d..7744281e 100644 --- a/projects/leiningen/todo-app/refactor-namespace/play-routes/index.html +++ b/projects/leiningen/todo-app/refactor-namespace/play-routes/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/refactor-namespace/task-routes/index.html b/projects/leiningen/todo-app/refactor-namespace/task-routes/index.html index f352cca1..c09d418d 100644 --- a/projects/leiningen/todo-app/refactor-namespace/task-routes/index.html +++ b/projects/leiningen/todo-app/refactor-namespace/task-routes/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/reloading-the-application/code-so-far/index.html b/projects/leiningen/todo-app/reloading-the-application/code-so-far/index.html index 9bdc0050..99278293 100644 --- a/projects/leiningen/todo-app/reloading-the-application/code-so-far/index.html +++ b/projects/leiningen/todo-app/reloading-the-application/code-so-far/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/reloading-the-application/index.html b/projects/leiningen/todo-app/reloading-the-application/index.html index 6c3de99b..e311e449 100644 --- a/projects/leiningen/todo-app/reloading-the-application/index.html +++ b/projects/leiningen/todo-app/reloading-the-application/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/reloading-the-application/middleware/index.html b/projects/leiningen/todo-app/reloading-the-application/middleware/index.html index a76cad3c..6c5a0025 100644 --- a/projects/leiningen/todo-app/reloading-the-application/middleware/index.html +++ b/projects/leiningen/todo-app/reloading-the-application/middleware/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/reloading-the-application/test-your-code-reloads/index.html b/projects/leiningen/todo-app/reloading-the-application/test-your-code-reloads/index.html index 2170d284..58404d5c 100644 --- a/projects/leiningen/todo-app/reloading-the-application/test-your-code-reloads/index.html +++ b/projects/leiningen/todo-app/reloading-the-application/test-your-code-reloads/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/task-handlers/add-a-task/index.html b/projects/leiningen/todo-app/task-handlers/add-a-task/index.html index 303788a1..5c9a4bce 100644 --- a/projects/leiningen/todo-app/task-handlers/add-a-task/index.html +++ b/projects/leiningen/todo-app/task-handlers/add-a-task/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/task-handlers/delete-a-task/index.html b/projects/leiningen/todo-app/task-handlers/delete-a-task/index.html index ab640bcf..284adb74 100644 --- a/projects/leiningen/todo-app/task-handlers/delete-a-task/index.html +++ b/projects/leiningen/todo-app/task-handlers/delete-a-task/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/task-handlers/index.html b/projects/leiningen/todo-app/task-handlers/index.html index b2875de0..7824135e 100644 --- a/projects/leiningen/todo-app/task-handlers/index.html +++ b/projects/leiningen/todo-app/task-handlers/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/task-handlers/show-task/index.html b/projects/leiningen/todo-app/task-handlers/show-task/index.html index fc0cafab..ee2efd55 100644 --- a/projects/leiningen/todo-app/task-handlers/show-task/index.html +++ b/projects/leiningen/todo-app/task-handlers/show-task/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/todo-app/unit-test-handler-function/index.html b/projects/leiningen/todo-app/unit-test-handler-function/index.html index 9a468211..be211ff5 100644 --- a/projects/leiningen/todo-app/unit-test-handler-function/index.html +++ b/projects/leiningen/todo-app/unit-test-handler-function/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/leiningen/working-example/index.html b/projects/leiningen/working-example/index.html index 64925482..f6ddc969 100644 --- a/projects/leiningen/working-example/index.html +++ b/projects/leiningen/working-example/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/slack-app/create-slack-app/index.html b/projects/slack-app/create-slack-app/index.html index 36221e0e..89228bfb 100644 --- a/projects/slack-app/create-slack-app/index.html +++ b/projects/slack-app/create-slack-app/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/slack-app/index.html b/projects/slack-app/index.html index 77ab09e9..fa5864f8 100644 --- a/projects/slack-app/index.html +++ b/projects/slack-app/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/slack-app/slack-api-methods/index.html b/projects/slack-app/slack-api-methods/index.html index 57f79cbe..d1b77444 100644 --- a/projects/slack-app/slack-api-methods/index.html +++ b/projects/slack-app/slack-api-methods/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/slack-app/slack-scopes/index.html b/projects/slack-app/slack-scopes/index.html index 15be6ddd..ea4dbfc5 100644 --- a/projects/slack-app/slack-scopes/index.html +++ b/projects/slack-app/slack-scopes/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/status-monitor-deps/application-server/index.html b/projects/status-monitor-deps/application-server/index.html index bd0cb6e6..48a2503b 100644 --- a/projects/status-monitor-deps/application-server/index.html +++ b/projects/status-monitor-deps/application-server/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/status-monitor-deps/continuous-integration/index.html b/projects/status-monitor-deps/continuous-integration/index.html index 094d9a0a..cc3b4bc6 100644 --- a/projects/status-monitor-deps/continuous-integration/index.html +++ b/projects/status-monitor-deps/continuous-integration/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/status-monitor-deps/debugging-requests/index.html b/projects/status-monitor-deps/debugging-requests/index.html index 8c93e295..6708522b 100644 --- a/projects/status-monitor-deps/debugging-requests/index.html +++ b/projects/status-monitor-deps/debugging-requests/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/status-monitor-deps/deployment-via-ci/index.html b/projects/status-monitor-deps/deployment-via-ci/index.html index 238d10dd..6a612c8f 100644 --- a/projects/status-monitor-deps/deployment-via-ci/index.html +++ b/projects/status-monitor-deps/deployment-via-ci/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/status-monitor-deps/index.html b/projects/status-monitor-deps/index.html index 3eb33eee..2d300826 100644 --- a/projects/status-monitor-deps/index.html +++ b/projects/status-monitor-deps/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/status-monitor-deps/refactor-handlers-and-tests/index.html b/projects/status-monitor-deps/refactor-handlers-and-tests/index.html index 3b7801fb..80063fbb 100644 --- a/projects/status-monitor-deps/refactor-handlers-and-tests/index.html +++ b/projects/status-monitor-deps/refactor-handlers-and-tests/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/projects/status-monitor-deps/unit-test-mocking-handlers/index.html b/projects/status-monitor-deps/unit-test-mocking-handlers/index.html index db296882..5f39c2b7 100644 --- a/projects/status-monitor-deps/unit-test-mocking-handlers/index.html +++ b/projects/status-monitor-deps/unit-test-mocking-handlers/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/reference/continuous-integration/heroku/index.html b/reference/continuous-integration/heroku/index.html index cd530484..8525c51b 100644 --- a/reference/continuous-integration/heroku/index.html +++ b/reference/continuous-integration/heroku/index.html @@ -27,7 +27,7 @@ - + @@ -35,7 +35,7 @@ - + diff --git a/reference/index.html b/reference/index.html index 6c99d1f7..79b8f86f 100644 --- a/reference/index.html +++ b/reference/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/reference/ring/index.html b/reference/ring/index.html index eca4a814..e1be8222 100644 --- a/reference/ring/index.html +++ b/reference/ring/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/reference/ring/request-map/index.html b/reference/ring/request-map/index.html index 807bec93..ac5803f0 100644 --- a/reference/ring/request-map/index.html +++ b/reference/ring/request-map/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/relational-databases-and-sql/h2-database/database-tools/index.html b/relational-databases-and-sql/h2-database/database-tools/index.html index d4e5c27a..464ab039 100644 --- a/relational-databases-and-sql/h2-database/database-tools/index.html +++ b/relational-databases-and-sql/h2-database/database-tools/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/relational-databases-and-sql/h2-database/index.html b/relational-databases-and-sql/h2-database/index.html index 2f21767a..9eaf4617 100644 --- a/relational-databases-and-sql/h2-database/index.html +++ b/relational-databases-and-sql/h2-database/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/relational-databases-and-sql/h2-database/schema-design/index.html b/relational-databases-and-sql/h2-database/schema-design/index.html index a2763648..0999866b 100644 --- a/relational-databases-and-sql/h2-database/schema-design/index.html +++ b/relational-databases-and-sql/h2-database/schema-design/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/relational-databases-and-sql/index.html b/relational-databases-and-sql/index.html index 8d9c999b..ed4f6e43 100644 --- a/relational-databases-and-sql/index.html +++ b/relational-databases-and-sql/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/relational-databases-and-sql/managing-connections/index.html b/relational-databases-and-sql/managing-connections/index.html index a06cca97..0af43d9e 100644 --- a/relational-databases-and-sql/managing-connections/index.html +++ b/relational-databases-and-sql/managing-connections/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/relational-databases-and-sql/next-jdbc-library/add-to-project/index.html b/relational-databases-and-sql/next-jdbc-library/add-to-project/index.html index ac123522..14b3bff2 100644 --- a/relational-databases-and-sql/next-jdbc-library/add-to-project/index.html +++ b/relational-databases-and-sql/next-jdbc-library/add-to-project/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/relational-databases-and-sql/next-jdbc-library/connection-pool-lifecycle/index.html b/relational-databases-and-sql/next-jdbc-library/connection-pool-lifecycle/index.html index 741972c3..0d6401c4 100644 --- a/relational-databases-and-sql/next-jdbc-library/connection-pool-lifecycle/index.html +++ b/relational-databases-and-sql/next-jdbc-library/connection-pool-lifecycle/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/relational-databases-and-sql/next-jdbc-library/database-specifications/index.html b/relational-databases-and-sql/next-jdbc-library/database-specifications/index.html index b5670789..d62bb165 100644 --- a/relational-databases-and-sql/next-jdbc-library/database-specifications/index.html +++ b/relational-databases-and-sql/next-jdbc-library/database-specifications/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/relational-databases-and-sql/next-jdbc-library/index.html b/relational-databases-and-sql/next-jdbc-library/index.html index 004cd3c7..633bc277 100644 --- a/relational-databases-and-sql/next-jdbc-library/index.html +++ b/relational-databases-and-sql/next-jdbc-library/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/relational-databases-and-sql/next-jdbc-library/next-jdbc-and-resultsets/index.html b/relational-databases-and-sql/next-jdbc-library/next-jdbc-and-resultsets/index.html index a5e86d72..385829cc 100644 --- a/relational-databases-and-sql/next-jdbc-library/next-jdbc-and-resultsets/index.html +++ b/relational-databases-and-sql/next-jdbc-library/next-jdbc-and-resultsets/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/relational-databases-and-sql/next-jdbc-library/simple-example/index.html b/relational-databases-and-sql/next-jdbc-library/simple-example/index.html index 78025508..a39e1952 100644 --- a/relational-databases-and-sql/next-jdbc-library/simple-example/index.html +++ b/relational-databases-and-sql/next-jdbc-library/simple-example/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/relational-databases-and-sql/postgresql-database/index.html b/relational-databases-and-sql/postgresql-database/index.html index 45ba121d..72be8e61 100644 --- a/relational-databases-and-sql/postgresql-database/index.html +++ b/relational-databases-and-sql/postgresql-database/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/search/search_index.json b/search/search_index.json index b6425f06..47f7cab7 100644 --- a/search/search_index.json +++ b/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Practicalli Clojure Web Services","text":"

Develop server-side web services and API's from the ground up using Clojure following a simple and data-centric design and applying functional programming concepts.

Use a REPL Workflow approach to provide instant feedback on the code behaviour as it is written, validating design decisions as they are made.

"},{"location":"#tools","title":"Tools","text":"

Clojure CLI is used to manage library dependencies and run Clojure code, enhanced with aliases from Practicalli Clojure CLI Config.

Larger projects use Integrant & Integrant REPL to manage components and state, using a reloaded REPL workflow to manage changes in addtion to evaluating functions in the REPL.

Persistence is provides via Postgresql (and eventually JUXT Crux)

tools.build will be used to create Clojure artefacts for deployment, with GitHub actions and Docker used for Continuous Integration and orchestrating systems.

make is a general build tool used to support project development and support automation of wokflow tasks.

Heroku deployment to be archived

Heroku Cloud service deployment approach is being archived as the service no longer provides a developer environment (November 2022)

Older content using Leiningen

Older content uses Leiningen for project configuration. This content can be converted to a Clojure CLI project by creating a deps.edn file containing the relevant dependencies. Add a build.clj configuration to create assets to deploy, e.g. jar & uberjar.

"},{"location":"#library-composition-approach","title":"Library Composition approach","text":"

The Clojure community provides a diverse set of libraries, each focused on a specific need. Libraries are assembled to rapidly develop a tailored solution, avoiding bloat and the unnecessary complexity that comes with large frameworks. Libraries are relatively simple to replace with alternatives or used as inspiration for your own custom functions.

Templates can be used to create example projects with common libraries, with code to show showing how libraries can be wired together. provide examples of libraries working together.

Avoiding large frameworks

Frameworks are design decisions others have made and generalised to solve a range of problem, so there is no guarantee on how many of those decisions are relevant for the current project.

Frameworks tend to include many features not relevant to the current problem, which can be challenging to remove or replace. Frameworks can be over relied upon, taking away an opportunity to think about the most relevant solution.

Clojure does not focus on the classic framework approach like Rails or Spring, for this reason.

"},{"location":"#navigate-the-book","title":"Navigate the book","text":"

Use the mouse or built-in key bindings to navigate the pages of the book

  • P , , : go to previous page
  • N , . : go to next page

Use the search box to quickly find a specific topic

  • F , S , / : open search dialog
  • Down , Up : select next / previous result
  • Esc , Tab : close search dialog
  • Enter : follow selected result
"},{"location":"#sponsor-my-work","title":"Sponsor my work","text":"

All sponsorship recieved is used to maintain and further develop the Practicalli series of books and videos, although most of the work is still done with my own time and cost.

Thank you to Cognitect, Nubank and a wide range of other sponsors from the Clojure community for your continued support

"},{"location":"#creative-commons-license","title":"Creative commons license","text":"This work is licensed under a Creative Commons Attribution 4.0 ShareAlike License (including images & stylesheets)."},{"location":"adding-more-route/using-cond-function/","title":"Using cond function","text":"

Change the if function to cond and define additional routes you want to match on. We will show you a /goodbye route, feel free to add your own.

Edit the src/webdev/core.clj file and update the greet function as follows

(defn greet\n  \"A function to process all requests for the web server.  The default route / returns one message, /goodbye route another. for all other routes an error message is returned\"\n  [request]\n  (cond\n   (= \"/\" (:uri request))\n   {:status 200\n    :body \"Hello, Clojure World.  I now update automatically\"\n    :headers {}}\n   (= \"/goodbye\" (:uri request))\n   {:status 200\n    :body \"This is the end, my old friend\"\n    :headers {}}\n   :else\n   {:status 404\n    :body \"Sorry, page not found\"\n    :headers {}}))\n

Writing a big cond statement for all our routes would be really tedious and difficult to manage. So lets look at Compojure.

"},{"location":"app-servers/","title":"Application servers","text":"

Application servers provide a common platform services to support server-side running of JVM applications (hence the term application server).

These servers are often referred to more generically as web servers as they mostly work over http / https.

Clojure uses embedded servers to support REPL Driven Development, so both new function definitions and server restarts can be managed within the context of a running REPL (avoiding the need to restart the REPL).

"},{"location":"app-servers/#application-components","title":"Application components","text":"
  • Routing
  • Requests
  • Responses
  • Middleware
"},{"location":"app-servers/#practicalli-defacto-library-choices","title":"Practicalli defacto library choices","text":"

Practicalli defacto choices for building web services:

Library Purpose ring/ring Provides Jetty and Ring - managing requests and responses in Clojure using hash-maps metosin/reitit Routing of request and responses, support for ring handlers and middleware (and interceptors)"},{"location":"app-servers/#example-projects","title":"Example Projects","text":"Project Description Status Monitor Clojure CLI project using Httpkit, Compojure for routing, Hiccup and SVG graphics. Deployed via CircleCI on Heroku Banking On Clojure Clojure CLI project using httpkit, Ring utilities, Compojure for routing. relational data store using next.jdbc, HoneySQL, clojure.spec & postgresql. Generative testing using clojure.spec ToDo app Leiningen project using Ring (Jetty), Compojure for routing and Hiccup for HTML generation"},{"location":"app-servers/app-server-logging/","title":"Application Server Logging","text":"
  • What to log in which environments
  • Logging levels
  • Logging as object rather than text
  • mulog
"},{"location":"app-servers/app-server-logging/#simplistic-logging","title":"Simplistic logging","text":"

println function sends information to the standard out and so is a very simple mechanism to create logs from specific parts of the application. This should be used sparingly and is no substitute for a specific logging framework.

println can be useful in the REPL the standard out message as well as the evaluation result (nil) are shown. println can provide additional feedback for non-terminating processes that run in the REPL, such as an application server.

"},{"location":"app-servers/app-server-logging/#logging-to-elastic-search-kibana","title":"Logging to Elastic Search / Kibana","text":"

Log messages as objects, rather than text strings, provides greater sophistication by search tools as the messages have a structure.

  • Elastisch - Clojure client for Elasticsearch and GitHub repository
  • Elasticsearch and Clojure: Getting Started - the practical academic
  • Spandex - Elasticsearch new low level rest-client wrapper
"},{"location":"app-servers/app-server-logging/#problematic-practices","title":"Problematic Practices","text":"

Logging to the REPL - sending lots of logs to the REPL makes the REPL much harder to use directly

Logging strings - logs entries are typically objects and far more searchable and discoverable that strings, so send objects to the logging service

"},{"location":"app-servers/atom-based-restart/","title":"Atom based restart","text":"

A Clojure atom is used to hold a reference to a running server instance.

An atom is a mutable container that holds any type of value. The value in the atom is immutable. The atom is mutable, but only with specific functions, avoiding locking issues often arising with mutable values.

  • swap! the current value in the atom using a function to create a new value
  • reset! the current value in the atom with a specific value
  • deref or @ returns the value contained within the atom
"},{"location":"app-servers/atom-based-restart/#reference-to-server-process","title":"Reference to server process","text":"

A reference to the server process will be held in a Clojure atom, a mutable container. An atom is used as a value for the server reference will be swapped into the atom on server start. The atom is set to nil when the server stops. Using an atom allows for this value to change.

Define a Clojure atom with the initial value of nil.

defonce is used instead of def to prevent the reference to the app-server being lost if the expression is re-evaluated. A restart of the REPL process is required before evaluation the expression has an effect.

(defonce app-server-instance (atom nil))\n
"},{"location":"app-servers/atom-based-restart/#-main-function","title":"-main function","text":"

The -main function determines an HTTP port value, from either an argument, an operating system $PORT environment variable or using the default 8888 value.

-main calls app-server-start which starts the app server and resets the value of the atom with a reference to that instance.

(.stop @app-server-instance) uses the instance reference to stop the server. app-server-stop function check to see if a running instance exists and if so, stops the server.

HTTP Kit timeout

HTTP Kit can be sent a timeout value to gracefully shut down the server. The app-server-stop function sends a :timeout 100 value to the running app server instance.

The REPL is still running, so the server can be started by calling (-main) or (app-server-start 8888).

app-server-restart is a convenience function that stops and starts the application server, meaning the developer only needs to evaluate (app-server-restart)

jettyhttpkit
(ns practicalli.example-webapp\n  (:gen-class)\n  (:require [ring.adapter.jetty :as jetty]\n            [compojure.core :refer [defroutes GET]]))\n\n;; Routing\n(defroutes app\n  (GET \"/\" [] {:status 200 :body \"App Server Running\"}))\n\n\n;; System\n;; Reference to server instance\n(defonce app-server-instance (atom nil))\n\n\n(defn app-server-start\n\"Start Jetty Application server, adding instance to global state\"\n  [port]\n  (reset! app-server-instance\n          (jetty/run-jetty #'app {:port port :join? false})))\n\n\n(defn app-server-stop\n  \"Check for a running app-server instance, shutdown if present\"\n  []\n  (when @app-server-instance\n    (.stop @app-server-instance))\n  (reset! app-server-instance nil))\n\n\n(defn app-server-restart\n  \"Stop and then start the application server, loading in the new code\"\n  []\n  (app-server-stop)\n  (app-server-start (Integer. (or (System/getenv \"PORT\") 8888))))\n\n\n(defn -main\n  \"Determine an HTTP port number and start application server on that port.\n  A value for port can be passed as the first argument to the command to start the application via the CLI\"\n  [& [port]]\n  (let [port (Integer. (or port\n                           (System/getenv \"PORT\")\n                           8888))]\n    (app-server-start port)))\n\n\n;; REPL driven development\n(comment\n (app-server-restart)\n (-main)\n)\n
(ns practicalli.example-webapp\n  (:gen-class)\n  (:require [org.httpkit.server :as app-server]\n            [compojure.core :refer [defroutes GET]]))\n\n\n;; Routing\n\n(defroutes app\n  (GET \"/\" [] {:status 200 :body \"App Server Running\"}))\n\n\n;; System\n\n(defonce app-server-instance (atom nil))\n\n\n(defn app-server-stop\n  \"Gracefully shutdown the server, waiting 100ms\"\n  []\n  (when-not (nil? @app-server-instance)\n    (@app-server-instance :timeout 100)\n    (reset! app-server-instance nil)\n    (println \"INFO: Application server shutting down...\")))\n\n\n(defn app-server-start\n  \"Start the application server and run the application\"\n  [port]\n  (println \"INFO: Starting server on port: \" port)\n\n  (reset! app-server-instance\n          (app-server/run-server #'app {:port (Integer/parseInt port)})))\n\n\n(defn app-server-restart\n  \"Convenience function to stop and start the application server\"\n  []\n  (app-server-stop)\n  (-main))\n\n(defn -main\n  \"Start the application server on a specific port\"\n  [& [port]]\n  (let [port (Integer. (or port (System/getenv \"PORT\") 8888))]\n    (app-server-start port)))\n\n\n;; REPL driven development\n\n(comment\n\n  ;; Re/Start application server\n  (app-server-restart)\n\n  ;; Shutdown server\n  (app-server-stop)\n\n)\n

Http-kit server documentation contains details of asynchronous websockets and HTTP streaming configurations.

"},{"location":"app-servers/clojure-project/","title":"New Clojure Project","text":"

Create a new Clojure project using Clojure CLI

deps-newclj-new

Create a new project using the :project/create alias from Practicalli Clojure CLI Config and the app template using deps-new

clojure -T:project/create :template app :name practicalli/web-service\n

Create a new project using the :project/new alias from Practicalli Clojure CLI Config and the app template using clj-new

clojure -T:project/new :template app :name practicalli/web-service\n

"},{"location":"app-servers/clojure-project/#add-web-server-library","title":"Add web server library","text":"

Add either Ring (Jetty) or Httpkit library as a project dependency to run an embedded server that listens to HTTP requests and passes those requests to the Clojure service.

JettyHTTP Kit

Add a ring library that includes an embedded Jetty server.

The ring/ring library includes all ring libraries including the embedded Jetty server

Edit the project deps.edn file and add the ring/ring {:mvn/version \"1.9.5\"} dependency to the top-level :deps key, which defines the libraries used to make the project.

{:paths [\"src\" \"resources\"]\n :deps {org.clojure/clojure {:mvn/version \"1.11.3\"}\n        ring/ring           {:mvn/version \"1.9.6\"}}}\n

Or add the ring/ring-core and ring/ring-jetty-adapter libraries, saving a few millisecond when starting the project.

{:paths [\"src\" \"resources\"]\n :deps {org.clojure/clojure     {:mvn/version \"1.11.3\"}\n        ring/ring-core          {:mvn/version \"1.9.6\"}\n        ring/ring-jetty-adapter {:mvn/version \"1.9.6\"}}}\n

Add the HTTP Kit Server library which includes the client and server namespaces, although only the Server namespace will be used.

Edit the project deps.edn file and add the http-kit/http-kit {:mvn/version \"2.3.0\"} dependency to the top-level :deps key, which defines the libraries used to make the project.

{:paths [\"src\" \"resources\"]\n :deps {org.clojure/clojure {:mvn/version \"1.11.3\"}\n        http-kit/http-kit   {:mvn/version \"2.3.0\"}}}\n
"},{"location":"app-servers/clojure-project/#ring-library","title":"Ring library","text":"

Ring is a Clojure web applications library inspired by Python's WSGI and Ruby's Rack. By abstracting the details of HTTP into a simple, unified API, Ring allows web applications to be constructed of modular components that can be shared among a variety of applications, web servers, and web frameworks.

Ring interface specification

Ring is composed of several libraries which can be included specifically, rather than requiring all of them with ring/ring

  • ring/ring-core - essential functions for handling parameters, cookies and more
  • ring/ring-devel - functions for developing and debugging Ring applications
  • ring/ring-servlet - construct Java servlets from Ring handlers
  • ring/ring-jetty-adapter - a Ring adapter that uses the Jetty webserver

Ring documentation Ring API docs

"},{"location":"app-servers/create-server/","title":"Create a server","text":"

To create a web (http) server using a common library, e.g. Jetty or Http-kit

  1. Require a library that provides the web server
  2. Create a function to start a server, taking a port number as an option
"},{"location":"app-servers/create-server/#require-web-server-library","title":"Require Web Server Library","text":"jettyhttpkit

Add the web server library to the namespace using a :reqiure directive.

(ns practicalli.web-server\n  (:gen-class)\n  (:require [ring.adapter.jetty :as http-server]))\n

Add the web server library to the namespace using a :reqiure directive.

(ns practicalli.web-server\n  (:gen-class)\n  (:require [org.httpkit.server :as http-server]))\n
"},{"location":"app-servers/create-server/#code-a-basic-web-server","title":"Code a basic web server","text":"jettyhttpkit

Define a function called server-start that takes a value for the port number which the server will listen too.

Call the run-jetty function from ring.adapter.jetty to start the Jetty Server. run-jetty takes several arguments

  • the main request handler, initially just a function (usually a router function to handle many different types of requests)
  • a hash-map of options, e.g. {:port 8080 :join? false}. Options are listed in the run-jetty function definition.

The :port key is associated with an integer value that represents the port number.

The :join key is associated with a boolean value, true if the the REPL process should attached to the Jetty thread (blocking input until server stops), false to continue in a separate thread.

  (defn server-start\n    [port]\n    (http-server/run-jetty #'app {:port port :join? false}))\n

Define a function called server-start that takes a value for the port number which the server will listen too.

Call the run-server function from org.httpkit.server to start the Http-kit server. run-jetty takes several arguments

  • the main request handler, initially just a function (usually a router function to handle many different types of requests)
  • a hash-map of options, e.g. {:port 8080 :join? false}. Options are listed in the run-server function definition.

The :port key is associated with an integer value that represents the port number.

(defn server-start\n  \"Start the application server and run the application\"\n  [port]\n  (app-server/run-server #'app {:port port}))\n

Http-kit server documentation contains details of asynchronous websockets and HTTP streaming configurations.

"},{"location":"app-servers/create-server/#request-handler-function","title":"Request handler function","text":"

The server passes all HTTP requests, converted to a request hash-map by ring, to the app function. The app function returns a ring response hash-map which is sent back to the client (browser) as an Http response.

Define a function that takes a request hash-map as an argument and returns a basic response hash-map.

(defn app [request]\n  {:status  200\n   :headers {:content-type \"text/html\"}\n   :body    \"<h1>Clojure Web Server Alive</h1>\"})\n

:status http status code as an integer - 200 means okay

:header response headers such as content type, a hash-map with optional values (the value can be an empty hash-map {})

:body a string containing the body of the response, such as text, HTML or JSON.

"},{"location":"app-servers/debugging/","title":"Debugging application servers","text":""},{"location":"app-servers/debugging/#debugging-handlers","title":"Debugging handlers","text":"

As handler functions are simply Clojure functions that take a request hash-map, those functions can be called from unit tests or the REPL to test they are working correctly.

"},{"location":"app-servers/debugging/#ring-mock","title":"Ring mock","text":"

Generate mock requests and responses (?) for testing handler functions

"},{"location":"app-servers/debugging/#problematic-practices-to-avoid","title":"Problematic Practices to avoid","text":"

Using (def name ,,,) expressions for debugging is very bad, especially if those expressions are left in production code.

(println ,,,) statements seem convenient however have very limited value. Using the REPL and REPL based debugging tools provide very useful output

"},{"location":"app-servers/http-kit-server-options/","title":"Http-kit Server Options","text":"

Available options are defined in the doc-string of org.httpkit.server/run-server

Options Description :ip Which ip (if has many ips) to bind :port Which port listen incomming request :thread Http worker thread count :queue-size Max job queued before reject to project self :max-body Max http body: 8m :max-ws Max websocket message size :max-line Max http inital line length :proxy-protocol Proxy protocol e/o #{:disable :enable :optional} :worker-name-prefix Worker thread name prefix :worker-pool ExecutorService to use for request-handling (:thread, :worker-name-prefix, :queue-size are ignored if set) :error-logger Arity-2 fn (args: string text, exception) to log errors :warn-logger Arity-2 fn (args: string text, exception) to log warnings :event-logger Arity-1 fn (arg: string event name) :event-names map of HTTP-Kit event names to respective loggable event names :server-header The \"Server\" header. If missing, defaults to \"http-kit\", disabled if nil. :legacy-return-value? true (default) returns a (fn stop-server [& {:keys [timeout] :or {timeout 100}}]) ; false (recommended) Returns the HttpServer which can be used with server-port, ; server-status, server-stop!, etc.

See Httpkit migration documentation to see the minor difference between Http-kit server and other ring compliant servers like Jetty.

"},{"location":"app-servers/java-system-properties/","title":"Java System properties","text":"

System properties can be set on the Java command line using the -Dpropertyname=value syntax. They can also be added at Clojure runtime using (System/getProperties) will return a Properties object with the system properties for the current REPL.

Properties are often defined in a *.properties file to configure the environment in containerized deployment processes. For example, the version of Java used in Heroku containers is set by adding java.runtime.version=11 property to a system.properties file.

"},{"location":"app-servers/java-system-properties/#commonly-used-properties","title":"Commonly used properties","text":"Java Runtime Description java.home JRE home directory java.library.path JRE library search path for search native libraries (usually taken from PATH environment variable) java.class.path JRE classpath e.g., '.' (dot \u2013 used for current working directory). java.ext.dirs JRE extension library path(s) java.version JDK version java.runtime.version JRE version File system Description file.separator symbol for file directory separator ('/' for Unix or '\\' for windows) path.separator symbol for separating path entries in PATH or CLASSPATH. (':' for Unix or ';' for windows) line.separator symbol for end-of-line / new line (\"\\n\" for Unix or \"\\r\\n\" for windows) or /Mac OS X. User system Description user.name the user\u2019s name. user.home the user\u2019s home directory. user.dir the user\u2019s current working directory Operating System Description os.name operating System name os.version operating System version os.arch operating System architecture"},{"location":"app-servers/java-system-properties/#examining-the-system-properties","title":"Examining the system properties","text":"

Evaluating (System/getProperties) on an Ubuntu Linux operating system running Java 11 and Spacemacs with CIDER returned the following properties.

  \"sun.desktop\" = \"gnome\"\n  \"awt.toolkit\" = \"sun.awt.X11.XToolkit\"\n  \"java.specification.version\" = \"11\"\n  \"sun.cpu.isalist\" = \"\"\n  \"sun.jnu.encoding\" = \"UTF-8\"\n  \"java.class.path\" = \"src:resources:/home/practicalli/.m2/repository/org/clojure/clojure/1.10.1/clojure-1.10.1.jar:/home/practicalli/.m2/repository/joda-time/joda-time/2...\n  \"java.vm.vendor\" = \"Ubuntu\"\n  \"sun.arch.data.model\" = \"64\"\n  \"sun.font.fontmanager\" = \"sun.awt.X11FontManager\"\n  \"java.vendor.url\" = \"https://ubuntu.com/\"\n  \"user.timezone\" = \"Europe/London\"\n  \"os.name\" = \"Linux\"\n  \"java.vm.specification.version\" = \"11\"\n  \"sun.java.launcher\" = \"SUN_STANDARD\"\n  \"user.country\" = \"US\"\n  \"sun.boot.library.path\" = \"/usr/lib/jvm/java-11-openjdk-amd64/lib\"\n  \"sun.java.command\" = \"clojure.main -m nrepl.cmdline --middleware [\\\"refactor-nrepl.middleware/wrap-refactor\\\", \\\"cider.nrepl/cider-middleware\\\"]\"\n  \"jdk.debug\" = \"release\"\n  \"sun.cpu.endian\" = \"little\"\n  \"user.home\" = \"/home/practicalli\"\n  \"user.language\" = \"en\"\n  \"java.specification.vendor\" = \"Oracle Corporation\"\n  \"clojure.libfile\" = \".cpcache/4064833315.libs\"\n  \"java.version.date\" = \"2020-04-14\"\n  \"java.home\" = \"/usr/lib/jvm/java-11-openjdk-amd64\"\n  \"file.separator\" = \"/\"\n  \"java.vm.compressedOopsMode\" = \"Zero based\"\n  \"line.separator\" = \"\\n\"\n  \"java.specification.name\" = \"Java Platform API Specification\"\n  \"java.vm.specification.vendor\" = \"Oracle Corporation\"\n  \"java.awt.graphicsenv\" = \"sun.awt.X11GraphicsEnvironment\"\n  \"sun.management.compiler\" = \"HotSpot 64-Bit Tiered Compilers\"\n  \"java.runtime.version\" = \"11.0.7+10-post-Ubuntu-3ubuntu1\"\n  \"user.name\" = \"practicalli\"\n  \"path.separator\" = \":\"\n  \"os.version\" = \"5.4.0-40-generic\"\n  \"java.runtime.name\" = \"OpenJDK Runtime Environment\"\n  \"file.encoding\" = \"UTF-8\"\n  \"java.vm.name\" = \"OpenJDK 64-Bit Server VM\"\n  \"java.vendor.url.bug\" = \"https://bugs.launchpad.net/ubuntu/+source/openjdk-lts\"\n  \"java.io.tmpdir\" = \"/tmp\"\n  \"java.version\" = \"11.0.7\"\n  \"user.dir\" = \"/home/practicalli/projects/clojure/database-access/banking-on-clojure-webapp\"\n  \"os.arch\" = \"amd64\"\n  \"java.vm.specification.name\" = \"Java Virtual Machine Specification\"\n  \"java.awt.printerjob\" = \"sun.print.PSPrinterJob\"\n  \"sun.os.patch.level\" = \"unknown\"\n  \"java.library.path\" = \"/usr/java/packages/lib:/usr/lib/x86_64-linux-gnu/jni:/lib/x86_64-linux-gnu:/usr/lib/x86_64-linux-gnu:/usr/lib/jni:/lib:/usr/lib\"\n  \"java.vendor\" = \"Ubuntu\"\n  \"java.vm.info\" = \"mixed mode, sharing\"\n  \"java.vm.version\" = \"11.0.7+10-post-Ubuntu-3ubuntu1\"\n  \"sun.io.unicode.encoding\" = \"UnicodeLittle\"\n  \"apple.awt.UIElement\" = \"true\"\n  \"java.class.version\" = \"55.0\"\n
"},{"location":"app-servers/jetty-server-options/","title":"Jetty Server Options","text":"

Option keys available to pass to the run-jetty function from ring.adaptor.jetty

Option Description :configurator a function called with the Jetty Server instance :async? if true, treat the handler as asynchronous :async-timeout async context timeout in ms (defaults to 0, no timeout) :async-timeout-handler an async handler to handle an async context timeout :port the port to listen on (defaults to 80) :host the hostname to listen on :join? blocks the thread until server ends (defaults to true) :daemon? use daemon threads (defaults to false) :http? listen on :port for HTTP traffic (defaults to true) :ssl? allow connections over HTTPS :ssl-port the SSL port to listen on (defaults to 443, implies :ssl? is true) :ssl-context an optional SSLContext to use for SSL connections :exclude-ciphers when :ssl? is true, additionally exclude these cipher suites :exclude-protocols when :ssl? is true, additionally exclude these protocols :replace-exclude-ciphers? when true, :exclude-ciphers will replace rather than add to the cipher exclusion list (defaults to false) :replace-exclude-protocols? when true, :exclude-protocols will replace rather than add to the protocols exclusion list (defaults to false) :keystore the keystore to use for SSL connections :keystore-type the keystore type (default jks) :key-password the password to the keystore :keystore-scan-interval if not nil, the interval in seconds to scan for an updated keystore :thread-pool custom thread pool instance for Jetty to use :truststore a truststore to use for SSL connections :trust-password the password to the truststore :max-threads the maximum number of threads to use (default 50) :min-threads the minimum number of threads to use (default 8) :max-queued-requests the maximum number of requests to be queued :thread-idle-timeout Set the maximum thread idle time. Threads that are idle for longer than this period may be stopped (default 60000) :max-idle-time the maximum idle time in milliseconds for a connection (default 200000) :client-auth SSL client certificate authenticate, may be set to :need,:want or :none (defaults to :none) :send-date-header? add a date header to the response (default true) :output-buffer-size the response body buffer size (default 32768) :request-header-size the maximum size of a request header (default 8192) :response-header-size the maximum size of a response header (default 8192) :send-server-version? add Server header to HTTP response (default true)"},{"location":"app-servers/middleware/","title":"Middleware","text":"

Apply common transformations to request and or response hash-maps, such as security tokens, cookie management, session and access management, presentation templates, etc.

Middleware is implemented by Clojure functions that receive a handler as an argument and return a handler as a result.

"},{"location":"app-servers/middleware/#middleware-in-ring","title":"Middleware in Ring","text":"

Middleware can wrap handlers or other middleware, affecting their behavior.

For example the wrap-reload middleware enables live reloading by detecting file changes and reloading affected functions into their namespace, before the request is passed to the relevant handler function

Middleware provided by Ring includes:

In ring/ring-core:

  • wrap-cookies (ring.middleware.cookies)
  • wrap-file (ring.middleware.file)
  • wrap-file-info (ring.middleware.file-info)
  • wrap-flash (ring.middleware.flash)
  • wrap-keyword-params (ring.middleware.keyword-params)
  • wrap-multipart-params (ring.middleware.multipart-params
  • wrap-nested-params (ring.middleware.nested-params
  • wrap-params (ring.middleware.params)
  • wrap-session (ring.middleware.session)

In ring/ring-devel:

  • wrap-lint (ring.middleware.lint)
  • wrap-reload (ring.middleware.reload)
  • wrap-stacktrace (ring.middleware.stacktrace)
"},{"location":"app-servers/overview/","title":"Overview of Application servers","text":"

Application servers for Clojure run an embedded JVM application, such as Jetty or http-kit server.

App servers are started within the Clojure REPL process, as an embedded server. This approach means that during development a server can be restarted to load in new code and immediately update the running application, without having to restart the REPL.

Embedded servers in Clojure

Clojure takes a more distributed approach to deployment, starting an embedded application server within the application itself. This approach is more conducive to the container and cloud compute infrastructure. Scaling is achieved by running multiple instances of the application, each on its own embedded application server.

In Java was common to have a single application server with all applications deployed as Jar or War archives. This fitted with the classic architecture of deploying on a single resource rich physical hardware server. Clojure applications can also be deployed in this classic approach if required.

"},{"location":"app-servers/overview/#which-application-server-to-use","title":"Which Application Server to use","text":"

The most commonly used application servers are in the table below, with Jetty being the most common as it is wrapped by the ring library.

Jetty is the the defacto server in Practicalli guides, with occasional examples using other libraries.

Application Server Description Eclipse Jetty The original embedded Java application server most commonly used for Clojure web apps Http-kit High performance Clojure/Java application server Apache Tomcat Classic Java application server, very common in JVM environments Netty Java NIO asynchronous event-driven network application framework Aleph HTTP Clojure (Netty) Ring-compliant server with support for returning Manifold for asynchronous programming"},{"location":"app-servers/overview/#eclipse-jetty","title":"Eclipse Jetty","text":"

Jetty is the most commonly used application server on the Java Virtual machine.

Eclipse Jetty provides a Web server and javax.servlet container, plus support for HTTP/2, WebSocket, OSGi, JMX, JNDI, JAAS and many other integrations. These components are open source and available for commercial use and distribution.

Eclipse Jetty is used in a wide variety of projects and products, both in development and production. Jetty can be easily embedded in devices, tools, frameworks, application servers, and clusters. See the Jetty Powered page for more uses of Jetty.

The current recommended version for use is Jetty 9

"},{"location":"app-servers/overview/#http-kit","title":"Http-kit","text":"

HTTP Kit is a minimalist, efficient, Ring-compatible HTTP client/server for Clojure.

HTTP Kit uses a event-driven architecture to support highly concurrent a/synchronous web applications. Feature a unified API for WebSocket and HTTP long polling/streaming

The underlying server is implemented in Java with a Clojure wrapper.

"},{"location":"app-servers/overview/#apache-tomcat","title":"Apache Tomcat","text":"

Apache Tomcat is an open source implementation of the Java Servlet, JavaServer Pages, Java Expression Language and Java WebSocket technologies. The Java Servlet, JavaServer Pages, Java Expression Language and Java WebSocket specifications are developed under the Java Community Process.

Apache Tomcat software powers numerous large-scale, mission-critical web applications across a diverse range of industries and organizations, example are listed on the PoweredBy page.

Apache Tomcat, Tomcat, Apache, the Apache feather, and the Apache Tomcat project logo are trademarks of the Apache Software Foundation.

Apache Tomcat 9.0 is the current stable version with active development now on version 10.

Tomcat can be run in embedded mode, so it is not necessary to build a WAR file and deploy it in a standalone Tomcat server.

  • Create a Java Web Application Using Embedded Tomcat - Heroku

Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.

Netty is a NIO client server framework which enables quick and easy development of network applications such as protocol servers and clients. It greatly simplifies and streamlines network programming such as TCP and UDP socket server.

"},{"location":"app-servers/overview/#aleph-and-manifold","title":"Aleph and Manifold","text":"

Manifold provides streams focused libraries, such as Aleph HTTP for web applications and TCP/UDP for more general networking.

"},{"location":"app-servers/route-requests/","title":"Route Requests","text":"

Https servers accept many different types of requests, defined as a combination of

  • Https protocol - GET, POST, etc.
  • Path of resource - page, api endpoint, etc.
"},{"location":"app-servers/route-requests/#defining-routes","title":"Defining routes","text":"

Reitit is a data driven approach to defining routes and support ring Ring request, response and middleware.

Compojure provides the defroutes macro and a simple DSL for defining routes.

reititcompojure

Add reitit as a dependency

deps.edn
{:paths\n [\"src\" \"resources\"]\n\n :deps\n {org.clojure/clojure {:mvn/version \"1.10.1\"}\n  http-kit/http-kit   {:mvn/version \"2.3.0\"}\n  ring/ring-core      {:mvn/version \"1.8.1\"}\n  metosin/reitit      {:mvn/version \"0.5.18\"}}}\n

Restart the REPL (unless Reitit was added using hotload)

Add reitit namespace to web-server namespace

(ns practicalli.web-server\n  (:gen-class)\n  (:require [ring.adapter.jetty :as http-server]\n            [reitit.core :as routing]))\n

Define example routes

(def routes\n [[\"/\"\n    {:handler (constantly {:status 200\n                           :headers {:content-type \"text/html\"}\n                           :body \"<h1>Welcome to Practicalli Clojure Web Server\"})}\n [\"status\"\n   {:handler (constantly {:status 200\n                          :headers {:content-type \"application/json\"}\n                          :body {:alive true}})}]]])\n

Define example routes using the reitit ring-handler

(defroutes app\n  \"Initial routes for web server\"\n  []\n\n  (reitit-ring/ring-handler\n   (reitit-ring/router       ;; routes defined as a vector of vectors\n    [\n     [\"/\"\n\n     ]\n]\n    ;; Middleware, coersion & content negotiation\n\n   ;; Default hanlder passed to ring-handler\n   (ring/routes\n    ;; Respond to any other route - returns blank page\n    ;; TODO: create custom handler for routes not recognised\n    (ring/create-default-handler))\n)))\n

Add Compojure as a dependency

{:paths\n [\"src\" \"resources\"]\n\n :deps\n {org.clojure/clojure {:mvn/version \"1.10.1\"}\n  http-kit/http-kit   {:mvn/version \"2.3.0\"}\n  ring/ring-core      {:mvn/version \"1.8.1\"}\n  ring/ring-devel     {:mvn/version \"1.8.1\"}\n  compojure/compojure {:mvn/version \"1.6.1\"}}}\n

Add Compojure namespace to web-server namespace

(ns practicalli.web-server\n  (:gen-class)\n  (:require [ring.adapter.jetty :as http-server]\n\n            [compojure.core  :refer [defroutes GET]]\n            [compojure.route :refer [not-found]\n\n            [ring.middleware.reload :refer [wrap-reload]]]))\n

Restart the REPL (unless Compojure was added using hotload)

Define routes using compojure

(defroutes app\n  (GET \"/\"         [] handler/welcome-page)\n  (GET \"/accounts\" [] handler/accounts-overview-page)\n  (GET \"/account\"  [] handler/account-history)\n  (GET \"/transfer\" [] handler/money-transfer)\n  (GET \"/payment\"  [] handler/money-payment)\n  (GET \"/register\" [] handler/register-customer) )\n
"},{"location":"app-servers/routing-libraries/","title":"Routing libraries","text":""},{"location":"app-servers/routing-libraries/#application-logic","title":"Application Logic","text":"
  • Routing
  • Requests
  • Responses
  • handlers
  • middleware
  • Serving static content
"},{"location":"app-servers/routing-libraries/#ring","title":"Ring","text":"

Ring is the defacto library for server-side web applications. Even if not using the Ring library, the contents that Ring established are used by other libraries.

"},{"location":"app-servers/routing-libraries/#compojure","title":"Compojure","text":"

Compojure is a library that works with Ring to manage Compojure also has convenience functions that make ring responses easier to generate.

In this section we will update our project to use Compojure.

"},{"location":"app-servers/routing-libraries/#bidi-bi-directional-uri-dispatch","title":"Bidi - Bi-directional URI dispatch","text":"

https://github.com/juxt/bidi Clojure and ClojureScript

bidi is written to do 'one thing well' (URI dispatch and formation) and is intended for use with Ring middleware, HTTP servers (including Jetty, http-kit and aleph) and is fully compatible with Liberator.

"},{"location":"app-servers/routing-libraries/#yada-resources-as-data","title":"yada - resources as data","text":"

yada is a web library for Clojure, designed to support the creation of production services via HTTP.

It has the following features:

  • Standards-based, comprehensive HTTP coverage (content negotiation, conditional requests, etc.)
  • Parameter validation and coercion, automatic Swagger support
  • Rich extensibility (methods, mime-types, security and more)
  • Asynchronous, efficient interceptor-chain design built on manifold
  • Excellent performance, suitable for heavy production workloads

yada is a sibling library to bidi - whereas bidi is based on routes as data, yada is based on resources as data.

"},{"location":"app-servers/routing-libraries/#reitit","title":"Reitit","text":"

A data approach to routing

"},{"location":"app-servers/routing/","title":"Routing","text":"

Compojure Bidi Reitit

"},{"location":"app-servers/routing/#injecting-resources-using-routing","title":"Injecting resources using routing","text":"
(defroutes\n  GET /accounts/account [] (partial db/connection request))\n

And then have a handler that takes both a request and resource arguments. This makes the handler pure in respect that it does not require any external data to do its job (yes the connection is external, but the reference to the connection is provided as an argument to the side effect is abstracted away).

Middleware could also be used to wrap all the routes, however, if some routes do not use the database then this approach adds redundancy and makes the abstraction feel too high a level in the application design. This approach also makes it harder to test the handlers as normal Clojure functions, as its not possible to simply call that function with an argument.

"},{"location":"app-servers/routing/#resources","title":"Resources","text":"
  • How to manage database connections in Clojure - ClojureVerse

  • https://devcenter.heroku.com/articles/database-connection-pooling-with-clojure

  • https://stackoverflow.com/questions/19776462/passing-state-as-parameter-to-a-ring-handler
"},{"location":"app-servers/simple-restart/","title":"Simple restart approach","text":"

Use a def expression to create a named reference to the running server, providing a simple way to stop the application server.

jettyhttpkit

The app-server starts when the application starts, as the app-server-start is called from -main once the port value has been taken from either an argument to -main, an operating system $PORT environment variable or the default 8080.

(def app-server-instance (-main 8080)) is placed within a (comment ) expression. This provides a manual way for the developer to start the application server.

The app-server-instance is a symbol pointing to the server instance. This instance can be used to shut down the server.

When the developer evaluates (.stop app-server-instance), the instance is used to shut down the running application server.

The REPL itself is still running, so the application can be started again quickly by evaluating (def app-server-instance (-main 8888)).

(ns practicalli.example-webapp\n  (:gen-class)\n  (:require [ring.adapter.jetty :as jetty]\n            [compojure.core :refer [defroutes GET]]))\n\n\n;; Routing\n\n(defroutes app\n  (GET \"/\" [] {:status 200 :body \"App Server Running\"}))\n\n\n;; System\n\n  (defn app-server-start\n    [port]\n    (jetty/run-jetty #'app {:port port :join? false}))\n\n\n  (defn -main [& [port]]\n    (let [port (Integer. (or port\n                             (System/getenv \"PORT\")\n                             8888))]\n      (app-server-start port)))\n\n\n;; REPL driven development\n\n(comment\n\n  (def app-server-instance (-main 8888))\n  (.stop app-server-instance)\n)\n

The -main function identifies a value for a port and calls app-server-start function which starts the http-kit server.

(def app-server-instance (-main 8888)) is placed within a (comment ) expression. This provides a manual way for the developer to start the application server.

The app-server-instance reference can be used to stop the app-server by calling it with the arguments :timeout 100 and gracefully shutting down the server, (app-server-instance :timeout 100).

(ns practicalli.example-webapp\n  (:gen-class)\n  (:require [org.httpkit.server :as app-server]\n            [compojure.core :refer [defroutes GET]]))\n\n\n;; Routing\n\n(defroutes app\n  (GET \"/\" [] {:status 200 :body \"App Server Running\"}))\n\n\n;; System\n\n(defn app-server-start\n  \"Start the application server and run the application\"\n  [port]\n  (println \"INFO: Starting server on port: \" port)\n  (app-server/run-server #'app {:port port}))\n\n(defn -main\n  \"Start the application server on a specific port\"\n  [& [port]]\n  (let [port (Integer. (or port (System/getenv \"PORT\") 8888))]\n    (app-server-start port)))\n\n\n;; REPL driven development\n\n(comment\n\n  (def app-server-instance (-main 8888))\n  (app-server-instance :timeout 100)\n)\n

Http-kit server documentation contains details of asynchronous websockets and HTTP streaming configurations.

"},{"location":"app-servers/start-server/","title":"Start a server","text":"

The very basics of starting an Http server with Jetty or Httpkit.

Projects containing application servers are typically started from the command line, especially when deployed.

During development an application server is typically managed via the REPL, either calling a or via a component lifecycle service (mount, integrant, component)

"},{"location":"app-servers/start-server/#the-main-function","title":"The -main function","text":"

The -main function is used to capture optional arguments to set configuration such as port and ip address. These values can either be passed as function arguments, operating system environment variables or default values.

Define a -main function that optionally takes an argument that will be used as a port number to listen for requests.

A value is bound to the local name port using either the argument to the -main function, an operating system environment variable or the default port number.

(defn -main\n  \"Start the application server on a specific port\"\n  [& [port]]\n  (let [port (Integer. (or port\n                           (System/getenv \"PORT\")\n                           8080))]\n    (server-start port)))\n

The app-server-start function starts a specific application server, eg. Jetty, http-kit-server.

"},{"location":"app-servers/start-server/#listen-on-port","title":"Listen on Port","text":"

Application servers listen on a specific port for messages over HTTP/S which is set when starting the application server. Public facing application will receive requests over port 80, although for security reasons a firewall or proxy is placed in front of the application server which redirects traffic to an internal port number for the application server.

Cloud application platforms (Heroku, Google, AWS) provide a port number each time a new cloud environment (eg. container) is provisioned, so an application server should read in this dynamically assigned port number from the provided operating system environment variable.

"},{"location":"app-servers/start-server/#integer-type-port-number","title":"Integer type port number","text":"

(Integer. port-number) will cast either a string or number to a JVM integer type. (Integer/parseInt port-number) is also commonly used and has the same result.

These functions are used to ensure an Integer type value is passed to the application server. As Clojure uses Java application servers (Java app servers have decades of development) then the correct type must be passed to avoid error.

"},{"location":"app-servers/start-server/#get-environment-variables","title":"Get environment variables","text":"

A common way to get environment variables from the operating system is to use the Java method, System/getenv.

(System/getenv \"PORT\") returns the value of the `PORT` environment variable.  If the variable is not set, then nil is returned (TODO: check what is actually returned)\n

System/getProperty method will get specific values from Java .properties files, usually from a system.properties file in the root of the project. System/getProperties will get all properties found in .properties files in the project. Settings typically found in the system.properties files include version of Java

java.runtime.version=11\n

The Java Tutorials: Properties

(System/getenv) returns a hash-map of all current environment variables. Wrap in a def name to make a useful REPL tool to inspect the current environment variables available.

(comment\n  ;; Get all environment variables\n  ;; use a data inspector to view environment-variables name\n  (def environment-variables\n    (System/getenv))\n  )\n

Use tools such as the clojure inspector or data inspector tools in Clojure aware editors (e.g. CIDER inspector)

"},{"location":"app-servers/start-server/#environ-library-for-environment-variables","title":"Environ library for environment variables","text":"

Use environment variables as Clojure keywords.

weavejester/environ library will source environment settings from Leiningen and Boot configurations, the Operating System and Java system property files. The library works for Clojure and ClojureScript projects.

Include environ/environ.core library as a dependency in the Clojure project configuration

Require the library in the web-service namespace ns form

(require '[environ.core :refer [env]])\n

Then call the env function with a property name to return the value associated with that property.

(env :port)\n
"},{"location":"app-servers/static-content/","title":"Static Content","text":"

Avoid serving large or complex static content

The most efficient and secure way of serving static content from a Clojure (or any other app) is to not server content directly. Using a static web server such as nginx or apache httpd provides a separation of concerns.

Nginx and Apache Httpd provide many features for serving static content and managing mime types, etc which would have little value to implement inside a Clojure web application.

Nginx and Apache Httpd can be configured as a reverse proxy, only redirecting specific request to the Clojure application

"},{"location":"assets/images/social/","title":"Social Cards","text":"

Social Cards are visual previews of the website that are included when sending links via social media platforms.

Material for MkDocs is configured to generate beautiful social cards automatically, using the colors, fonts and logos defined in mkdocs.yml

Generated images are stored in this directory.

"},{"location":"building-api/","title":"Server-side API's","text":"

APIs are an excellent way to make a service accessible to the wider world.

Server-side apis in Clojure can be self-documenting (OpenAPI), highly efficient and a joy to develop.

The ring specification abstracts HTTP requests & responses, automatically converting to and from Clojure hash-maps which are far simpler and effecitive to work with.

Clojure routing libraries typically take either use a data configuration or provide a Domain Specific Language (DSL) for defining routes.

Practicalli recommends Reitit

There are many excellent routing libraries available for Clojure, however reitit is very well documented and takes a data centric approach.

Practicalli has found reitit very easy to work with on new project and to migrate existing project.

"},{"location":"building-api/#reitit","title":"Reitit","text":"

Reitit is a data defined routing library which can be used with the Ring specification and middleware.

Route configuration is pre-compiled, so if highly efficient. Routing is also bi-directional. Defining route configuration as data simplifies validation, with errors returned containing clojure.spec information.

  • define routes as data (validated with clojure.spec)
  • ring support
  • middleware support
  • data coercion
  • validation (clojure.spec or Malli)
  • swagger (openapi) documentation with custom templates

Reitit

"},{"location":"building-api/#compojure","title":"Compojure","text":"

compojure library provides the defroutes macro and HTTP methods (GET, POST, etc) as a DSL for defining routes.

"},{"location":"building-api/#compojure-api","title":"compojure-api","text":"

Compojure API library and template provide a quick way to create an API, by extending Compojure features.

compojure-api library

Compojure API documentation

"},{"location":"building-api/#prismatic-schema","title":"Prismatic schema","text":"

Schema validation defines the shape of any data that the API will respond with as well as any data that is sent along with a request.

"},{"location":"building-api/#self-documenting-with-swagger","title":"Self-documenting with Swagger","text":"

This template contains Swagger that documents the API's you are creating and ring-swagger constructs the documentation as you create your code.

"},{"location":"building-api/#references","title":"References","text":"
  • Getting started with Compojure API
  • ring-swagger
  • RESTful CRUD APIs Using Compojure-API and Toucan - part1
  • RESTful CRUD APIs Using Compojure-API and Toucan - part2
"},{"location":"building-api/#alternatives","title":"Alternatives","text":"
  • Yada introduction - JUXT.pro
  • Yada manual
"},{"location":"building-api/cheshire/","title":"Cheshire - fast JSON encoding","text":"

Cheshire is fast JSON encoding library with support for Date/UUID/Set/Symbol encoding and SMILE support.

  • Github repository and usage
  • API documentation
"},{"location":"building-api/compojure-api-template/","title":"compojure-api template","text":"

Quickly create the basics of a server-side webapp with the compojure-api template for Leiningen.

lein new compojure-api project-name\n

This command creates a new Clojure project in a directory called scoreboard-service.

"},{"location":"building-api/compojure-api-template/#adding-tests","title":"Adding tests","text":"

Using either of the +clojure-test or +midge will add the specific test library to the project created.

lein new compojure-api project-name +clojure-test\n
"},{"location":"building-api/create-compojure-api-project/","title":"Create a compojure-api project","text":""},{"location":"building-api/create-compojure-api-project/#notecreate-a-project-with-tests","title":"NOTE::Create a project with tests","text":"
lein new compojure-api my-api +clojure-test\n
"},{"location":"building-api/create-compojure-api-project/#deconstruct-the-project","title":"Deconstruct the project","text":"

The project this template creates is relatively simple in terms of dependencies in the project.clj file

  (defproject my-api \"0.1.0-SNAPSHOT\"\n    :description \"Experimenting with the compojure-api\"\n    :dependencies [[org.clojure/clojure \"1.8.0\"]\n                   [metosin/compojure-api \"1.1.11\"]]\n    :ring {:handler my-api.handler/app}\n    :uberjar-name \"server.jar\"\n    :profiles {:dev {:dependencies [[javax.servlet/javax.servlet-api \"3.1.0\"]\n                                   [cheshire \"5.5.0\"]\n                                   [ring/ring-mock \"0.3.0\"]]\n                    :plugins [[lein-ring \"0.12.0\"]]}})\n

Interesting things to note are its using the lein-ring plugin, so we should run the application with lein ring server.

When we want to deploy the application then we should use the lein ring uberjar command to create an uberjar (a java archive file that includes our Clojure application and the clojure.core library, so we can just run it as a java library).

"},{"location":"building-api/create-compojure-api-project/#dev-profile","title":":dev profile","text":"

In the :dev profile, dependencies include ring/ring-mock library to help us test our server-side web application.

There is also the cheshire library to help us work with JSON data in an efficient way.

"},{"location":"building-api/json-files/","title":"Working with JSON files","text":"

slurp will read files into our Clojure code.

(slurp \"spicy-vegan-pepperoni.json\")\n

We can use cheshire library to convert the JSON to a Clojure data structure.

(cheshire/parse-string\n  (slurp \"spicy-vegan-pepperoni.json\"))\n;; => {\"name\" \"Spicy Vegan Pepperoni\", \"size\" \"XL\", \"origin\" {\"country\" \"PO\", \"city\" \"Tampere\"}, \"description\" \"Healthy and delicious Vegan version of a double pepperoni pizza with some jalapenos to spice it up\"}\n

Lets pretty print the Clojure data structure to make it easier to read

(clojure.pprint/pprint\n  (cheshire/parse-string\n    (slurp \"spicy-vegan-pepperoni.json\")))\n;; => nil\n\n;; From the REPL output\n{\"name\"   \"Spicy Vegan Pepperoni\",\n \"size\"   \"XL\",\n \"origin\" {\"country\" \"PO\", \"city\" \"Tampere\"},\n \"description\"\n \"Healthy and delicious Vegan version of a double pepperoni pizza with some jalapenos to spice it up\"}\n
"},{"location":"building-api/plumatic-schema/","title":"Plumatic schema - defining the shape of data","text":"

As an API is an external system then it is important to define the shape of data coming into and leaving the application.

Plumatic schema is a simple way to define the shape of data in Clojure without having to define fixed static types.

"},{"location":"building-api/plumatic-schema/#dice-roll-result","title":"Dice roll result","text":"

In this example. our API is related to a game and we call our API to get the result of a dice roll

(s/defschema DiceRollResult\n  {:result s/Int})\n
"},{"location":"building-api/plumatic-schema/#customer","title":"Customer","text":"

Most business systems (and most systems in general) have some concept of a user or customer. Here we define a schema for a valid customer.

In this example, a valid customer lives in one of two cities, as defined using an schema enumeration (enum)

(s/defschema Customer {:id      s/Str,\n                       :name    s/Str\n                       :address {:street s/Str\n                                 :city   (s/enum :maidstone :dover)}})\n
"},{"location":"building-api/plumatic-schema/#pizza-order","title":"Pizza Order","text":"

We can make the data be as specific or as general as we need. Enumerations allow us to limit the set of valid options. If there were a lot of options then it may be useful to define them as a data structure in their own namespace.

(s/defschema Pizza\n  {:name           s/Str\n   :size           (s/enum :L :M :S)\n   :origin         {:country (s/enum :FI :PO)\n                    :city    s/Str}\n   (s/optional-key\n     :description) s/Str})\n\n(s/defschema Customer {:id      s/Str,\n                       :name    s/Str\n                       :address {:street s/Str\n                                 :city   (s/enum :maidstone :dover)}})\n
"},{"location":"building-api/plumatic-schema/#legitimate-ferry-company","title":"Legitimate Ferry Company","text":"

We can use the data we define to ensure that something is valid. For example, if a Ferry company uses this API to register themselves as a business, we can ensure we capture the number of ferries they have.

In the logic of our API we can use the number of ferries value to check if we should register this company. If it has no ferries, then we shouldn't register the company.

(s/defschema FerryCompany\n  {:name              s/Str\n   :number-of-ferries Long\n   :country           (s/enum :UK :France :Netherlands)\n   (s/optional-key\n     :description)    s/Str})\n
"},{"location":"building-api/ring-mock/","title":"ring-mock","text":"

ring-mock is a testing library for server-side applications

Ring-Mock creates Ring request maps to assist with defining tests in Clojure.

"},{"location":"building-api/ring-mock/#installation","title":"Installation","text":"

Add the following development dependency to your project.clj file:

[ring/ring-mock \"0.3.2\"]\n
"},{"location":"building-api/ring-mock/#examples","title":"Examples","text":"
(ns your-app.core-test\n  (:require [clojure.test :refer :all]\n            [your-app.core :refer :all]\n            [ring.mock.request :as mock]))\n\n(deftest your-handler-test\n  (is (= (your-handler (mock/request :get \"/doc/10\"))\n         {:status  200\n          :headers {\"content-type\" \"text/plain\"}\n          :body    \"Your expected result\"})))\n\n(deftest your-json-handler-test\n  (is (= (your-handler (-> (mock/request :post \"/api/endpoint\")\n                           (mock/json-body {:foo \"bar\"})))\n         {:status  201\n          :headers {\"content-type\" \"application/json\"}\n          :body    {:key \"your expected result\"}})))\n
"},{"location":"building-api/ring-swagger/","title":"ring-swagger","text":"

ring-swagger is a Swagger 2.0 implementation for Clojure/Ring using Plumatic Schema (support for clojure.spec via spec-tools.

  • Transforms deeply nested Schemas into Swagger JSON Schema definitions
  • Extended & symmetric JSON & String serialization & coercion
  • Middleware for handling Schemas Validation Errors & Publishing swagger-data
  • Local api validator
  • Swagger artifact generation
  • swagger.json via ring.swagger.swagger2/swagger-json
  • Swagger UI bindings. (get the UI separately as jar or from NPM)
"},{"location":"building-api/ring-swagger/#documentation","title":"Documentation","text":"
  • ring-swagger API documentation
"},{"location":"building-api/swagger/","title":"Swagger - self describing APIs","text":""},{"location":"building-api/terminology/","title":"Terminology","text":""},{"location":"building-api/terminology/#application-programming-interface-api","title":"Application Programming Interface (API)","text":"

An API defines how to use another piece of software. The API shows you the public functions and data you can use with your own software, allowing you to do more with less code.

https://en.wikipedia.org/wiki/Application_programming_interface

"},{"location":"building-api/terminology/#uniform-resource-identifier-uri","title":"Uniform Resource Identifier (URI)","text":"

A Uniform Resource Identifier (URI) is a string of characters that unambiguously identifies a particular resource. To guarantee uniformity, all URIs follow a predefined set of syntax rules,[1] but also maintain extensibility through a separately defined hierarchical naming scheme (e.g. \"http://\").

https://en.wikipedia.org/wiki/Uniform_Resource_Identifier

"},{"location":"building-api/terminology/#uniform-resource-locator-url","title":"Uniform Resource Locator (URL)","text":"

A web page and the images and videos it contains all have their own URL, a specific address where they can be found on the Internet.

A URL is a specific form of URI for web pages and the content that they contain.

https://en.wikipedia.org/wiki/URL

"},{"location":"building-api/testing-api/","title":"Testing our API","text":"

We used the clojure-test option when we created the project, so we will use this built in library.

"},{"location":"building-api/testing-api/#writing-tests","title":"Writing tests","text":"

Writing tests is just the same as other Clojure applications.

(deftest a-test\n\n  (testing \"Test GET request to /hello?name={a-name} returns expected response\"\n    (let [response (app (-> (mock/request :get  \"/api/plus?x=1&y=2\")))\n          body     (parse-body (:body response))]\n      (is (= (:status response) 200))\n      (is (= (:result body) 3)))))\n
"},{"location":"building-api/testing-api/#using-helper-functions","title":"Using helper functions","text":"

It is good practice to create helper functions to extract out common code into its onw function. This saves on duplication, reduces maintenance and should improve the readability of your tests.

Here is an example of a helper function that reads data in the form of JSON and creates a Clojure map for us to work with.

(defn parse-body [body]\n  (cheshire/parse-string (slurp body) true))\n
"},{"location":"building-api/testing-api/#hintcheshire-api","title":"HINT::Cheshire API","text":"

See the parse-string description in the Cheshire API documentation

"},{"location":"building-api/testing-api/#including-test-libraries-in-the-namespace","title":"Including test libraries in the namespace","text":"

Including the testing libraries is standard :require statements.

(ns my-api.core-test\n  (:require [cheshire.core :as cheshire]\n            [clojure.test :refer :all]\n            [my-api.handler :refer :all]\n            [ring.mock.request :as mock]))\n
"},{"location":"building-api/testing-api/#ringmock-library","title":"ring.mock library","text":"

A library to help you mock parts of your server-side application. This works just as well for APIs as web applications.

"},{"location":"building-api/testing-api/#hintwriting-files-in-clojure-with-spit","title":"HINT::Writing files in Clojure with spit","text":"

spit is a simple function that will write files.

"},{"location":"building-api/end-to-end-testing/","title":"End to end API testing","text":"

There are several tools for testing your API.

Tools Description OpenAPI (Swagger) Provides live documentation of an API and ability to run API calls curl Command line tool for talking to the web (client side) Insomnia.rest HTTP and GraphQL toolbelt for debugging APIs (client side) Postman Collaborative platform for API development

Open API should be built into any API you build as it provides living documentation of your API as you develop, as well as a way for developers to test queries against your API.

curl is the classic command line tool for testing anything on the web. Its an excellent tool for one off tests or for writing a batch of tests in a script.

Insomnia is a great tool to help you debug your API and generate code for API calls in over 30 different programming languages.

Postman is aimed more at the corporate developer or someone dealing with a large set of APIs. It requires more setup although provides more features.

"},{"location":"building-api/end-to-end-testing/curl/","title":"curl","text":""},{"location":"building-api/end-to-end-testing/curl/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":"

Pull requests are welcome

"},{"location":"building-api/end-to-end-testing/httpie/","title":"HTTPie","text":""},{"location":"building-api/end-to-end-testing/postman/","title":"Postman","text":""},{"location":"building-api/end-to-end-testing/postman/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":"

Pull requests are welcome

"},{"location":"building-api/end-to-end-testing/swagger/","title":"Swagger","text":""},{"location":"building-api/end-to-end-testing/swagger/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":"

Pull requests are welcome

"},{"location":"building-api/projects/game-scoreboard/","title":"Create a new project with compojure-api template","text":"

You can quickly create the basics of a server-side webapp with the compojure-api template for Leiningen.

lein new compojure-api game-scoreboard +clojure-test\n

This command creates a new Clojure project in a directory called game-scoreboard.

"},{"location":"building-api/projects/game-scoreboard/#update-clojure-version","title":"Update Clojure version","text":"

Edit the project.clj file and update the org.clojure/clojure dependency to 1.10.0

"},{"location":"building-api/projects/game-scoreboard/defining-scoreboard/","title":"Defining Scores and a Scoreboard","text":"

We use the Plumatic schema to define what a score looks like, as well as what the overall scoreboard looks like.

"},{"location":"building-api/projects/game-scoreboard/defining-scoreboard/#scores","title":"Scores","text":"

A score is an whole number (Integer) that represents the score achieved for a particular game

(schema/defschema Score\n  {:player-id   schema/Uuid\n   :score       schema/Int\n\n   (schema/optional-key\n     :gravitar) schema/Str})\n
"},{"location":"building-api/projects/game-scoreboard/defining-scoreboard/#leaderboard","title":"Leaderboard","text":"

The Leader board is a collection of scores for a game. The scoreboard is ordered by highest value by default.

"},{"location":"building-api/projects/game-scoreboard/defining-scoreboard/#player","title":"Player","text":""},{"location":"building-api/projects/game-scoreboard/defining-scoreboard/#player-accounts","title":"Player accounts","text":""},{"location":"building-api/projects/game-scoreboard/defining-scores/","title":"Defining Scores","text":""},{"location":"building-api/projects/game-scoreboard/defining-scores/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"building-api/projects/game-scoreboard-ui/","title":"Game Scoreboard UI","text":"

Create a Web user interface for the Scoreboard so we can test out the API and do something interesting with the results.

  • Create a project using figwheel-main
"},{"location":"building-api/projects/game-scoreboard-ui/create-project/","title":"Create new project using figwheel-main","text":"

Create a new project using figwheel-main, the newest version of figwheel.

Include the reagent library to make the project a single page app in the style of react.js.

lein new figwheel-main game-scoreboard-ui -- --reagent\n

Change into the 'game-scoreboard-ui' directory and run 'lein fig:build'

cd game-scoreboard-ui\nlein fig:build\n

Your default browser will open at localhost:9500

"},{"location":"building-api/reitit/","title":"Reitit routing library","text":"

Reitit routing for client and server-side routing

Supports use of ring specification and middleware

Provides content negotiation and data validation.

"},{"location":"building-api/reitit/#library-dependency","title":"Library Dependency","text":"

Include the bundled distribution containing all modules

Clojure CLILeiningen deps.edn
metosin/reitit {:mvn/version \"0.7.0-alpha5\"}\n
[metosin/reitit \"0.7.0-alpha5\"]\n
"},{"location":"building-api/reitit/#require-namespace","title":"Require namespace","text":"

Require the reitit.ring namespace to provide routing and ring specification support.

ProjectREPL
(:require \n [reitit.ring :as ring])\n
(require '[reitit.ring :as ring])\n
"},{"location":"building-api/reitit/#router-function","title":"Router function","text":"

Takes all requests and delegates them to handler functions

Routes are defined as a vector of vectors structure, with each nested vector containing a string key defining unique paths that match a specific request.

Each key has a configuration map to define a handler for a specific HTTP method (:get :ost etc.)

(def router\n  (ring/ring-handler\n   (ring/router\n    [[\"/\" {:get welcome}]]\n    [\"/status\" {:get status}])))\n

Passing system configuration argument

Component systems (donut, integrant) use a system configuration (hash-map) which is passed to the router component during start up.

Define the router as a function to recieve the system configuration as an argument and make it available in part or full to the handler functions.

(defn app\n  [system-config]\n  (ring/ring-handler\n   (ring/router\n    [[\"/api\"\n      [\"/v1\"\n       (scoreboard/routes system-config)]]]\n\n    ;; - middleware, coersion & content negotiation\n    router-configuration)))\n
"},{"location":"building-api/reitit/#handler-functions","title":"Handler functions","text":"

Handler functions are Clojure functions that take a request map.

(defn welcome [_]\n  {:status 200 :body \"Awesome Reitit Ring\"})\n

constantly function is commonly used for handlers that do not use the request data initially. constantly returns an anonymous function that takes the ring request hash-map.

(def status\n  (constantly \n    (ring.util.response/response \n     {:application \"practicalli awesome-api Service\" :status \"Alive\"})))\n

Use _ argument name when request not used

Handlers might not use the data in a request to return a response. By convention the _ character is used for the argument name when the request data is not used.

System Status handler

An example status report handler namespace

(ns practicalli.awesome-api.api.system-admin\n  \"Gameboard API system administration handlers\"\n  (:require [ring.util.response :refer [response]]))\n\n(def status\n  \"Service status report for external monitoring services, e.g. Statuscake\n  Return:\n  - `constantly` returns an anonymous function that returns a ring response hash-map\"\n  (constantly (response {:application \"practicalli awesome-api Service\" :status \"Alive\"})))\n

"},{"location":"clojure-databases/","title":"Clojure Databases","text":"Database Description Juxt Crux Bi-temporal schema-less high performance CQRS style database Onyx Cognitect Datomic (commercial product)"},{"location":"clojure-databases/crux/","title":"Crux - bi-temporal schema-less document database","text":"

Crux is a general purpose database with graph-oriented bi-temporal indexes. Datalog, SQL & EQL queries are supported along with Java, HTTP & Clojure APIs. The Datalog query interface that can be used to express complex joins and recursive graph traversals.

"},{"location":"clojure-databases/crux/#getting-started","title":"Getting Started","text":"

Follow the Crux Earth Assignment Tutorial, in either the self-contained Next-Journal environment or as your own Clojure project.

{% tabs clojure=\"Clojure CLI tools\", lein=\"Leiningen\" %}

{% content \"clojure\" %} Using the Clojure CLI tools and practicalli/clojure-deps-edn configuration, create a new project:

clojure -X:project/new :template app :name practicalli/crux-demo\n

{% content \"lein\" %} Using the Leiningen build tool, create a new project:

lein new app practicalli/crux-demo\n

{% endtabs %}

Install Crux as a library in a Clojure project or use the pre-built docker image.

Note: to have more than one set of tabs in a page, simply create unique id's for the tabs, e.g. practicalli2

Experiment with the Crux-labs workshop project, which contains examples of using Crux.

"},{"location":"clojure-databases/crux/#resources","title":"Resources","text":"
  • Library dependency (clojars)
  • Reference Documentation
  • Community discussions (Zulip)
  • GitHub discussions

{% youtube %} https://www.youtube.com/watch?v=JkZfQZGLPTA

"},{"location":"clojure-databases/crux/#unbundled-architectural-overview","title":"Unbundled architectural overview","text":"

Crux follows an unbundled architectural, decoupled components communicating via an immutable log and document store. crux-rocksdb is the high performance default data store, with a range of storage options available for embedded usage and cloud scaling.

Crux embraces the transaction log as the central point of coordination when running as a distributed system. Use of a separate document store enables simple eviction of active and historical data to assist with technical compliance for information privacy regulations.

This design makes it feasible and desirable to embed Crux nodes directly within your application processes, which reduces deployment complexity and eliminates round-trip overheads when running complex application queries.

"},{"location":"full-app/","title":"Building a full database backed app","text":""},{"location":"introduction/contributing/","title":"Contributing to Practicalli","text":"

Practicalli books are written in markdown and use MkDocs to generate the published website via a GitHub workflow. MkDocs can also run a local server using the make docs target from the Makefile

By submitting content ideas and corrections you are agreeing they can be used in this book under the Creative Commons Attribution ShareAlike 4.0 International license. Attribution will be detailed via GitHub contributors.

All content and interaction with any persons or systems must be done so with respect and within the Practicalli Code of Conduct.

"},{"location":"introduction/contributing/#book-status","title":"Book status","text":""},{"location":"introduction/contributing/#submit-and-issue-or-idea","title":"Submit and issue or idea","text":"

If something doesnt seem quite right or something is missing from the book, please raise an issue via the GitHub repository explaining in as much detail as you can.

Raising an issue before creating a pull request will save you and the maintainer time.

"},{"location":"introduction/contributing/#considering-a-pull-request","title":"Considering a Pull request?","text":"

Before investing any time in a pull request, please raise an issue explaining the situation. This can save you and the maintainer time and avoid rejected pull requests.

Please keep pull requests small and focused, as they are much quicker to review and easier to accept. Ideally PR's should be for a specific page or at most a section.

A PR with a list of changes across different sections will not be merged, it will be reviewed eventually though.

"},{"location":"introduction/contributing/#thank-you-to-everyone-that-has-contributed","title":"Thank you to everyone that has contributed","text":"

A huge thank you to Rich Hickey and the team at Cognitect for creating and continually guiding the Clojure language. Special thank you to Alex Miller who has provided excellent advice on working with Clojure and the CLI tooling.

The Clojure community has been highly supportive of everyone using Clojure and I'd like to thank everyone for the feedback and contributions. I would also like to thank everyone that has joined in with the London Clojurins community, ClojureBridgeLondon, Clojurians Slack community, Clojurians Zulip community and Clojureverse community.

Thank you to everyone who sponsors the Practicalli websites and videos and for the Clojurists Together sponsorship, it helps me continue the work at a much faster pace.

Special thanks to Bruce Durling for getting me into Cloure in the first place.

"},{"location":"introduction/overview/","title":"Overview of Clojure Web Services","text":"

A Web service receives data, does something with that data and returns data as a result. This is the essence of how a function works in Clojure. So its really simple to design web apps with Clojure.

Clojure data is managed via immutable data structures. The majority of the Clojure code will be stateless or managing state with immutable data structures, therefore code for services will be less complex and less prone to conflicts.

Relevant changes to data is persisted to a data store, e.g. PostgreSQL, Datomic or XTDB

So reliable services are very easy to build and simple to scale via parallelism.

"},{"location":"introduction/overview/#libraries-over-frameworks","title":"Libraries over frameworks","text":"

Clojure takes a modular approach to building services, assembling commonly used libraries from the community.

Java Interoperability is simple in Clojure, so its trivial to use Java libraries or any libraries that run on the JVM (Scala, Jython, etc.).

Web services in Clojure are typically built from a collection of highly focused libraries. Each library has a specific focus and enables a modular approach, as you can swap components & libraries easily should there be value in a different approach.

Common libraries for web app development include:

  • Ring - a web application library
  • Compojure - an simple way to define routes for your ring webapp
  • Hiccup or Selmer- to generate HTML from Clojure data structures
  • Pedestal
  • jdbc.next - a modern low-level Clojure wrapper for JDBC access to databases
  • clojure.java.jdbc - simple low level SQL library
  • Korma, YesQL, hugSql - database abstraction layers
  • Prismatic Schema - database schema mapping
  • Migratus - database migrations (and all the things)

Large frameworks constrain the design of a service, forcing development to live inside the constraints of that framework as it can be difficult to break out of the design the framework imposes.

"},{"location":"introduction/overview/#project-templates","title":"Project templates","text":"

Projects can be created from templates to avoid starting from scratch each time.

deps-newclj-new

deps-new creates Clojure CLI projects from templates and is very simple to create your own templates

clojure -T:project/create :template app :name practicalli/gameboard\n

clj-new supports a very wide range of templates although has a more involved design when it comes to creating your own templates. Maintained templates used by clj-new should support both Clojure CLI and Leiningen

clojure -T:project/new :template luminus :name practicalli/gameboard\n

Convert Leiningen project to Clojure CLI

Add a deps.edn file that contains a {} hash-map with a :deps key that is associated with a hash-map of library dependencies, the same dependencies from the project.clj configuration file updated to the Clojure CLI format, e.g. {org.clojure/clojure {:mvn/version \"1.11.3\"}}

There are many great templates to try that provide insight into building webapps in Clojure.

  • compojure - a common web application approach with ring and compojure
  • compojure-api - quickly build API's with ring, compojure and openapi (swagger) for self-documentation
  • luminus - a flexible template to create server-side and full stack web applications
  • pedestal-service - an opinionated, extensible & scalable framework
  • duct - data-oriented production-grade server-side web applications
  • JUXT Edge - a curated base project to build your own applications and services

You can find a range of project templates by searching for lein-template on Clojars.org. There is also a guide to writing templates on Leiningen.org

"},{"location":"introduction/repl-workflow/","title":"REPL Driven Development","text":"

Always be REPL'ing

Coding without a REPL feels limiting. The REPL provides fast feedback from code as its crafted, testing assumptions and design choices every step of the journey to a solution - John Stevenson, Practical.li

Clojure is a powerful, fun and highly productive language for developing applications and services. The clear language design is supported by a powerful development environment known as the REPL (read, evaluate, print, loop). The REPL gives you instant feedback on what your code does and enables you to test either a single expression or run the whole application (including tests).

REPL driven development is the foundation of working with Clojure effectively

An effective Clojure workflow begins by running a REPL process. Clojure expressions are written and evaluated immediately to provide instant feedback. The REPL feedback helps test the assumptions that are driving the design choices.

  • Read - code is read by the Clojure reader, passing any macros to the macro reader which converts those macros into Clojure code.
  • Evaluate - code is compiled into the host language (e.g. Java bytecode) and executed
  • Print - results of the code are displayed, either in the REPL or as part of the application.
  • Loop - the REPL is a continuous process that evaluates code, either a single expression or the whole application.

Design decisions and valuable data from REPL experiments can be codified as specifications and unit tests

Practicalli REPL Reloaded Workflow

The principles of REPL driven development are implemented in practice using the Practicalli REPL Reloaded Workflow and supporting tooling. This workflow uses Portal to inspect all evaluation results and log events, hot-load libraries into the running REPL process and reloads namespaces to support major refactor changes.

"},{"location":"introduction/repl-workflow/#evaluating-source-code","title":"Evaluating source code","text":"

A REPL connected editor is the primary tool for evaluating Clojure code from source code files, displaying the results inline.

Source code is automatically evaluated in its respective namespace, removing the need to change namespaces in the REPL with (in-ns) or use fully qualified names to call functions.

Evaluate Clojure in a Terminal UI REPL

Entering expressions at the REPL prompt evaluates the expression immediately, returning the result directly underneath

"},{"location":"introduction/repl-workflow/#rich-comment-blocks-living-documentation","title":"Rich Comment blocks - living documentation","text":"

The (comment ,,,) function wraps code that is only run directly by the developer using a Clojure aware editor.

Expressions in rich comment blocks can represent how to use the functions that make up the namespace API. For example, starting/restarting the system, updating the database, etc. Expressions provide examples of calling functions with typical arguments and make a project more accessible and easier to work with.

Clojure Rich Comment to manage a service

(ns practicalli.gameboard.service)\n\n(defn app-server-start [port] ,,,)\n(defn app-server-start [] ,,,)\n(defn app-server-restart [] ,,,)\n\n(defn -main\n  \"Start the service using system components\"\n  [& options] ,,,)\n\n(comment\n  (-main)\n  (app-server-start 8888)\n  (app-server-stop)\n  (app-server-restart 8888)\n\n  (System/getenv \"PORT\")\n  (def environment (System/getenv))\n  (def system-properties (System/getProperties))\n  ) ; End of rich comment block\n

Rich comment blocks are very useful for rapidly iterating over different design decisions by including the same function but with different implementations. Hide clj-kondo linter warnings for redefined vars (def, defn) when using this approach.

;; Rich comment block with redefined vars ignored\n#_{:clj-kondo/ignore [:redefined-var]}\n(comment\n  (defn value-added-tax []\n    ;; algorithm design - first idea)\n\n  (defn value-added-tax []\n    ;; algorithm design - second idea)\n\n  ) ;; End of rich comment block\n

The \"Rich\" in the name is an honourary mention to Rich Hickey, the author and benevolent dictator of Clojure design.

"},{"location":"introduction/repl-workflow/#design-journal","title":"Design Journal","text":"

A journal of design decisions makes the code easier to understand and maintain. Code examples of design decisions and alternative design discussions are captured, reducing the time spent revisiting those discussions.

Journals simplify the developer on-boarding processes as the journey through design decisions are already documented.

A Design Journal is usually created in a separate namespace, although it may start as a rich comment at the bottom of a namespace.

A journal should cover the following aspects

  • Relevant expressions use to test assumptions about design options.
  • Examples of design choices not taken and discussions why (saves repeating the same design discussions)
  • Expressions that can be evaluated to explain how a function or parts of a function work

The design journal can be used to create meaningful documentation for the project very easily and should prevent time spent on repeating the same conversations.

Example design journal

Design journal for TicTacToe game using Reagent, ClojureScript and Scalable Vector Graphics

"},{"location":"introduction/repl-workflow/#viewing-data-structures","title":"Viewing data structures","text":"

Pretty print shows the structure of results from function calls in a human-friendly form, making it easier for a developer to parse and more likely to notice incorrect results.

Tools to view and navigate code

  • Cider inspector is an effective way to navigate nested data and page through large data sets.
  • Portal Inspector to visualise many kinds of data in many different forms.

"},{"location":"introduction/repl-workflow/#code-style-and-idiomatic-clojure","title":"Code Style and idiomatic Clojure","text":"

Clojure aware editors should automatically apply formatting that follows the Clojure Style guide.

Live linting with clj-kondo suggests common idioms and highlights a wide range of syntax errors as code is written, minimizing bugs and therefore speeding up the development process.

Clojure LSP is build on top of clj-kondo

Clojure LSP uses clj-kondo static analysis to provide a standard set of development tools (format, refactor, auto-complete, syntax highlighting, syntax & idiom warnings, code navigation, etc).

Clojure LSP can be used with any Clojure aware editor that provides an LSP client, e.g. Spacemacs, Doom Emacs, Neovim, VSCode.

Clojure Style Guide

The Clojure Style guide provides examples of common formatting approaches, although the development team should decide which of these to adopt. Emacs clojure-mode will automatically format code and so will Clojure LSP (via cljfmt). These tools are configurable and should be tailored to the teams standard.

"},{"location":"introduction/repl-workflow/#data-and-function-specifications","title":"Data and Function specifications","text":"

Clojure spec is used to define a contract on incoming and outgoing data, to ensure it is of the correct form.

As data structures are identified in REPL experiments, create data specification to validate the keys and value types of that data.

;; ---------------------------------------------------\n;; Address specifications\n(spec/def ::house-number string?)\n(spec/def ::street string?)\n(spec/def ::postal-code string?)\n(spec/def ::city string?)\n(spec/def ::country string?)\n(spec/def ::additional string?)\n\n(spec/def ::address   ; Composite data specification\n  (spec/keys\n   :req-un [::street ::postal-code ::city ::country]\n   :opt-un [::house-number ::additional]))\n;; ---------------------------------------------------\n

As the public API is designed, specifications for each functions arguments are added to validate the correct data is used when calling those functions.

Generative testing provides a far greater scope of test values used incorporated into unit tests. Data uses clojure.spec to randomly generate data for testing on each test run.

"},{"location":"introduction/repl-workflow/#test-driven-development-and-repl-driven-development","title":"Test Driven Development and REPL Driven Development","text":"

Test Driven Development (TDD) and REPL Driven Development (RDD) complement each other as they both encourage incremental changes and continuous feedback.

Test Driven Development fits well with Hammock Time, as good design comes from deep thought

  • RDD enables rapid design experiments so different approaches can easily and quickly be evaluated .
  • TDD focuses the results of the REPL experiments into design decisions, codified as unit tests. These tests guide the correctness of specific implementations and provide critical feedback when changes break that design.

Unit tests should support the public API of each namespace in a project to help prevent regressions in the code. Its far more efficient in terms of thinking time to define unit tests as the design starts to stabilize than as an after thought.

clojure.test library is part of the Clojure standard library that provides a simple way to start writing unit tests.

Clojure spec can also be used for generative testing, providing far greater scope in values used when running unit tests. Specifications can be defined for values and functions.

Clojure has a number of test runners available. Kaocha is a test runner that will run unit tests and function specification checks.

Automate local test runner

Use kaocha test runner in watch mode to run tests and specification check automatically (when changes are saved)

clojure -X:test/watch\n

"},{"location":"introduction/repl-workflow/#continuous-integration-and-deployment","title":"Continuous Integration and Deployment","text":"

Add a continuous integration service to run tests and builds code on every shared commit. Spin up testable review deployments when commits pushed to a pull request branch, before pushing commits to the main deployment branch, creating an effective pipeline to gain further feedback.

  • CircleCI provides a simple to use service that supports Clojure projects.
  • GitHub Workflows and GitHub actions marketplace to quickly build a tailored continuous integration service, e.g. Setup Clojure GitHub Action.
  • GitLab CI

"},{"location":"introduction/repl-workflow/#live-coding-with-data-stuart-halloway","title":"Live Coding with Data - Stuart Halloway","text":"

There are few novel features of programming languages, but each combination has different properties. The combination of dynamic, hosted, functional and extended Lisp in Clojure gives developers the tools for making effective programs. The ways in which Clojure's unique combination of features can yield a highly effective development process.

Over more than a decade we have developed an effective approach to writing code in Clojure whose power comes from composing many of its key features. As different as Clojure programs are from e.g. Java programs, so to can and should be the development experience. You are not in Kansas anymore!

This talk presents a demonstration of the leverage you can get when writing programs in Clojure, with examples, based on my experiences as a core developer of Clojure and Datomic.

"},{"location":"introduction/requirements/","title":"Requirements","text":"

Practicalli provides an install guide for Clojure and a wide selection of Clojure aware editors

Recommended development tools for this guide are:

  • Java OpenJDK version 17
  • Clojure CLI and Practicalli Clojure CLI Config
  • A Clojure aware editor

Code examples can be used with any Clojure build tool, although this guide focuses on using Clojure CLI tools. Some examples use Leiningen and will be updated to Clojure CLI, although the Clojure code will be the same.

"},{"location":"introduction/requirements/#additional-development-tools","title":"Additional Development tools","text":"

To complete all the steps in this guide, especially around deployment tasks, additional development tools and services are required.

Development Tools Version Test (command line) Git client latest git --version Docker Desktop latest docker --version Postgres database latest

GitHub and GitHub actions will be predominantly used in this guide, although more use of CircleCI and GitLab will also be introduced. CircleCI is a developer focused service for Continuous Integration, developed with Clojure, providing obs that package up common workflows such as deploying to specific Cloud services.

Continuous Integration Services GitHub Actions GitLab CI CircleCI Heroku deployment to be deprecated

Heroku has been used to simplify deployment directly from source code using existing build packs. Heroku now requires a commercial license for deployment so this content is to be deprecated.

"},{"location":"introduction/requirements/#persistence-alternatives","title":"Persistence Alternatives","text":"

Practicalli is considering other persistent storage approaches for this guide and any contributions in this regard is much appreciated

  • Crux - an open source document database with bitemporal graph queries
  • Datomic - a commercial transactional database with a flexible data model, elastic scaling, and rich queries.
  • Amazon Aurora - MySQL and PostgreSQL compatible cloud native relational database
  • Amazon DynamoDB with Clojure Faraday library - for persisting JSON like data structures
"},{"location":"introduction/requirements/#leiningen-approach-to-be-archived","title":"Leiningen approach (to be archived)","text":"

Install Leiningen for the Leiningen Todo App project and test the Leiningen install by running the command lein version in a terminal application.

"},{"location":"introduction/writing-tips/","title":"Writing tips for MkDocs","text":"

Making the docs more engaging using the mkdocs-material theme reference guide

Configuring Colors

Material for MkDocs - Changing the colors lists the primary and accent colors available.

HSL Color Picker for codes to modify the theme style, overriding colors in docs/assets/stylesheets/extra.css

"},{"location":"introduction/writing-tips/#hypertext-links","title":"Hypertext links","text":"

Links open in the same browser window/tab by default.

Add {target=_blank} to the end of a link to configure opening in a new tab

[link text](url){target=_blank}\n
"},{"location":"introduction/writing-tips/#buttons","title":"Buttons","text":"

Convert any link into a button by adding {.md-button} class names to end of the markdown for a link, which uses .md-button-primary by default. Include target=_blank for buttons with links to external sites.

[link text](http://practical.li/blog){.md-button target=_blank}\n

Or specify a different class

[link text](http://practical.li/blog){.md-button .md-button-primary}\n

Add an icon to the button

Practicalli Issues Practicalli Blog

[:fontawesome-brands-github: Practicalli Issues](http://practical.li/blog){ .md-button .md-button-primary }\n[:octicons-heart-fill-24: Practicalli Blog](http://practical.li/blog){ .md-button .md-button-primary }\n

Search all supported icons

"},{"location":"introduction/writing-tips/#youtube-video","title":"YouTube video","text":"

Use an iframe element to include a YouTube video, wrapping in a paragraph tag with center alignment to place the video in a centered horizontal position

<p style=\"text-align:center\">\n<iframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/embed/rQ802kSaip4\" title=\"YouTube video player\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe>\n</p>\n

mkdocs material does not have direct support for adding a YouTube video via markdown.

"},{"location":"introduction/writing-tips/#admonitions","title":"Admonitions","text":"

Supported admonition types

Note

Use !!! followed by NOTE

Adding a title

Use !!! followed by NOTE and a \"title in double quotes\"

Shh, no title bar just the text... Use !!! followed by NOTE and a \"\" empty double quotes

Abstract

Use !!! followed by ABSTRACT

Info

Use !!! followed by INFO

Tip

Use !!! followed by TIP

Success

Use !!! followed by SUCCESS

Question

Use !!! followed by QUESTION

Warning

Use !!! followed by WARNING

Failure

Use !!! followed by FAILURE

Danger

Use !!! followed by DANGER

Bug

Use !!! followed by BUG

Example

Use !!! followed by EXAMPLE

Quote

Use !!! followed by QUOTE

"},{"location":"introduction/writing-tips/#collapsing-admonitions","title":"Collapsing admonitions","text":"Note

Collapse those admonitions using ??? instead of !!!

Replace with a title

Use ??? followed by NOTE and a \"title in double quotes\"

Expanded by default

Use ???+, note the + character, followed by NOTE and a \"title in double quotes\"

"},{"location":"introduction/writing-tips/#inline-blocks","title":"Inline blocks","text":"

Inline blocks of text to make a very specific callout within text

Info

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.

Adding something to then end of text is probably my favourite

Info

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.

"},{"location":"introduction/writing-tips/#code-blocks","title":"Code blocks","text":"

Code blocks include a copy icon automatically

Syntax highlighting in code blocks

(defn my-function  ; Write a simple function\n  \"With a lovely doc-string\"\n  [arguments]\n  (map inc [1 2 3]))\n

Give the code block a title using title=\"\" after the backtics and language name

src/practicalli/gameboard.clj
(defn my-function\n  \"With a lovely doc-string\"\n  [arguments]\n  (map inc [1 2 3]))\n

We all like line numbers, especially when you can set the starting line

src/practicalli/gameboard.clj
(defn my-function\n  \"With a lovely doc-string\"\n  [arguments]\n  (map inc [1 2 3]))\n

Add linenums=42 to start line numbers from 42 onward

clojure linenums=\"42\" title=\"src/practicalli/gameboard.clj\"\n
"},{"location":"introduction/writing-tips/#annotations","title":"Annotations","text":"

Annotations in a code block help to highlight important aspects. Use the comment character for the language followed by a space and a number in brackets

For example, in a shell code block, use # (1) where 1 is the number of the annotation

Use a number after the code block to add the text for the annotation, e.g. 1.. Ensure there is a space between the code block and the annotation text.

ls -la $HOME/Downloads  # (1)\n
  1. I'm a code annotation! I can contain code, formatted text, images, ... basically anything that can be written in Markdown.

Code blocks with annotation, add ! after the annotation number to suppress the # character

(defn helper-function\n  \"Doc-string with description of function purpose\" ; (1)!\n  [data]\n  (merge {:fish 1} data)\n  )\n
  1. Always include a doc-string in every function to describe the purpose of that function, identifying why it was added and what its value is.

GitHub action example with multiple annotations

name: ci # (1)!\non:\n  push:\n    branches:\n      - master # (2)!\n      - main\npermissions:\n  contents: write\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-python@v4\n        with:\n          python-version: 3.x\n      - run: pip install mkdocs-material # (3)!\n      - run: mkdocs gh-deploy --force\n
  1. You can change the name to your liking.

  2. At some point, GitHub renamed master to main. If your default branch is named master, you can safely remove main, vice versa.

  3. This is the place to install further [MkDocs plugins] or Markdown extensions with pip to be used during the build:

    pip install \\\n  mkdocs-material \\\n  mkdocs-awesome-pages-plugin \\\n  ...\n
"},{"location":"introduction/writing-tips/#highlight-lines-in-code-blocks","title":"Highlight lines in code blocks","text":"

Add highlight line meta data to a code block after the opening backticks and code block language.

hl_lines=\"2\" highlights line 2 in the codeblock

(defn my-function\n  \"With a lovely doc-string\"\n  [arguments]\n  (map\n   inc\n   [1 2 3]))\n
"},{"location":"introduction/writing-tips/#embed-external-files","title":"Embed external files","text":"

--8<-- in a code block inserts code from a source code file or other text file

Specify a local file from the root of the book project (the directory containing mkdocs.yml)

Scheduled Version Check GitHub Workflow from source code file scheduled version check
---\n# ------------------------------------------\n# Scheduled check of versions\n# - use as non-urgent report on versions\n# - Uses POSIX Cron syntax\n#   - Minute [0,59]\n#   - Hour [0,23]\n#   - Day of the month [1,31]\n#   - Month of the year [1,12]\n#   - Day of the week ([0,6] with 0=Sunday)\n#\n# Using liquidz/anta to check:\n# - GitHub workflows\n# - deps.edn\n# ------------------------------------------\n\nname: \"Scheduled Version Check\"\non:\n  schedule:\n    # - cron: \"0 4 * * *\" # at 04:04:04 ever day\n    # - cron: \"0 4 * * 5\" # at 04:04:04 ever Friday\n    - cron: \"0 4 1 * *\" # at 04:04:04 on first day of month\n  workflow_dispatch: # Run manually via GitHub Actions Workflow page\n\njobs:\n  scheduled-version-check:\n    name: \"Scheduled Version Check\"\n    runs-on: ubuntu-latest\n    steps:\n      - run: echo \"\ud83d\ude80 Job automatically triggered by ${{ github.event_name }}\"\n      - run: echo \"\ud83d\udc27 Job running on ${{ runner.os }} server\"\n      - run: echo \"\ud83d\udc19 Using ${{ github.ref }} branch from ${{ github.repository }} repository\"\n\n      - name: \"Checkout code\"\n        uses: actions/checkout@v3\n      - run: echo \"\ud83d\udc19 ${{ github.repository }} repository was cloned to the runner.\"\n\n      - name: \"Antq Check versions\"\n        uses: liquidz/antq-action@main\n        with:\n          excludes: \"\"\n          skips: \"boot clojure-cli pom shadow-cljs leiningen\"\n\n      # Summary\n      - run: echo \"\ud83c\udfa8 library versions checked with liquidz/antq\"\n      - run: echo \"\ud83c\udf4f Job status is ${{ job.status }}.\"\n
Practicalli Project Templates Emacs project configuration - .dir-locals.el
((clojure-mode . ((cider-preferred-build-tool . clojure-cli)\n                  (cider-clojure-cli-aliases . \":test/env:dev/reloaded\"))))\n

Code example reuse

Use an embedded local or external file (URL) when the same content is required in more than one place in the book.

An effective way of sharing code and configuration mutliple times in a book or across multiple books.

"},{"location":"introduction/writing-tips/#content-tabs","title":"Content tabs","text":"

Create in page tabs that can also be

Setting up a project

Clojure CLILeiningen
clojure -T:project/new :template app :name practicalli/gameboard\n
lein new app practicalli/gameboard\n

Or nest the content tabs in an admonition

Run a terminal REPL

Clojure CLILeiningen
clojure -T:repl/rebel\n
lein repl\n
"},{"location":"introduction/writing-tips/#diagrams","title":"Diagrams","text":"

Neat flow diagrams

Diagrams - Material for MkDocs

graph LR\n  A[Start] --> B{Error?};\n  B -->|Yes| C[Hmm...];\n  C --> D[Debug];\n  D --> B;\n  B ---->|No| E[Yay!];

UML Sequence Diagrams

sequenceDiagram\n  Alice->>John: Hello John, how are you?\n  loop Healthcheck\n      John->>John: Fight against hypochondria\n  end\n  Note right of John: Rational thoughts!\n  John-->>Alice: Great!\n  John->>Bob: How about you?\n  Bob-->>John: Jolly good!

state transition diagrams

stateDiagram-v2\n  state fork_state <<fork>>\n    [*] --> fork_state\n    fork_state --> State2\n    fork_state --> State3\n\n    state join_state <<join>>\n    State2 --> join_state\n    State3 --> join_state\n    join_state --> State4\n    State4 --> [*]

Class diagrams - not needed for Clojure

Entity relationship diagrams are handy though

erDiagram\n  CUSTOMER ||--o{ ORDER : places\n  ORDER ||--|{ LINE-ITEM : contains\n  LINE-ITEM {\n    customer-name string\n    unit-price int\n  }\n  CUSTOMER }|..|{ DELIVERY-ADDRESS : uses
"},{"location":"introduction/writing-tips/#keyboard-keys","title":"Keyboard keys","text":"

Represent key bindings with Keyboard keys. Each number and alphabet character has their own key.

  • 1 ++1++ for numbers
  • l ++\"l\"++ for lowercase character
  • U ++u++ for uppercase character or ++\"U\"++ for consistency

Punctionation keys use their name

  • Space ++spc++
  • , ++comma++
  • Left ++arrow-left++

For key sequences, place a space between each keyboard character

  • Space g s ++spc++ ++\"g\"++ ++\"s\"++

For key combinations, use join they key identifies with a +

  • Meta+X ++meta+x++
  • Ctrl+Alt+Del ++ctrl+alt+del++

MkDocs keyboard keys reference

"},{"location":"introduction/writing-tips/#images","title":"Images","text":"

Markdown images can be appended with material tags to set the size of the image, whether to appear on light or dark theme and support lazy image loading in browsers

SizeLazy LoadingAlignTheme SpecificAll Image Attributes

{style=\"height:150px;width:150px\"} specifies the image size

![Kitty Logo](https://raw.githubusercontent.com/practicalli/graphic-design/live/icons/kitty-light.png#only-dark){style=\"height:150px;width:150px\"}\n

{loading=lazy} specifies an image should lazily load in the browser

![Kitty Logo](https://raw.githubusercontent.com/practicalli/graphic-design/live/icons/kitty-light.png){loading=lazy}\n

{aligh=left} or {aligh=right} specifies the page alignment of an image.

![Kitty Logo](https://raw.githubusercontent.com/practicalli/graphic-design/live/icons/kitty-light.png#only-dark){align=right}\n![Kitty Logo](https://raw.githubusercontent.com/practicalli/graphic-design/live/icons/kitty-dark.png#only-light){align=right}\n

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.

![Kitty Logo](image/kitty-light.png#only-dark) or ![Kitty Logo](image/kitty-light.png#only-light) specifies the theme the image should be shown, allowing different versions of images to be shown based on the theme.

![Kitty Logo](https://raw.githubusercontent.com/practicalli/graphic-design/live/icons/kitty-light.png#only-dark){style=\"height:150px;width:150px\"}\n![Kitty Logo](https://raw.githubusercontent.com/practicalli/graphic-design/live/icons/kitty-dark.png#only-light){style=\"height:150px;width:150px\"}\n
Use the theme toggle in the top nav bar to see the icon change between light and dark.

Requires the color pallet toggle

Alight right, lazy load and set image to 150x150

![Kitty Logo](https://raw.githubusercontent.com/practicalli/graphic-design/live/icons/kitty-light.png#only-dark){align=right loading=lazy style=\"height:64px;width:64px\"}\n![Kitty Logo](https://raw.githubusercontent.com/practicalli/graphic-design/live/icons/kitty-dark.png#only-light){align=right loading=lazy style=\"height:64px;width:64px\"}\n

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.

"},{"location":"introduction/writing-tips/#lists","title":"Lists","text":"

Task lists

  • Lorem ipsum dolor sit amet, consectetur adipiscing elit
  • Vestibulum convallis sit amet nisi a tincidunt
    • In hac habitasse platea dictumst
    • In scelerisque nibh non dolor mollis congue sed et metus
    • Praesent sed risus massa
  • Aenean pretium efficitur erat, donec pharetra, ligula non scelerisque

Task List example

- [x] Lorem ipsum dolor sit amet, consectetur adipiscing elit\n- [ ] Vestibulum convallis sit amet nisi a tincidunt\n    * [x] In hac habitasse platea dictumst\n    * [x] In scelerisque nibh non dolor mollis congue sed et metus\n    * [ ] Praesent sed risus massa\n- [ ] Aenean pretium efficitur erat, donec pharetra, ligula non scelerisque\n
"},{"location":"introduction/writing-tips/#tooltips","title":"Tooltips","text":"

The humble tool tip

Hover me

with references

Hover me

Icon tool tip with a title

"},{"location":"introduction/writing-tips/#abreviations","title":"Abreviations","text":"

The HTML specification is maintained by the W3C.

[HTML]: Hyper Text Markup Language [W3C]: World Wide Web Consortium

"},{"location":"introduction/writing-tips/#magic-links","title":"Magic links","text":"

MagicLink can auto-link HTML, FTP, and email links. It can auto-convert repository links (GitHub, GitLab, and Bitbucket) and display them in a more concise, shorthand format.

Email Practicalli

Practicalli Neovim

"},{"location":"libraries/reitit/","title":"Reitit - fast data driven routing for Clojure and ClojureScript","text":""},{"location":"libraries/reitit/constructing-routes/","title":"Reitit: Constructing routes","text":"

Create a simple Clojure project

clojure -T:project/new :template app :name practicalli/reitit-routing\n

Require reitit

(require '[reitit.core :as reitit])\n

Define several simple routes using reitit router

Routes are defined as a collection (vector) of vectors, with each vector defining the path of the route and an optional name.

reitit.core/router creates a router from the collection of vectors and an optional hash-map of routes configuration options (e.g middleware)

(def router\n    (reitit/router\n     [[\"/api/ping\" ::ping]\n      [\"/api/game-scoreboard/:score-id\" ::game-score]]))\n

names are used to provide a unique way of referring to a route throughout the whole project, as they are a namespace qualified keyword

Selects implementation based on route details. The following options are available:

Key description :path Base-path for routes :routes Initial resolved routes (default []) :data Initial route data (default {}) :spec clojure.spec definition for a route data, see reitit.spec on how to use this :syntax Path-parameter syntax as keyword or set of keywords (default #{:bracket :colon}) :expand Function of arg opts => data to expand route arg to route data (default reitit.core/expand) :coerce Function of route opts => route to coerce resolved route, can throw or return nil :compile Function of route opts => result to compile a route handler :validate Function of routes opts => () to validate route (data) via side-effects :conflicts Function of {route #{route}} => () to handle conflicting routes :exception Function of Exception => Exception to handle creation time exceptions (default reitit.exception/exception) :router Function of routes opts => router to override the actual router implementation

Routes can be found by either path or name

"},{"location":"micro-framework/","title":"Micro-frameworks","text":"

Introducing common micro-frameworks (curated libraries and configuration) as a basis for your own projects.

  • Luminus
  • Pedestal
  • JUXT Edge
"},{"location":"micro-framework/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":"

Pull requests are welcome

"},{"location":"micro-framework/edge/","title":"JUXT Edge","text":"

JUXT Edge is a foundation for Clojure projects, using leading edge libraries and with upgrade path as the Edge project evolves.

Projects are created with Clojure CLI and tools.deps

"},{"location":"micro-framework/edge/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":"

Pull requests are welcome

"},{"location":"micro-framework/luminus/","title":"Luminus","text":"

Luminus is a clojure micro-framework based on a set of lightweight libraries. It ams to provide a robust and configurable template to generate web services and applications.

Luminus also supports ClojureScript for browser based and Mobile UI's.

"},{"location":"micro-framework/luminus/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":"

Pull requests are welcome

"},{"location":"micro-framework/luminus/#hintluminus-uses-leiningen","title":"Hint::Luminus uses Leiningen","text":"

TODO: review how easy it would be to convert this to a tools.deps project.

"},{"location":"micro-framework/pedestal/","title":"Pedestal","text":""},{"location":"micro-framework/pedestal/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":"

Pull requests are welcome

"},{"location":"micro-services/","title":"Clojure Microservices","text":"

Clojure Microservices are an architectural design approach, with advantages and constraints.

Domain Driven Design (DDD) princilples are relevant to microservice design.

Effective testing is essential to avoid regressions and help ensure a consistent API across all services. Changes should be additional rather than breaking where ever possible.

Aspects of a Microservice

A microservice is a natural concequence of applying the single responsibility princilple at the architecture level

A microservice typicaly has its own data store, persistence layer or connection to an event stream

A microservice should to be loosley coupled to be effecitve to use and maintain

Avoid breaking API changes

The internal implementation of any microservice should be readily changed without breaking an established (shared) API.

Once a microservice API is published to the system, only additional changes should be made to avoid breaking other services that depend on the microservice.

Where breaking changes are the only remaining option, extensive communication is essential across the organisation.

Page work in progress"},{"location":"micro-services/#anatomy-of-a-microservice","title":"Anatomy of a microservice","text":"
  • Resource
  • service layer
  • domain model
  • Repositories
  • Persistent layer | Gateway
"},{"location":"micro-services/#gateway","title":"Gateway","text":"

Abstract away the communication layer between micro-services

"},{"location":"micro-services/#ddd-bounded-context","title":"DDD Bounded context","text":"

grouping your domain model in to self contained logical chunks

  • small islands of concepts with relationships between them
  • connection over rest or lightweight event bus
  • can be deployed quickly (hours not days/weeks)
  • understand the concequence of changes to a microservice
"},{"location":"micro-services/#resilience","title":"Resilience","text":"

Microservices should not fail or cause others to fail

Comprehensive documentation should be maintained for each microservice, optionally generating and publishing the API documentation from the code of the service itself, e.g. swagger, backstage.io

Testing should focus on maintaining a robust API, ensuring changes are additive to avoid breaking the overall system of microservices.

  • unit tests are self-contained, testing the handler functions that compose the overall API
  • integration tests ensure external services continue to provide expected results and shape of responses
  • end-to-end tests are very challenging when the system is first evolving, as design can change rapidly

unit tests should not delay the rate at which you deploy your microservices

"},{"location":"project-url-shortner/","title":"Project: URL Shortner as a Service","text":"

In this section we will build a webservice to create short url's for web addresses, as with services such as bit.ly.

The web service will also manage the redirection of your browser from the short url to the real web address.

This project will take the simplest approach and is therefore not attempting to build a production ready service.

"},{"location":"project-url-shortner/add-alias-to-database/","title":"add alias to database","text":""},{"location":"project-url-shortner/add-static-resources/","title":"Add static resources","text":""},{"location":"project-url-shortner/alias-generator/","title":"Alias generator","text":""},{"location":"project-url-shortner/compojure-template/","title":"Compojure Template","text":""},{"location":"project-url-shortner/compojure-template/#compojure-api","title":"Compojure API","text":"

https://weavejester.github.io/compojure/index.html

"},{"location":"project-url-shortner/create-database/","title":"create database","text":""},{"location":"project-url-shortner/create-html-form/","title":"Create HTML Form","text":""},{"location":"project-url-shortner/create-project/","title":"Create project","text":"

To create a web service we can use two commonly used libraries in Clojure, ring and compojure.

Ring provides many low-level functions to manage web requests and responses as well as providing an embedded web server (ie. Jetty). Most importantly it abstracts away all the complicated details of HTTP communication. So as a developer of the web app you mostly focus on processing a request map and returning a response map.

Compojure provides a simple way to define routes for your application, eg what function is called when a browser requests a specific url.

lein new compojure shorturl-service\n\ncd shorturl-service\n

Open the project.clj file in an editor and take a look at the dependencies added to the project.

(defproject shorturl-service \"0.1.0-SNAPSHOT\"\n  :description \"FIXME: write description\"\n  :url \"http://example.com/FIXME\"\n  :min-lein-version \"2.0.0\"\n  :dependencies [[org.clojure/clojure \"1.8.0\"]\n                 [compojure \"1.5.1\"]\n                 [ring/ring-defaults \"0.2.1\"]]\n  :plugins [[lein-ring \"0.9.7\"]]\n  :ring {:handler shorturl-service.handler/app}\n  :profiles\n  {:dev {:dependencies [[javax.servlet/servlet-api \"2.5\"]\n                        [ring/ring-mock \"0.3.0\"]]}})\n

Apart from Clojure itself, the compojure and ring libraries have been added.

The :plugins section adds lein-ring which allows us to run the server using the command lein ring server

The :ring section defines the default function to call when running the project

The :profiles section adds libraries useful for development and testing.

"},{"location":"project-url-shortner/create-project/#note-create-a-new-project-using-leiningen-and-the-compojure-template-then-go-into-the-project-created","title":"Note:: Create a new project using Leiningen and the compojure template, then go into the project created.","text":""},{"location":"project-url-shortner/delete-alias-from-database/","title":"delete alias from database","text":""},{"location":"project-url-shortner/design-data-structure/","title":"Design data structure","text":"

One of the first decisions is how to design the data structure to hold our short url and full url addresses.

"},{"location":"project-url-shortner/design-data-structure/#the-simplest-approach","title":"The simplest approach","text":"

We could create a really simple data structure with a vector

[\"goouk\" \"https://duckduckgo.com/\"]\n

In order to hold multiple short url mappings, we could have a vector of vectors

[[\"duckduckgo\" \"https://duckduckgo.com/\"]\n [\"practicalli\" \"https://practical.li\"]\n [\"slashdot\" \"https://slashdot.com\"]]\n
Although the above is nice and simple, it does not provide any context for the data.

"},{"location":"project-url-shortner/design-data-structure/#using-a-map-for-context","title":"Using a map for context","text":"

The keys in a map can add specific meaning and context to the design of the data structure.

Here are two examples, the first with keys as strings the second as keys as keywords.

{\"short-url\" \"practicalli\" \"full-url\" \"http://practical.li\"}\n\n{:short-url \"practicalli\" :full-url \"http://practical.li\"}\n

As keys need to be unique, then if we have multiple short url mappings to contain, then we need another data structure for each map.

[{:short-url \"practicalli\" :full-url \"http://practical.li\"}\n {:short-url \"slashdot\" :full-url \"https://slashdot.org\"}]\n

In the above design we cannot use a single map as all the keys in a map need to be unique

"},{"location":"project-url-shortner/design-data-structure/#simplifying-the-map","title":"Simplifying the map","text":"

As keys must be unique in a map, we cannot have multiple keys called :short-url, therefore using that design we cant have a single map.

We could simplify the map and remove the current keys and use the value for the short-url as the key and the full url as the value. This means we could just have a single map for all our short-url mappings.

{\"practicalli\" \"http://practical.li\"\n \"slashdot\"    \"https://slashdot.org\"\n \"duckduckgo\"  \"https://duckduckgo.com/\"}\n

Using Clojure keywords for the keys would also allow us to look up the full url addresses using the feature of maps that make keywords act like functions. This feature of the keyword in a map is just like calling the get function on the map with a specific key.

(def url-map\n  {:practicalli \"http://practical.li\"\n   :slashdot \"https://slashdot.org\"\n   :duck-duck-go \"https://duckduckgo.com/\"})\n\n(get url-map :practicalli)\n;; => \"http://practicalli.co.uk\"\n\n(url-map :practicalli)\n;; => \"http://practicalli.co.uk\"\n\n(:practicalli url-map )\n;; => \"http://practicalli.co.uk\"\n
"},{"location":"project-url-shortner/disable-anti-forgery-check/","title":"Disable anti-forgery check","text":"

The ring-defaults library provides sensible Ring middleware defaults, especially in terms of security. The ring-defaults library is included in the

Anyone can send a GET request to a ring webapp, however with ring-defaults included then only pages / URLs from the webapp itself are allowed to POST.

Ring uses an anti-forgery token that needs to be setup in the project, otherwise you get the dreaded \"Invalid Anti-forgery token\" error message.

To keep things simple we are going to turn off the anti-forgery settings provided by Ring-Defaults so we can make our POST without the CSRF protection.

(def app\n  (wrap-defaults\n  app-routes\n  (assoc-in site-defaults [:security :anti-forgery] false)))\n
"},{"location":"project-url-shortner/disable-anti-forgery-check/#note-edit-the-definition-of-app-in-srcshorturl-servicehandlerclj-and-replace-site-defaults-with-a-function-to-set-anti-forgery-to-false-in-site-defaults","title":"Note:: Edit the definition of app in src/shorturl-service/handler.clj and replace site-defaults with a function to set :anti-forgery to false in site-defaults.","text":""},{"location":"project-url-shortner/disable-anti-forgery-check/#ring-middleware-defaults","title":"Ring middleware defaults","text":"

There are a number of ring middleware defaults that define some common middleware functions for your app.

For example, here is the site-defaults definition

(def site-defaults\n  \"A default configuration for a browser-accessible website, based on current\n  best practice.\"\n  {:params    {:urlencoded true\n               :multipart  true\n               :nested     true\n               :keywordize true}\n   :cookies   true\n   :session   {:flash true\n               :cookie-attrs {:http-only true}}\n   :security  {:anti-forgery   true\n               :xss-protection {:enable? true, :mode :block}\n               :frame-options  :sameorigin\n               :content-type-options :nosniff}\n   :static    {:resources \"public\"}\n   :responses {:not-modified-responses true\n               :absolute-redirects     true\n               :content-types          true\n               :default-charset        \"utf-8\"}})\n
"},{"location":"project-url-shortner/disable-anti-forgery-check/#a-rough-guide-to-security-middleware-from-ring","title":"A rough guide to security middleware from ring","text":"
  • :anti-forgery - Set to true to add CSRF protection via the ring-anti-forgery library.
  • :content-type-options - Prevents attacks based around media-type confusion. See: wrap-content-type-options.
  • :frame-options - Prevents your site from being placed in frames or iframes. See: wrap-frame-options.
  • :hsts - If true, enable HTTP Strict Transport Security. See: wrap-hsts.
  • :ssl-redirect - If true, redirect all HTTP requests to the equivalent HTTPS URL. A map with an :ssl-port option may be set instead, if the HTTPS server is on a non-standard port. See: wrap-ssl-redirect.
  • :xss-protection - Enable the X-XSS-Protection header that tells supporting browsers to use heuristics to detect XSS attacks. See: wrap-xss-protection.

See more details of ring middleware defaults

"},{"location":"project-url-shortner/disable-anti-forgery-check/#adding-other-functions-as-middleware","title":"Adding other functions as middleware","text":"

ring just uses function composition for middleware you can simply wrap your own function calls around the call to wrap defaults, so long as those functions deal with request/response maps appropriately.

(def app\n  (my-additional-middleware\n    (wrap-defaults app-routes site-defaults)\n  arguments to my additional middleware))\n

Or use the threading macro

(def app\n  (-> (wrap-defaults app-routes site-defaults)\n      (friend-stuff arg arg)\n      (other-middleware arg arg arg))\n
"},{"location":"project-url-shortner/get-alias-from-database/","title":"get alias from database","text":""},{"location":"project-url-shortner/html-form/","title":"HTML Form","text":""},{"location":"project-url-shortner/if-let-function/","title":"if-let function","text":""},{"location":"project-url-shortner/named-alias-handler/","title":"Named alias handler","text":""},{"location":"project-url-shortner/persist-aliases/","title":"Persist aliases","text":""},{"location":"project-url-shortner/postgres-setup/","title":"Postgres setup","text":""},{"location":"project-url-shortner/redirect-to-full-url/","title":"Redirect short URL to full web address","text":"

Adding a redirect is very easy to do with ring, as the ring library provides a function called redirect that takes a url as an argument

(ns shorturl-service.handler\n  (:require [compojure.core :refer :all]\n            [compojure.route :as route]\n            [ring.middleware.defaults :refer [wrap-defaults site-defaults]]\n            [ring.handler.dump :refer [handle-dump]]\n            [ring.util.response :refer [redirect]]))\n
"},{"location":"project-url-shortner/redirect-to-full-url/#note-include-the-ringutilresponseredirect-function-into-the-shorturl-servicehandler-namespace-so-that-we-can-simply-call-the-redirect-function","title":"Note:: Include the ring.util.response/redirect function into the shorturl-service.handler namespace so that we can simply call the redirect function","text":""},{"location":"project-url-shortner/redis-setup/","title":"Redis setup","text":""},{"location":"project-url-shortner/refacor-hiccup-form/","title":"Refactor: Hiccup form","text":""},{"location":"project-url-shortner/return-short-url/","title":"Return short URL","text":""},{"location":"project-url-shortner/return-url-aliases/","title":"Return URL aliases","text":""},{"location":"project-url-shortner/run-project/","title":"Run project","text":"

The compojure template provides a working webserver and simple webapp out of the box.

lein ring server\n

If you have not used Compojure or Ring previously, then it may take a few seconds to download their libraries from the Internet before starting the Jetty web server.

You should see a output after the leiningen command showing you that the server has started

2016-07-15 13:44:02.242:INFO:oejs.Server:jetty-7.6.13.v20130916\n2016-07-15 13:44:02.313:INFO:oejs.AbstractConnector:Started SelectChannelConnector@0.0.0.0:3000\nStarted server on port 3000\n

Your default browser should also open at http://localhost:3000 with a message saying \"Hello World\". If your browser does not open then check for errors in the terminal where you ran the leiningen command.

"},{"location":"project-url-shortner/run-project/#note-run-the-project-to-start-the-server-and-webapp","title":"Note:: Run the project to start the server and webapp","text":""},{"location":"project-url-shortner/test-app-reloading/","title":"Test app reloading","text":"

The compojure template added several middleware functions to our project to make the webapp easier to work with. The wrap-reload middleware picks up changes to our code and will automatically load them into our running webapp. This provides rapid feedback on your coding.

(defroutes app-routes\n  (GET \"/\" [] \"Hello reloaded World\")\n  (route/not-found \"Not Found\"))\n

Now, refresh your browser and see the changes made.

"},{"location":"project-url-shortner/test-app-reloading/#note-make-a-simple-change-to-the-app-routes-function-in-srcshorturl-servicehandlerclj-file-eg-change-the-hello-world-string-to-hello-reloaded-world","title":"Note:: Make a simple change to the app-routes function in src/shorturl-service/handler.clj file, eg. change the \"Hello World\" string to \"Hello reloaded World\"","text":""},{"location":"project-url-shortner/test-app-reloading/#hint-you-should-only-need-to-restart-the-server-again-if-you-add-libraries-or-define-code-outside-of-the-scope-of-the-app-or-if-your-code-crashes-the-server-but-i-am-sure-that-wont-happen","title":"Hint:: You should only need to restart the server again if you add libraries or define code outside of the scope of the app. Or if your code crashes the server, but I am sure that wont happen :)","text":""},{"location":"project-url-shortner/using-ring-redirect/","title":"Using Ring Redirect","text":""},{"location":"project-url-shortner/whats-in-a-request/","title":"What is in a Request","text":"

The ring library converts the HTTP request into a clojure map, making it really easy to extract some or all of the values out of the request map.

"},{"location":"project-url-shortner/whats-in-a-request/#ring-parameters","title":"ring parameters","text":"

The wrap-params middleware function adds support for url-encoded parameters.

URL-encoded parameters are the primary way browsers pass values to web applications. These parameters are sent when a user submits a form.

When applied to a handler, the parameter middleware adds three new keys to the request map:

  • :query-params - A map of parameters from the query string
  • :form-params - A map of parameters from submitted form data
  • :params - A merged map of all parameters
"},{"location":"project-url-shortner/whats-in-a-request/#viewing-ring-parameters","title":"viewing ring parameters","text":"

You could write a function to simply display all the parameters as the body of the response. However Ring already provides a function to do this called handle-dump

To use the handle-dump function, first include the function in the namespace using (:require [ring.handler.dump :refer [handle-dump]])

(ns shorturl-service.handler\n  (:require [compojure.core :refer :all]\n            [compojure.route :as route]\n            [ring.middleware.defaults :refer [wrap-defaults site-defaults]]\n            [ring.handler.dump :refer [handle-dump]]))\n

As we have multiple :require statements we can simply chain them all together as above.

"},{"location":"project-url-shortner/whats-in-a-request/#note-add-the-ringhandlerdumphandle-dump-function-into-the-shorturl-servicehandler-namespace-along-with-other-require-statements","title":"Note:: Add the ring.handler.dump/handle-dump function into the shorturl-service.handler namespace along with other require statements","text":""},{"location":"projects/","title":"Project Guides","text":"

Follow a step-by-step project guide and build a live services which can also be deployed.

"},{"location":"projects/banking-on-clojure/","title":"Banking on Clojure web application","text":"

Work In Progress

Project actively being developed as part of the Practicalli Study group WebApps.

Code so far is shared on practicalli/banking-on-clojure-webapp GitHub repository

Building a Banking application using Clojure, spec, H2 (development) & Postgresql (live) databases and next.jdbc for SQL queries (migratus for db migrations).

The system infrastructure uses Jetty or HTTP-kit (making this switchable at runtime) and a component life cycle system (probably mount).

"},{"location":"projects/banking-on-clojure/#application-design-in-progress","title":"Application Design (in progress)","text":"

Data Specifications created using clojure.spec.alpha

  • Customer Details
  • Account holder
  • Bank account
  • Multiple Bank accounts
  • Credit Card
  • Mortgate

Functions and function specifications using clojure.spec.alpha

  • register-account-holder
  • open-credit-account
  • open-savings-account
  • open-credit-card-account
  • open-mortgage-account
  • Make a payment
  • Send account notification
  • Check for overdraft

Functions with specifications are instrumented to check arguments passed during function calls.

Generative testing is carried out via the kaocha test runner

"},{"location":"projects/banking-on-clojure/#development-workflow","title":"Development Workflow","text":"
  • Write a failing test
  • write mock data
  • write an function definition that returns the argument
  • run tests - tests should fail
  • write a spec for the functions argument - customer
  • write a spec for the return value
  • write a spec for relationship between args and return value
  • replace the mock data with generated values from specification
  • update functions and make tests pass
  • instrument functions
  • run specification checks
"},{"location":"projects/banking-on-clojure/account-overview-page/","title":"Initial design","text":"

The initial design is a simple copy/paste approach to see what the results look like in the web page. Once the design has been established, the code will be refactored to reduce duplication.

(defn accounts-overview-page\n  [request]\n  (response\n    (html5\n      {:lang \"en\"}\n      [:head\n       (include-css \"https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css\")]\n      [:body\n       [:section {:class \"hero is-info\"}\n        [:div {:class \"hero-body\"}\n         [:div {:class \"container\"}\n          [:h1 {:class \"title\"} \"Banking on Clojure\"]\n          [:p {:class \"subtitle\"}\n           \"Making your money immutable\"]]]]\n\n       [:section {:class \"section\"}\n        [:article {:class \"media\"}\n         [:figure {:class \"media-left\"}\n          [:p {:class \"image is-64x64\"}\n           [:img {:src \"https://raw.githubusercontent.com/jr0cket/developer-guides/master/clojure/clojure-bank-coin.png\"}]]]\n         [:div {:class \"media-content\"}\n          [:div {:class \"content\"}\n           [:h3 {:class \"subtitle\"}\n            \"Current Account : &lambda;1,000,000\"]\n           [:p \"Account number: 123456789   Sort code: 01-02-01\"]]]\n         [:div {:class \"media-right\"}\n          (link-to {:class \"button is-primary\"} \"/transfer\" \"Transfer\")\n          (link-to {:class \"button is-info\"} \"/payment\" \"Payment\")]]\n\n        [:article {:class \"media\"}\n         [:figure {:class \"media-left\"}\n          [:p {:class \"image is-64x64\"}\n           [:img {:src \"https://raw.githubusercontent.com/jr0cket/developer-guides/master/clojure/clojure-bank-coin.png\"}]]]\n         [:div {:class \"media-content\"}\n          [:div {:class \"content\"}\n           [:h3 {:class \"subtitle\"}\n            \"Savings Account : &lambda;1,000,000 \"]\n           [:p \"Account number: 123454321    Sort code: 01-02-01\"]]]\n         [:div {:class \"media-right\"}\n          (link-to {:class \"button is-primary\"} \"/transfer\" \"Transfer\")\n          (link-to {:class \"button is-info\"} \"/payment\" \"Payment\")]]\n\n        [:article {:class \"media\"}\n         [:figure {:class \"media-left\"}\n          [:p {:class \"image is-64x64\"}\n           [:img {:src \"https://raw.githubusercontent.com/jr0cket/developer-guides/master/clojure/clojure-bank-coin.png\"}]]]\n         [:div {:class \"media-content\"}\n          [:div {:class \"content\"}\n           [:h3 {:class \"subtitle\"}\n            \"Tax Free Savings Account : &lambda;1,000,000 \"]]]\n         [:div {:class \"media-right\"}\n          (link-to {:class \"button is-primary\"} \"/transfer\" \"Transfer\")\n          (link-to {:class \"button is-info\"} \"/payment\" \"Payment\")]]\n\n        [:article {:class \"media\"}\n         [:figure {:class \"media-left\"}\n          [:p {:class \"image is-64x64\"}\n           [:img {:src \"https://raw.githubusercontent.com/jr0cket/developer-guides/master/clojure/clojure-bank-coin.png\"}]]]\n         [:div {:class \"media-content\"}\n          [:div {:class \"content\"}\n           [:h3 {:class \"subtitle\"}\n            \"Mortgage Account : &lambda;1,000,000 \"]\n           [:p \"Mortgage Reference: 98r9e8r79wr87e9232\"]]]\n         [:div {:class \"media-right\"}\n          (link-to {:class \"button is-primary\"} \"/transfer\" \"Transfer\")\n          (link-to {:class \"button is-info\"} \"/payment\" \"Payment\")]]\n\n        ]])))\n
"},{"location":"projects/banking-on-clojure/account-overview-page/#refactor-page-layout-with-functions","title":"Refactor page layout with functions","text":"

Duplicate markup code for pages can be wrapped in a helper function that generated the correct code, usually given a few specific arguments.

Its usually more effective to create the design with duplicate code first and then see the sections of code that are duplicated.

  (defn unordered-list [items]\n    [:ul\n     (for [i items]\n       [:li i])])\n

Many lines of code can now be reduced to a single line when adding an unordered list to a page defined with hiccup.

  [:div\n   (unordered-list [\"collection\" \"of\" \"list\" \"items\"])]\n

Applying this technique to the bank accounts overview page. Find the code that is repeated the most and define a function containing that code.

The function should take arguments, typically a map so its more flexible in what arguments must be passed and increases the usability of the function

(defn bank-account-media-object\n  [account-details]\n  [:article {:class \"media\"}\n   [:figure {:class \"media-left\"}\n    [:p {:class \"image is-64x64\"}\n     [:img {:src \"https://raw.githubusercontent.com/jr0cket/developer-guides/master/clojure/clojure-bank-coin.png\"}]]]\n   [:div {:class \"media-content\"}\n    [:div {:class \"content\"}\n     [:h3 {:class \"subtitle\"}\n      (str (:account-type account-details) \" : &lambda;\" (:account-value account-details))]\n     [:p\n      (str \"Account number: \" (:account-number account-details) \" Sort code: \" (:account-sort-code account-details))]]]\n   [:div {:class \"media-right\"}\n    (link-to {:class \"button is-primary\"} \"/transfer\" \"Transfer\")\n    (link-to {:class \"button is-info\"} \"/payment\" \"Payment\")]])\n

Calling the bank-account-media-object function with a map will generate the same resulting page.

  (bank-account-media-object\n    {:account-type      \"Current Account\"\n     :account-number    \"123456789\"\n     :account-value     \"i1,000,000\"\n     :account-sort-code \"01-02-01\"})\n

Result of calling the bank-account-media-object with that map:

[:article {:class \"media\"}\n [:figure {:class \"media-left\"}\n  [:p {:class \"image is-64x64\"}\n   [:img {:src \"https://raw.githubusercontent.com/jr0cket/developer-guides/master/clojure/clojure-bank-coin.png\"}]]]\n [:div {:class \"media-content\"}\n  [:div {:class \"content\"}\n   [:h3 {:class \"subtitle\"} \"Current Account : &lambda;\" \"i1,000,000\"]\n   [:p \"Account number: \" {:account-number {:account-type \"Current Account\", :account-number \"123456789\", :account-value \"i1,000,000\", :account-sort-code \"01-02-01\"}} \" Sort code: \" {:account-sort-code {:account-type \"Current Account\", :account-number \"123456789\", :account-value \"i1,000,000\", :account-sort-code \"01-02-01\"}}]]]\n   [:div {:class \"media-right\"}\n    [:a {:href #object[java.net.URI 0x3516357f \"/transfer\"], :class \"button is-primary\"} (\"Transfer\")]\n    [:a {:href #object[java.net.URI 0x4cf62d27 \"/payment\"], :class \"button is-info\"} (\"Payment\")]]]\n

If some key/value pairs are not included, the function will still work. Worst case is that there will be gaps where expected values would otherwise be included in the page.

Instead of many duplicate lines of code, the accounts-overview-page is reduce to less than half the code is far more readable as it is expressed as data in a hash-map.

(defn accounts-overview-page\n  [request]\n  (response\n    (html5\n      {:lang \"en\"}\n      [:head\n       (include-css \"https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css\")]\n      [:body\n       [:section {:class \"hero is-info\"}\n        [:div {:class \"hero-body\"}\n         [:div {:class \"container\"}\n          [:h1 {:class \"title\"} \"Banking on Clojure\"]\n          [:p {:class \"subtitle\"}\n           \"Making your money immutable\"]]]]\n\n       [:section {:class \"section\"}\n        (bank-account-media-object {:account-type  \"Current Account\" :account-number    \"123456789\"\n                                    :account-value \"1,234\"       :account-sort-code \"01-02-01\"})\n\n        (bank-account-media-object {:account-type  \"Savings Account\" :account-number    \"123454321\"\n                                    :account-value \"2,000\"       :account-sort-code \"01-02-01\"})\n\n        (bank-account-media-object {:account-type  \"Tax Free Savings Account\" :account-number    \"123454321\"\n                                    :account-value \"20,000\"                :account-sort-code \"01-02-01\"})\n\n        (bank-account-media-object {:account-type  \"Mortgage Account\" :account-number    \"98r9e8r79wr87e9232\"\n                                    :account-value \"354,000\"          :account-sort-code \"01-02-01\"})\n\n        ]])))\n
"},{"location":"projects/banking-on-clojure/account-overview-page/#wrap-results-in-page-heading","title":"Wrap results in page heading","text":"

The heading is common in all pages, so it can be extracted to a function that processes the result of all handlers, this is referred to as middleware.

"},{"location":"projects/banking-on-clojure/clojure-server-project/","title":"Create Clojure server project","text":"

Initially the project is configured with a simple application sever using http-kit and routing defined by compojure library. The ring library is used to generate well-formed responses.

"},{"location":"projects/banking-on-clojure/clojure-server-project/#create-project","title":"Create project","text":"

Create a Clojure CLI project from the app template

Practicalli Clojure CLI ConfigAlias Definition

Using :project/create alias from Practicalli Clojure CLI Config

clojure -T:project/create :template app :name practicalli.banking-on-clojure/service :target-dir banking-on-clojure\n

Add an alias definition to the user configuration for Clojure CLI, eg. $XDG_CONFIG_HOME/clojure/deps.edn or $HOME/.clojure/deps.edn

:project/create\n{:replace-deps {io.github.seancorfield/deps-new {:git/tag \"v0.4.13\" :git/sha \"879c4eb\"}}\n :exec-fn      org.corfield.new/create\n :exec-args    {:template app :name practicalli/playground}}\n

Consider using Practicalli Clojure CLI Config to simply add a wide range of tools for Clojure CLI

"},{"location":"projects/banking-on-clojure/clojure-server-project/#library-dependencies","title":"Library Dependencies","text":"

Add the http-kit, compojure and ring libraries to the project configuration

deps.edn
{:paths [\"src\" \"resources\"]\n :deps {org.clojure/clojure {:mvn/version \"1.11.3\"}\n        http-kit/http-kit   {:mvn/version \"2.3.0\"}\n        ring/ring           {:mvn/version \"1.9.6\"}}}\n
"},{"location":"projects/banking-on-clojure/clojure-server-project/#configure-namespace","title":"Configure namespace","text":"

Add org.httpkit.server, compojure and ring.util.response as required namespaces

src/practicalli/banking_on_clojure/service.clj
(ns practicalli.banking-on-clojure\n  (:gen-class)\n  (:require [org.httpkit.server :as app-server]\n            [compojure.core :refer [defroutes GET]]\n            [ring.util.response :refer [response]]))\n
"},{"location":"projects/banking-on-clojure/clojure-server-project/#routing-and-request-handlers","title":"Routing and Request handlers","text":"

compojure provides an abstraction for routing. The defroutes function directs requests to handlers, which are Clojure functions that take a request hash-map as an argument.

The routing is based on the http protocol (GET, POST, etc) and URL.

src/practicalli/banking_on_clojure/service.clj
(defn welcome-page\n  [request]\n  (response \"Banking on Clojure\"))\n\n(defroutes app\n  (GET \"/\" [] welcome-page))\n
"},{"location":"projects/banking-on-clojure/clojure-server-project/#defining-the-application-server-system","title":"Defining the application server system","text":"

A clojure.core/atom is used to hold a reference to application server instance for stopping/restarting the server.

src/practicalli/banking_on_clojure/service.clj
(defonce app-server-instance (atom nil))\n

A function to start the application server on a given HTTP port number.

The start process sends a timestamped log message to standard out before starting the application server.

The app-server-instance is updated with a reference to the running application server.

src/practicalli/banking_on_clojure/service.clj
(defn app-server-start\n  \"Start the application server and log the time of start.\"\n\n  [http-port]\n  (println (str (java.util.Date.)\n                \" INFO: Starting server on port: \" http-port))\n  (reset! app-server-instance\n          (app-server/run-server #'app {:port http-port})))\n

A function to stop the application server, send out a timestamped log message and remove the application server reference.

src/practicalli/banking_on_clojure/service.clj
(defn app-server-stop\n  \"Gracefully shutdown the server, waiting 100ms.  Log the time of shutdown\"\n  []\n  (when-not (nil? @app-server-instance)\n    (@app-server-instance :timeout 100)\n    (reset! app-server-instance nil)\n    (println (str (java.util.Date.)\n                  \" INFO: Application server shutting down...\"))))\n

A function to restart the application server, which simply calls the stop and start functions.

src/practicalli/banking_on_clojure/service.clj
(defn app-server-restart\n  \"Convenience function to stop and start the application server\"\n\n  [http-port]\n  (app-server-stop)\n  (app-server-start http-port))\n

A -main function that will be called from the command line, taking an optional HTTP port. If a port number is no provided as an argument, an operating system environment variable called PORT is used or the default 8888 is used.

Using an operating system environment variable is important when deploying the application to a cloud environment.

src/practicalli/banking_on_clojure/service.clj
(defn -main\n  \"Select a value for the http port the app-server will listen to\n  and call app-server-start\n\n  The http port is either an argument passed to the function,\n  an operating system environment variable or a default value.\"\n\n  [& [http-port]]\n  (let [http-port (Integer. (or http-port (System/getenv \"PORT\") \"8888\"))]\n    (app-server-start http-port)))\n
"},{"location":"projects/banking-on-clojure/clojure-server-project/#repl-driven-development-helpers","title":"REPL driven development helpers","text":"

A comment block is added at the end of the code to show how to start/stop/restart the web application, along with a few useful expressions.

src/practicalli/banking_on_clojure/service.clj
(comment\n  ;; Start application server - via `-main` or `app-server-start`\n  (-main)\n  (app-server-start 8888)\n\n  ;; Stop / restart application server\n  (app-server-stop)\n  (app-server-restart 8888)\n\n  ;; Get PORT environment variable from Operating System\n  (System/getenv \"PORT\")\n\n  ;; Get all environment variables\n  ;; use a data inspector to view environment-variables name\n  (def environment-variables\n    (System/getenv))\n\n  ;; Check values set in the default system properties\n  (def system-properties\n    (System/getProperties))\n  )\n
"},{"location":"projects/banking-on-clojure/clojure-server-project/#the-complete-code-so-far","title":"The Complete code so far","text":"src/practicalli/banking_on_clojure/service.clj
(ns practicalli.banking-on-clojure\n  (:gen-class)\n  (:require [org.httpkit.server :as app-server]\n            [compojure.core :refer [defroutes GET]]\n            [ring.util.response :refer [response]]))\n\n;; ---------------------------------------\n;; Request handlers\n\n(defn welcome-page\n  [request]\n  (response \"Banking on Clojure\"))\n;; ---------------------------------------\n\n;; ---------------------------------------\n;; Request Routing\n\n(defroutes app\n  (GET \"/\" [] welcome-page))\n;; ---------------------------------------\n\n;; ---------------------------------------\n;; System\n\n;; Reference to application server instance for stopping/restarting\n(defonce app-server-instance (atom nil))\n\n\n(defn app-server-start\n  \"Start the application server and log the time of start.\"\n\n  [http-port]\n  (println (str (java.util.Date.)\n                \" INFO: Starting server on port: \" http-port))\n  (reset! app-server-instance\n          (app-server/run-server #'app {:port http-port})))\n\n\n(defn app-server-stop\n  \"Gracefully shutdown the server, waiting 100ms.  Log the time of shutdown\"\n  []\n  (when-not (nil? @app-server-instance)\n    (@app-server-instance :timeout 100)\n    (reset! app-server-instance nil)\n    (println (str (java.util.Date.)\n                  \" INFO: Application server shutting down...\"))))\n\n\n(defn app-server-restart\n  \"Convenience function to stop and start the application server\"\n\n  [http-port]\n  (app-server-stop)\n  (app-server-start http-port))\n\n\n(defn -main\n  \"Select a value for the http port the app-server will listen to\n  and call app-server-start\n\n  The http port is either an argument passed to the function,\n  an operating system environment variable or a default value.\"\n\n  [& [http-port]]\n  (let [http-port (Integer. (or http-port (System/getenv \"PORT\") \"8888\"))]\n    (app-server-start http-port)))\n;; ---------------------------------------\n\n;; ---------------------------------------\n;; REPL driven development helpers\n\n(comment\n  ;; Start application server - via `-main` or `app-server-start`\n  (-main)\n  (app-server-start 8888)\n\n  ;; Stop / restart application server\n  (app-server-stop)\n  (app-server-restart 8888)\n\n  ;; Get PORT environment variable from Operating System\n  (System/getenv \"PORT\")\n\n  ;; Get all environment variables\n  ;; use a data inspector to view environment-variables name\n  (def environment-variables\n    (System/getenv))\n\n  ;; Check values set in the default system properties\n  (def system-properties\n    (System/getProperties))\n  )\n;; ---------------------------------------\n
"},{"location":"projects/banking-on-clojure/clojure-spec-generate-mock-data/","title":"Clojure spec generate mock data","text":""},{"location":"projects/banking-on-clojure/clojure-spec-generate-mock-data/#generate-database-record-data-from-clojure-specifications","title":"Generate database record data from Clojure Specifications","text":"

The Clojure specifications developed for the banking-on-clojure application can be used to generate random data that can be used to test the database schema and CRUD functions.

In the database schema design page the next.jdbc.sql functions were used to define a generic function for inserting records into a database

(defn insert-record\n  [table record-data db-spec]\n  (with-open [connection (jdbc/get-connection db-spec)]\n    (jdbc-sql/insert! connection table record-data)))\n

The call to this function uses a value for record-data that is almost identical to the value generated from the practicalli.banking-on-clojure/account-holder specification. The only difference is the key name styles.

(create-record db-specification-dev\n                 :public.account_holders\n                 {:account_holder_id      (java.util.UUID/randomUUID)\n                  :first_name             \"Rachel\"\n                  :last_name              \"Rocketpack\"\n                  :email_address          \"rach@rocketpack.org\"\n                  :residential_address    \"1 Ultimate Question Lane, Altar IV\"\n                  :social_security_number \"BB104312D\"})\n
"},{"location":"projects/banking-on-clojure/clojure-spec-generate-mock-data/#generating-data-from-clojure-specifications","title":"Generating data from Clojure Specifications","text":"

clojure.spec.alpha/gen takes a spec and returns a reference to a generator for that specification.

clojure.spec.gen.alpha/generate returns a random value using the spec generator.

Generate a value from the account-holder specification

(spec-gen/generate (spec/gen ::account-holder))\n

Create a simple helper function in practicalli/specifications-banking.clj to generating mock data from the specifications relevant to the database.

(defn mock-data-account-holder\n  []\n  (spec-gen/generate (spec/gen ::account-holder)))\n

The practicalli.database-access/create-record function can now be passed a generated record-data argument using (practicalli.specifications-banking/mock-data-account-holder)

(create-record\n  db-specification-dev\n  :public.account_holders\n  (practicalli.specifications-banking/mock-data-account-holder))\n
"},{"location":"projects/banking-on-clojure/clojure-spec-generate-mock-data/#clojure-and-database-naming-disparity","title":"Clojure and database naming disparity","text":"

When calling create-record with a specification there is a disparity between the names of the keys from the clojure specification and the names of the columns in database.

  • Clojure uses kebab-case
  • Database uses snake_case

Most relational databases will not accept column names in kebab-case.

Do we have to compromise the Clojure style kebab-case just for the database? Do we have to create our own generators or transform code to convert the specs generated?

"},{"location":"projects/banking-on-clojure/clojure-spec-generate-mock-data/#automatically-converting-key-names","title":"Automatically converting key names","text":"

Clojure uses kebab-case for key names in Clojure specs (and all names in general).

Relational databases use snake_case for table and column names and most databases will not support kebab-case.

It is simple to convert between the two cases, as its simply a string replacement for - with _. clj-commons/camel-snake-kebab is a library that converts between each of the naming styles.

camel-snake-kebab.core/->snake_case takes a name and returns it in snake_case

next.jdbc support conversion between camel-case names when clj-commons/camel-snake-kebab is added to the project dependencies.

The next.jdbc.sql CRUD functions take an optional configuration hash-map as the fourth argument. When ,,, is on the class path, next.jdbc has two hash-maps available that will define functions to use from the ,,, library to do the name conversions.

  • next.jdbc/snake-kebab-opts for unqualified Clojure keywords
  • next.jdbc/unqualified-snake-kebab-opts for unqualified Clojure keywords
"},{"location":"projects/banking-on-clojure/clojure-spec-generate-mock-data/#refactor-crud-functions-to-automatically-convert-case","title":"Refactor CRUD functions to automatically convert case","text":"

Refactor the CRUD functions to include the snake-kebab-opts hash-map as an argument.

Only those functions that take keywords as part of the argument need to be changed, so next.jdbc.sql/query does not need to change and therefore the practicalli.database-access/read-record remains unchanged.

Refactor the practicalli.database-access/create-record function

(defn create-record\n  \"Insert a single record into the database using a managed connection.\n  Arguments:\n  - table - name of database table to be affected\n  - record-data - Clojure data representing a new record\n  - db-spec - database specification to establish a connection\"\n  [db-spec table record-data]\n  (with-open [connection (jdbc/get-connection db-spec)]\n    (jdbc-sql/insert! connection table record-data jdbc/snake-kebab-opts)))\n

Refactor the practicalli.database-access/update-record function

(defn update-record\n  \"Insert a single record into the database using a managed connection.\n  Arguments:\n  - table - name of database table to be affected\n  - record-data - Clojure data representing a new record\n  - db-spec - database specification to establish a connection\n  - where-clause - column and value to identify a record to update\"\n  [db-spec table record-data where-clause]\n  (with-open [connection (jdbc/get-connection db-spec)]\n    (jdbc-sql/update! connection table record-data where-clause jdbc/snake-kebab-opts)))\n
"},{"location":"projects/banking-on-clojure/clojure-spec-generate-mock-data/#disparity-between-spec-namespace-and-database-design","title":"Disparity between spec namespace and database design","text":"

A qualified keyword is where that keyword has a namespace, eg. :practicalli/name rather than :name

Using qualified keywords is recommended so that they can be unique across the application (and ideally multiple applications).

When using a database, the table name can be used to qualify the results returned from queries to the database. However, if the table names are different to the clojure.spec specification then it is harder to test

To resolve this issue, either the specifications should be refactored or the database names. I suspect probably both would benefit from some redesign now experience has been gained in using them.

"},{"location":"projects/banking-on-clojure/continuous-integration/","title":"Continuous Integration with CirceCI","text":"

The application infrastructure has been established and now the main body of the development can commence. Therefore it is very valuable to establish a continuous integration pipeline.

Practicalli Clojure: Continuous Integration with CircleCI covers in detail how to use Continuous Integration with Clojure projects (deps.edn and Leiningen).

"},{"location":"projects/banking-on-clojure/continuous-integration/#using-kaocha-test-runner","title":"Using kaocha test runner","text":"

LambdaIsland kaocha test runner is used as the unit test runner as it will also run generative tests where functions have specifications defined.

Add a :test/run alias to the deps.edn file in the root of the project.

The configuration runs Kaocha without test randomisation for a consistent test order and stops the test runner if a test fails, ensuring time is not spent running tests after a failure.

:test/run\n{:extra-paths [\"test\"]\n :extra-deps {lambdaisland/kaocha {:mvn/version \"1.60.977\"}}\n :exec-fn kaocha.runner/exec-fn\n :exec-args {:randomize? false\n             :fail-fast? true}}\n

Create the file bin/kaocha in the root of the project and make it executable (e.g. chmod a+x bin/kaocha)

#!/usr/bin/env bash\n\n## Script to run the kaocha test runner\n## for unit tests and clojure spec generative tests\n\nclojure -X:test/run \"$@\"\n
"},{"location":"projects/banking-on-clojure/continuous-integration/#configure-circleci-pipeline","title":"Configure CircleCI pipeline","text":"

Configure a pipeline to use a docker image with Java 17 and the latest Clojure CLI tools

The configuration uses the Kaocha Orb to simplify the configuration required to use the Kaocha test runner from within CircleCI.

A run step will call the kaocha script that is included in the project code repository and run the unit tests. If function specifications are present in the project, generative tests will also be run.

version: 2.1  # circleci configuration version\n\norbs:\n  kaocha: lambdaisland/kaocha@0.0.3 # Org settings > Security > uncertified orbs\n\njobs:    # basic units of work in a run\n  build: # runs not using Workflows must have a `build` job as entry point\n    working_directory: ~/build    # directory where steps will run\n    docker:                       # run the steps with Docker\n      - image: cimg/clojure:1.10  # image is primary container where `steps` are run\n    environment:                  # environment variables for primary container\n      JVM_OPTS: -Xmx3200m         # limit the maximum heap size to prevent out of memory errors\n    steps:                        # commands that comprise the `build` job\n      - checkout                  # check out source code to working directory\n      - restore_cache:            # restores saved cache if checksum hasn't changed since the last run\n          key: banking-on-clojure-webapp-{{ checksum \"deps.edn\" }}\n      - run: clojure -X:test/runner\n      - save_cache:               # generate and store cache in the .m2 directory using a key template\n          paths:\n            - ~/.m2\n            - ~/.gitlibs\n          key: banking-on-clojure-webapp-{{ checksum \"deps.edn\" }}\n      - run: bin/kaocha --reporter kaocha.report/documentation --no-randomize --no-color --plugin kaocha.plugin.alpha/spec-test-check\n

Enable 3rd Party Orbs

Enable 3rd Party Orbs in Organisation > Security settings

"},{"location":"projects/banking-on-clojure/create-records/","title":"Create database records","text":"

Several options were explored when designing database query functions. Using next.jdbc.sql functions provides a Clojure data structures approach, where as next.jdbc/execute! uses specific SQL statement code.

Take the SQL approach if generating SQL statements directly.

Take the Clojure approach if to generate SQL statements from Clojure data structures.

"},{"location":"projects/banking-on-clojure/create-records/#generic-create-record-function","title":"Generic create record function","text":"

Use the generic create function from the database schema design section

(defn create-record\n  \"Insert a single record into the database using a managed connection.\n  Arguments:\n  - table - name of database table to be affected\n  - record-data - Clojure data representing a new record\n  - db-spec - database specification to establish a connection\"\n  [db-spec table record-data]\n  (with-open [connection (jdbc/get-connection db-spec)]\n    (jdbc-sql/insert! connection table record-data)))\n
"},{"location":"projects/banking-on-clojure/create-records/#create-a-new-account_holder-record","title":"Create a new account_holder record","text":"

Call the create-record function with the development database specification, the account holder table name and a Clojure hash-map of the record data.

Each key in the map represents a column name and the value associated with the key is the value to be inserted in the record for its column.

(create-record db-specification-dev\n               \"public.account_holders\"\n               {:account_holder_id      (java.util.UUID/randomUUID)\n                :first_name             \"Rachel\"\n                :last_name              \"Rocketpack\"\n                :email_address          \"rach@rocketpack.org\"\n                :residential_address    \"1 Ultimate Question Lane, Altar IV\"\n                :social_security_number \"BB104312D\"})\n
"},{"location":"projects/banking-on-clojure/create-records/#create-account-record","title":"Create account record","text":"

Create a new record in the public.accounts table.

(create-record db-specification-dev\n               \"public.accounts\"\n               {:account_id        (java.util.UUID/randomUUID)\n                :account_number    \"1234567890\"\n                :account_sort_code \"102010\"\n                :account_name      \"Current\"\n                :current_balance   100\n                :last_updated      \"2020-09-11\"\n                :account_holder_id (java.util.UUID/randomUUID)})\n
"},{"location":"projects/banking-on-clojure/create-records/#create-transaction-record","title":"Create transaction record","text":"

Create a record in the public.transaction_history table.

(create-record db-specification-dev\n               \"public.transaction_history\"\n               {:transaction_id        (java.util.UUID/randomUUID)\n                :transaction_reference \"Salary\"\n                :transaction_date      \"2020-09-11\"\n                :account_number        \"1234567890\"})\n

Generating example data from Clojure Spec

Clojure Spec: generate mock database data

"},{"location":"projects/banking-on-clojure/cyclic-load-dependency/","title":"Cyclic Load Dependency","text":"

A cyclic load dependency is where one namespace requires one or more other namespaces that then require the original namespace, forming a loop when resolve all the required namespaces. When there are cyclic namespace dependencies a warning is returned when evaluating any of the namespaces involved.

A good way to spot cyclic load dependencies it to regularly run a test runner on the code, or even set up a test runner to watch for changes to the file system which then triggers an automatic test run. For example, kaocha --watch.

"},{"location":"projects/banking-on-clojure/cyclic-load-dependency/#tips-on-avoiding-cyclic-load-dependencies","title":"Tips on avoiding cyclic load dependencies","text":"
  • Add comment sections describing the purpose of the different parts of code, creating logical separation of code. Refactor of namespaces is far easier to see and helps ensure related code is kept together.

  • Ensure test and specification code is in its own namespace. Keeping the right level of abstraction in tests and clojure spec test.check can help avoid issues.

  • Use require to add dependent namespaces, preferably using a meaningful :as alias or using :refer for only the function names used. (:require ,,, :refer :all) or (use ,,) expressions can pull in too many other namespaces and cause issues

  • A cyclic load dependency error message is a good point to review and refactor the application design.

  • Where two namespaces are interdependent on each other, factor out shared code into a third namespace required by each of the other two. This breaks the dependency between the two original namespaces.

"},{"location":"projects/banking-on-clojure/cyclic-load-dependency/#example-banking-on-clojure","title":"Example - Banking on Clojure","text":"

The practicalli.banking-on-clojure.handler namespace contains functions that need to access data in the database, so the practicalli.database-access namespace is required.

(ns practicalli.banking-on-clojure.handler\n  (:require\n   ;; Web Application\n   [ring.util.response :refer [response]]\n   [hiccup.core :refer [html]]\n   [hiccup.page :refer [html5 include-js include-css]]\n   [hiccup.element :refer [link-to]]\n\n   ;; Data access\n   [practicalli.database-access :as data-access]))\n

The practicalli.database-access namespace required the practicalli.banking-on-clojure.specification namespace to use the specifications for generating data

(ns practicalli.database-access\n  (:require [next.jdbc :as jdbc]\n            [next.jdbc.sql :as jdbc-sql]\n            [next.jdbc.specs :as jdbc-spec]\n\n            [practicalli.banking-on-clojure.specification]))\n

In the practicalli.banking-on-clojure.specification namespace, a functional specification had been defined for the register-account-holder function, which required the practicalli.banking-on-clojure.handler to be required. Even though the specifications testing had logically moved to the database-access namespace, this functional specification remained.

(ns practicalli.banking-on-clojure.specification\n  (:require\n   ;; Clojure Specifications\n   [clojure.spec.alpha :as spec]\n   [clojure.spec.gen.alpha :as spec-gen]\n   [clojure.spec.test.alpha :as spec-test]\n\n   ;; Helper namespaces\n   [clojure.string]\n\n   [practicalli.banking-on-clojure.handler :as handler]))\n

This set of require expressions lead to a cyclic load dependency error.

-> indicate that a namespace requires the namespace it is pointing too.

Removing the require of practicalli.banking-on-clojure.handler from the practicalli.banking-on-clojure.specification namespace breaks the cyclic dependency.

"},{"location":"projects/banking-on-clojure/cyclic-load-dependency/#testing-the-database-on-ci","title":"Testing the database on CI","text":"

Need to create the tables before the tests can run. - update the schema so the create tables can run without failure, check if table exist and if not create it.

"},{"location":"projects/banking-on-clojure/database-queries/","title":"Defining Database Queries","text":"

Using the SQL statement for Inserting records as an example, several different approached are covered for defining database queries. The options are similar for update and delete queries.

All options use the with-open function to wrap the connection to the database, to automatically close that connection once the function has completed.

Approach Description next.jdbc/execute! function Simple approached used previously for creating tables Defining a generic function Pass SQL statements and connection into a single function, using def to define sql statements next.jdbc.sql/* functions Generate SQL statements from Clojure data structures, e.g. hash-maps, vectors, etc. Generic function with next.jdbc.sql/* functions Generic insert, update and delete functions that take a Clojure data structures"},{"location":"projects/banking-on-clojure/database-queries/#example-sql-queries-from-dbeaver","title":"Example SQL queries from DBeaver","text":"

Using the DBeaver tool the basic form of an insert command is generated from Generate SQL > DDL

INSERT INTO PUBLIC.ACCOUNT_HOLDERS\n (ACCOUNT_HOLDER_ID, FIRST_NAME, LAST_NAME, EMAIL_ADDRESS, RESIDENTIAL_ADDRESS, SOCIAL_SECURITY_NUMBER)\nVALUES(?, '', '', '', '', '');\n
"},{"location":"projects/banking-on-clojure/database-queries/#using-the-general-execute-command","title":"Using the general execute! command","text":"

Using the general jdbc/execute! is the same form as used previously to create, show and drop database tables.

(defn persist-account-holder\n  \"Persist a new account holder record\"\n  [account-holder-id db-spec]\n  (with-open [connection (jdbc/get-connection db-spec)]\n    (jdbc/execute!\n      connection\n      [(str \"insert into account_holders(\n               account_holder_id,first_name,last_name,email_address,residential_address,social_security_number)\n             values(\n               '\" account-holder-id \"', 'Jenny', 'Jetpack', 'jen@jetpack.org', '42 Meaning Lane, Altar IV', 'AB101112C' )\")])) )\n

Call the function with a randomly generated UUID value for the account_holder_id and the database details in the form of the development database specification.

(persist-account-holder (java.util.UUID/randomUUID) db-specification-dev)\n
"},{"location":"projects/banking-on-clojure/database-queries/#using-a-generic-function-approach","title":"Using a generic function approach","text":"

Write a Clojure function that takes in any SQL statement and executes that against a specific database specification.

(defn database-update\n  [sql-statement db-spec ]\n  (with-open [connection (jdbc/get-connection db-spec)]\n    (jdbc/execute! connection sql-statement)))\n

Refactor sql statements into their own vars, so they can be reused.

(def account-holder-jenny\n  [(str \"insert into account_holders(account_holder_id,first_name,last_name,email_address,residential_address,social_security_number)\n    values('\" (java.util.UUID/randomUUID) \"', 'Jenny', 'Jetpack', 'jen@jetpack.org', '42 Meaning Lane, Altar IV', 'AB101112C' )\")])\n

Update the database using the name of the SQL statement

(database-update account-holder-jenny db-specification-dev)\n

Limitation of def

The def expression must be evaluated each time a new value for the account_holder_id is required. The first time the def is evaluated, the java-util.UUID/randomUUID function is evaluated to a specific value and that value is cached.

Using the account-holder-jenny name in other code will use the cache until the def expression is forcefully evaluated (by the developer or by restarting the REPL).

"},{"location":"projects/banking-on-clojure/database-queries/#using-nextjdbc-friendly-functions","title":"Using next.jdbc friendly functions","text":"

Using next.jdbc.sql functions. For example:

(jdbc-sql/insert! ds :address {:name \"A. Person\" :email \"albert@person.org\"})\n

For the banking-on-clojure project this would take the form

(defn add-account-holder\n  [account-holder-id data-source]\n  (jdbc-sql/insert!\n    data-source\n    :table-name {:column-name \"value\" ,,,}))\n

In this example, the next.jdbc insert! function is used to add an account holder record.

(defn add-account-holder\n  [account-holder-id db-spec]\n  (with-open [connection (jdbc/get-connection db-spec)]\n    (jdbc-sql/insert!\n      connection\n      :public.account_holders {:account_holder_id      account-holder-id\n                               :first_name             \"Rachel\"\n                               :last_name              \"Rocketpack\"\n                               :email_address          \"rach@rocketpack.org\"\n                               :residential_address    \"1 Ultimate Question Lane, Altar IV\"\n                               :social_security_number \"BB104312D\"})))\n

Calling the function with generated data.

(add-account-holder (java.util.UUID/randomUUID) db-specification-dev)\n
"},{"location":"projects/banking-on-clojure/database-queries/#generic-insert-function-with-nextjdbcsql","title":"Generic insert function with next.jdbc.sql","text":"
(defn insert-record\n  [table record-data db-spec]\n  (with-open [connection (jdbc/get-connection db-spec)]\n    (jdbc-sql/insert! connection table record-data)))\n

The data to pass in looks familiar. Its the table name plus a data structure that looks like a specification for an account holder.

:public.account_holders\n\n{:account_holder_id      (java.util.UUID/randomUUID)\n :first_name             \"Rachel\"\n :last_name              \"Rocketpack\"\n :email_address          \"rach@rocketpack.org\"\n :residential_address    \"1 Ultimate Question Lane, Altar IV\"\n :social_security_number \"BB104312D\"}\n

So the specifications already defined can be used to generate mock data for the database.

"},{"location":"projects/banking-on-clojure/database-tables/","title":"Design and Create Database Tables","text":"

The design for Banking On Clojure database contains three tables, account_holders, accounts and transaction_history.

account_holders contains a unique entry for every customer in the bank.

accounts contains every account created for the bank. Each account has an account holder, a current balance and a date of when the current balance was last updated.

transaction_history contains every transaction that takes place in the bank. Each transaction is related to a specific account. The current balance for an account is built from all the transactions for a specific account. The number of transactions used to calculate the current balance is reduced by using the last_updated value from accounts.

"},{"location":"projects/banking-on-clojure/database-tables/#organising-the-code","title":"Organising the code","text":"

The SQL statements that create the database tables will be bound to a suitable name using def. The value will be a vector containing the string of the SQL statement.

The SQL statements are generated by the DBever database management tool. The tables are created in DBeaver and the DDL script is exported for each table and pasted in the Clojure code.

A create-tables helper function executes the given SQL statements on a specific data source, from within a transaction so that all table are either created or none are created.

db-specification-dev is a name bound to the database specification map for the development database. This should eventually end up as an aero configuration along with the production database specification.

(def db-specification-dev {:dbtype \"h2\" :dbname \"banking-on-clojure\"})\n
"},{"location":"projects/banking-on-clojure/database-tables/#define-account-holders-table","title":"Define ACCOUNT-HOLDERS table","text":"

Define the SQL statement to create a table to hold all the ACCOUNT-HOLDERS.

The design includes all the customer details plus from the Banking on Clojure specifications and the account_holder_id that uniquely identifies the customer.

(def schema-account-holders-table\n  [\"CREATE TABLE PUBLIC.ACCOUNT_HOLDERS(\n     ACCOUNT_HOLDER_ID UUID(16) NOT NULL,\n     FIRST_NAME VARCHAR(32),\n     LAST_NAME VARCHAR(32),\n     EMAIL_ADDRESS VARCHAR(32),\n     RESIDENTIAL_ADDRESS VARCHAR(255),\n     SOCIAL_SECURITY_NUMBER VARCHAR(32),\n     CONSTRAINT CONSTRAINT_3 PRIMARY KEY (ACCOUNT_HOLDER_ID))\"])\n

UUID inefficient for large data sets

Using random data, like uuid, for indexes can be inefficient especially for larger data sets. There may be data types for each specific database that provide more efficient ways of managing unique ids. For the scope of this project, using a UUID is acceptable.

"},{"location":"projects/banking-on-clojure/database-tables/#define-accounts-table","title":"Define ACCOUNTS table","text":"

Define the SQL statement to create the ACCOUNTS

Each account is associated with an ACCOUNT_HOLDER_ID so that all the accounts belonging to a customer can be easily found.

The current balance holds the value of credit in an account at the time of the last updated date. The current_balance is calculated from the values in the transaction_history and updates to the current_balance will update the last_updated value. This provides a very simplistic mechanism for quickly presenting the value of an account.

(def schema-accounts-table\n  [\"CREATE TABLE PUBLIC.ACCOUNTS(\n     ACCOUNT_ID UUID(16) NOT NULL,\n     ACCOUNT_NUMBER INTEGER NOT NULL AUTO_INCREMENT,\n     ACCOUNT_SORT_CODE VARCHAR(6),\n     ACCOUNT_NAME VARCHAR(32),\n     CURRENT_BALANCE VARCHAR(255),\n     LAST_UPDATED DATE,\n     ACCOUNT_HOLDER_ID VARCHAR(100) NOT NULL,\n     CONSTRAINT ACCOUNTS_PK PRIMARY KEY (ACCOUNT_ID))\"] )\n
"},{"location":"projects/banking-on-clojure/database-tables/#define-transaction_history-table","title":"Define TRANSACTION_HISTORY table","text":"

Define the SQL statement to create the ACCOUNTS

All transactions include a value of the transaction, a reference to explain the purpose of the transaction, a date the transaction occurred, the account the transaction comes from and the account the transaction goes to.

(def schema-transaction-history-table\n  [\"CREATE TABLE PUBLIC.TRANSACTION_HISTORY(\n     TRANSACTION_ID UUID(16) NOT NULL,\n     TRANSACTION_VALUE INTEGER NOT NULL,\n     TRANSACTION_REFERENCE VARCHAR(32),\n     TRANSACTION_DATE DATE,\n     ACCOUNT_FROM INTEGER,\n     ACCOUNT_TO INTEGER,\n     CONSTRAINT TRANSACTION_HISTORY_PK PRIMARY KEY (TRANSACTION_ID))\"])\n

Constraint Naming

Constraints are used to add Primary Keys to database tables. Each constraint needs an identifier which is included in error reporting when there are issues. It is recommended to use meaningful names for identifiers to trace the source of errors and also support maintenance of the overall database design.

"},{"location":"projects/banking-on-clojure/database-tables/#execute-table-schema-in-the-development-database","title":"Execute Table schema in the development database","text":"

Define a function to use the table creation SQL statements and execute them on the given database.

The function uses with-open to create and manage a connection, closing that connection when the function has completed.

(defn create-table\n  \"Establish a connection to the data source and create a table within a transaction.\n  Close the database connection.\n  Arguments:\n  - table-schemas: a vector containing an sql statements to create a table\"\n  [table-schema data-spec]\n\n  (with-open [connection (jdbc/get-connection data-spec)]\n    (jdbc/execute! connection  table-schemas)))\n

with-open - managing resources

with-open ensures that resources get closed and clearly defines the scope of the using the resource.

This helps the developer avoid using Clojure\u2019s lazy sequences in a with-open block. Within the scope of the with-open expression it is important to make sure that the result is eagerly evaluated to avoid accessing the resource after it\u2019s closed, or fall foul of the \"ResultSet closed\" or \"transaction closed\" errors.

Refactor the function to execute all the SQL statements in a transaction, so either all the databases are created or none are.

Using transactions can help prevent databases becoming in an inconsistent state due to only partial completion of a set of SQL statements.

(defn create-tables\n  \"Establish a connection to the data source and create all tables within a transaction.\n  Close the database connection.\n  Arguments:\n  - table-schemas: a vector of sql statements, each creating a table\"\n  [table-schemas data-spec]\n\n  (with-open [connection (jdbc/get-connection data-spec)]\n    (jdbc/with-transaction [transaction connection]\n      (doseq [sql-statement table-schemas]\n        (jdbc/execute! transaction sql-statement) ))))\n

Refactor for connection pools

with-open function can be removed from the create-tables function when using a connection pool, passing the existing connection from the pool to the jdbc/with-transaction function.

Calling the create-tables function will create the database tables in the development database.

The H2 database writes the tables to disk in the banking-on-clojure.mv.db file. Unless the table is dropped, there is no need to evaluate this function again.

"},{"location":"projects/banking-on-clojure/database-tables/#viewing-tables-in-the-database","title":"Viewing tables in the database","text":"
(defn information-tables\n  [data-source]\n  (jdbc/execute!\n    data-source\n    [\"select * from information_schema.tables\"])\n

Create a helper function to show the schema of any particular table. The function takes a table name and using str to combine table name with the rest of the SQL statement.

(defn show-schema\n  [table-name]\n  (jdbc/execute!\n    data-source\n    [(str \"show columns from \" table-name)]))\n

A specific schema can be viewed by calling the show-schema function

(show-schema \"accounts\")\n

Refactor the show-schema function to take the data source as an argument as well as the table name. The function is then usable for development, staging and production data sources (although its not generally advisable to update production database from a REPL in the development environment once its live)

(defn show-schema\n  [data-source table-name]\n  (jdbc/execute! data-source [(str \"show columns from \" table-name)]))\n

The connection is not manged though, so refactor again and add the with-open command to ensure the connection is closed once the function has finished.

(defn show-schema\n  [db-spec table-name]\n  (with-open [connection (jdbc/get-connection db-spec)]\n    (jdbc/execute! connection [(str \"SHOW COLUMNS FROM \" table-name)])))\n
"},{"location":"projects/banking-on-clojure/database-tables/#removing-dropping-database-tables","title":"Removing (dropping) database tables","text":"

A helper function for removing tables from the database, dropping the data and the schema of the table.

drop-table function only differs from the show-schema function in the specific SQL statement it uses.

(defn drop-table\n  [db-spec table-name]\n  (with-open [connection (jdbc/get-datasource db-spec)]\n    (jdbc/execute! connection [(str \"DROP TABLE \" table-name)])))\n

A generic helper function could be used if the SQL statement were also an argument.

"},{"location":"projects/banking-on-clojure/database-tables/#managing-database-tables-from-the-repl","title":"Managing database tables from the REPL","text":"

When designing the database schema it can be useful to iterate quickly around the design. Using a Rich Comment Block to hold expressions to create show and drop tables is an effective way to manage the database schema quickly.

(comment  ;; Managing Schemas\n\n  ;; Create all tables in the development database\n  (create-tables [schema-account-holders-table schema-accounts-table schema-transaction-history-table]\n                 db-specification-dev)\n\n  ;; View application table schema in development database\n  (show-schema data-source-dev \"PUBLIC.ACCOUNT_HOLDERS\")\n  (show-schema data-source-dev \"PUBLIC.ACCOUNTS\")\n  (show-schema data-source-dev \"PUBLIC.TRANSACTION_HISTORY\")\n\n  ;; View database system schema in development database\n  (show-schema data-source-dev \"INFORMATION_SCHEMA.TABLES\")\n\n  ;; Remove tables from the development database\n  (drop-table data-source-dev \"PUBLIC.ACCOUNT_HOLDERS\")\n  (drop-table data-source-dev \"PUBLIC.ACCOUNTS\")\n  (drop-table data-source-dev \"PUBLIC.TRANSACTION_HISTORY\"))\n

Manage database schema with Migratus

Migratus provides an elegant approach to evolving database schema

Common errors

Syntax error in SQL statement can occur if the SQL statement is not correct, the most common cause is a missing comma.

Databases do not always support exactly the same SQL syntax, especially around types and more advanced features. SQL statements may not work exactly the same for each database. Using tools like DBever will generated SQL expressions for specific databases.

"},{"location":"projects/banking-on-clojure/delete-records/","title":"Delete Records in the database","text":"

Using next.jdbc.sql functions provides a Clojure data structures approach, where as next.jdbc/execute! uses specific SQL statement code.

{% tabs clojure=\"next.jdbc.sql functions\", sql=\"next.jdbc/execute!\" %}

{% content \"clojure\" %}

"},{"location":"projects/banking-on-clojure/delete-records/#generic-delete-record-function","title":"Generic delete record function","text":"

Use the generic delete function from the database schema design section

(defn delete-record\n  \"Insert a single record into the database using a managed connection.\n  Arguments:\n  - table - name of database table to be affected\n  - record-data - Clojure data representing a new record\n  - db-spec - database specification to establish a connection\"\n  [db-spec table where-clause]\n  (with-open [connection (jdbc/get-connection db-spec)]\n    (jdbc-sql/delete! connection table where-clause)))\n
"},{"location":"projects/banking-on-clojure/delete-records/#delete-an-existing-account_holder-record","title":"Delete an existing account_holder record","text":"

Call the delete-record function with the development database specification, the table name and a where clause to locate the specific record to delete. This where clause should use a unique value, e.g. the primary key for the table.

  (delete-record db-specification-dev :public.account_holders {:account_holder_id \"0bed6afe-6740-46a1-b924-36ef192eac66\"})\n

If the record deletion is successful then :update-count 1 value is returned

  ;; => #:next.jdbc{:update-count 1}\n
"},{"location":"projects/banking-on-clojure/delete-records/#deleting-an-existing-account-record","title":"Deleting an existing account record","text":"

Update an existing record in the public.accounts table, providing new values for current_balance and last_updated columns.

(delete-record db-specification-dev :public.accounts {:account_number \"1234567890\"})\n
"},{"location":"projects/banking-on-clojure/delete-records/#deleting-an-existing-transaction-record","title":"Deleting an existing transaction record","text":"

Update an existing record in the public.transaction_history table.

(delete-record db-specification-dev :public.transaction_history {:transaction_id  \"8ac89cfc-6874-4ebe-9ee4-59b8c5e971ff\"})\n

{% content \"sql\" %}

"},{"location":"projects/banking-on-clojure/delete-records/#insert-account_holders","title":"Insert account_holders","text":""},{"location":"projects/banking-on-clojure/delete-records/#insert-accounts","title":"Insert accounts","text":""},{"location":"projects/banking-on-clojure/delete-records/#insert-transactions","title":"Insert transactions","text":"

{% endtabs %}

"},{"location":"projects/banking-on-clojure/delete-records/#hintgenerating-example-data-from-clojure-spec","title":"Hint::Generating example data from Clojure Spec","text":"

Clojure Spec: generate mock database data

"},{"location":"projects/banking-on-clojure/deployment-pipeline/","title":"Deployment Pipeline Approach","text":"

Using the Heroku Application platform cloud simplifies the deployment of the Clojure web application.

"},{"location":"projects/banking-on-clojure/deployment-pipeline/#12-factor-approach","title":"12 Factor approach","text":"

Following the 12 factor principles, the deployment is driven by source code to multiple environments.

"},{"location":"projects/banking-on-clojure/deployment-pipeline/#heroku-pipelines","title":"Heroku pipelines","text":"

Using Heroku Pipelines the staging environment is promoted to production rather than being rebuilt

The Heroku dashboard can be used to promote the application into production, once the staging application is signed off.

"},{"location":"projects/banking-on-clojure/deployment-pipeline/#heroku-build-process","title":"Heroku Build process","text":"

The build process starts when commits are pushed to Heroku, either directly or via a continuous integration service (eg. CircleCI).

"},{"location":"projects/banking-on-clojure/deployment-via-ci/","title":"Deployment via Continuous Integration","text":"

Deployment will be via a workflow to the CircleCI configuration that deploys the application to a staging environment on successful completion of running all tests in the project. Once the staging application is approved, the application build can be promoted to production.

"},{"location":"projects/banking-on-clojure/deployment-via-ci/#add-heroku-orb-to-circleci-configuration","title":"Add Heroku orb to CircleCI configuration","text":"

Edit .circleci/config.yml and add the heroku orb and a workflow to Heroku. The workflow has a dependency on the build job, so that will take place first.

The Heroku workflow will build the application from source code using the heroku/deploy-via-git. Only changes pushed to the live branch of the GitHub repository will be used in the Heroku deploy workflow.

version: 2.1  # circleci configuration version\n\norbs:\n  kaocha: lambdaisland/kaocha@0.0.1 # Org settings > Security > uncertified orbs\n  heroku: circleci/heroku@1.2.6 # Invoke the Heroku orb\n\nworkflows:\n  heroku_deploy:\n    jobs:\n      - build\n      - heroku/deploy-via-git: # Use the pre-configured job, deploy-via-git\n          requires:\n            - build\n          filters:\n            branches:\n              only: live\n\njobs:    # basic units of work in a run\n  build: # runs not using Workflows must have a `build` job as entry point\n    working_directory: ~/build # directory where steps will run\n    docker:                                                      # run the steps with Docker\n      - image: circleci/clojure:openjdk-11-tools-deps-1.10.1.727 # image is primary container where `steps` are run\n    environment:            # environment variables for primary container\n      JVM_OPTS: -Xmx3200m   # limit the maximum heap size to prevent out of memory errors\n    steps:             # commands that comprise the `build` job\n      - checkout       # check out source code to working directory\n      - restore_cache: # restores saved cache if checksum hasn't changed since the last run\n          key: banking-on-clojure-webapp-{{ checksum \"deps.edn\" }}\n      - run: clojure -R:test:runner -Spath\n      - save_cache:    # generate and store cache in the .m2 directory using a key template\n          paths:\n            - ~/.m2\n            - ~/.gitlibs\n          key: banking-on-clojure-webapp-{{ checksum \"deps.edn\" }}\n      - run: bin/kaocha --reporter kaocha.report/documentation --no-randomize --no-color --plugin kaocha.plugin.alpha/spec-test-check\n
"},{"location":"projects/banking-on-clojure/deployment-via-ci/#add-depstar-to-build-an-uberjar","title":"Add depstar to build an uberjar","text":"

The depstar tool creates a Java archive (jar) package of the application. The deps.edn configuration in the root of the project already contains an uberjar alias for this tool.

Check the project builds the uberjar locally:

clojure -X:project/uberjar\n

This will be the same command used in the build script

"},{"location":"projects/banking-on-clojure/deployment-via-ci/#create-a-custom-build-behaviour","title":"Create a custom build behaviour","text":"

Heroku build scripts use Leiningen by default. Configure Heroku to build with Clojure Tools, create a custom build file which will run instead of Leiningen.

Create a file called bin/build script in the root of the project

#!/usr/bin/env bash\nclojure -X:project/uberjar\n

Create an empty project.clj file so that Heroku recognized the project as Clojure.

"},{"location":"projects/banking-on-clojure/deployment-via-ci/#define-how-to-run-the-application","title":"Define how to run the application","text":"

Create a Procfile file in the root of the project directory containing the command to run the application.

Use the $PORT as an argument to the command. Heroku automatically assigns a port number for an application to listen upon when creating a contain in which the application will run. This port number is set using the PORT environment variable and is available to the application on startup. Using the PORT environment variable ensures the Clojure application will receive requests.

web: java -jar banking-on-clojure.jar $PORT\n
"},{"location":"projects/banking-on-clojure/deployment-via-ci/#specifying-a-java-version","title":"Specifying a Java version","text":"

Create a system.properties and specify the Java version to use for the application. Java 1.8 is the default version use on Heroku, however, our development environment is Java 11, so add a property to set the Java runtime to version 11.

java.runtime.version=17\n
"},{"location":"projects/banking-on-clojure/deployment-via-ci/#circleci-environment-variables","title":"CircleCI Environment Variables","text":"

Open the CircleCI and select project settings > Environment Variables

Add environment variables to define where the Heroku application can be found and a token to provide access.

Environment Variable Value HEROKU_API_KEY name of the application created on Heroku HEROKU_APP_NAME API key found in Account Settings > API Key"},{"location":"projects/banking-on-clojure/deployment-via-ci/#heroku-pipeline-configuration","title":"Heroku Pipeline configuration","text":"

Login to the Heroku dashboard and create a new pipeline called banking-on-clojure-webapp

In the Heroku dashboard, open the application Settings and add a Config Vars using the name CLOJURE_CLI_VERSION with a value of 1.10.1.727

Using Heroku Pipelines the staging environment is promoted to production rather than being rebuilt

The Heroku dashboard can be used to promote the application into production, once the staging application is signed off.

"},{"location":"projects/banking-on-clojure/deployment-via-ci/#push-changes-to-trigger-build","title":"Push changes to trigger build","text":"

Commit the changes and push them to the GitHub repository.

git push heroku live:main\n

Heroku only deploys code pushed to the main (or master) branch of the remote. Pushing code to another branch of the heroku remote has no effect. Using the live:local form will push the local live branch to the remote main branch on Heroku.

This triggers a build by CircleCI. The build downloads the dependencies and runs the unit tests. If the tests pass, then the Heroku deploy workflow starts.

The two stages can be seen in the dashboard as the pipeline runs.

Now visit the deployed Heroku application to see it in action.

"},{"location":"projects/banking-on-clojure/deployment-via-ci/#troubleshooting","title":"Troubleshooting","text":"

If there are issues, then use the Heroku toolbelt to look at the logs. In a command line terminal, issue the login command which opens a web browser to login to Heroku. Once logged in, run the heroku logs command to view the latest logs

heroku login\n\nheroku logs --app banking-on-clojure\n

The logs can also be viewed live, as the application is being deployed by including the --tail option when running the heroku logs command in a terminal

heroku logs --app banking-on-clojure --tail\n

The example Heroku logs show that the banking-on-clojure is using the default port number if non is supplied as an argument, rather than Heroku assigned port. Heroku therefore considers the application as unresponsive and sets it status to crashed, tearing down the container the application is running in.

These logs were generated before adding the $PORT to the command in the Procfile.

"},{"location":"projects/banking-on-clojure/deployment-via-ci/#no-forced-pushes","title":"No forced pushes","text":"

Heroku doesn't like force Git pushes coming via CircleCI.

To get around this, either don't do force pushes to GitHub, or add the Heroku repository for the project as a remote to local git repository.

Heroku repository details in heroku dashboard Settings under App Information

Changes can now be pushed, ideally using force-with-lease to Heroku repository.

"},{"location":"projects/banking-on-clojure/deployment-via-ci/#stopping-the-application","title":"Stopping the application","text":"

An application can be run for free on Heroku with the monthly free credits provided. However, to make the most out of these free credits then applications not in use should be shut down

Run the following command in the root of the Clojure project.

heroku ps:stop banking-on-clojure\n
"},{"location":"projects/banking-on-clojure/development-database/","title":"Provision the development database","text":"

To keep the development environment self-contained the H2 in-memory database will be used for development of the application. Postgresql database will be used for testing and live environments hosted in the Cloud.

"},{"location":"projects/banking-on-clojure/development-database/#add-h2-database-library-dependency","title":"Add H2 database library dependency","text":"

Edit the deps.edn file in the root of the project directory. In the :deps hash-map, add next.jdbc library as a main dependency.

As the H2 database is only used for development create a :dev alias include an :extra-deps section for the H2 driver

{:deps\n {org.clojure/clojure        {:mvn/version \"1.10.1\"}\n  org.seancorfield/next.jdbc {:mvn/version \"1.1.569\"}}}\n\n{:aliases\n  {:dev\n   {:extra-deps {com.h2database/h2 {:mvn/version \"1.4.200\"}}}}}\n
"},{"location":"projects/banking-on-clojure/development-database/#create-a-namespace-for-database-access","title":"Create a namespace for database access","text":"

Create a new file in the project called src/practicalli/database-access.clj which will contain the code for accessing the database.

Require the next.jdbc namespace using the jdbc alias.

(ns practicalli.database-access\n  (:require [next.jdbc :as jdbc]))\n

next.jdbc will use the database specification to look up the driver namespace for the specific database. Therefore the database driver namespaces do not need to be explicitly required in the namepace.

"},{"location":"projects/banking-on-clojure/development-database/#specifying-the-database-and-connection","title":"Specifying the database and connection","text":"

H2 in-memory database is used as a self-contained database, providing a simple way to start evaluating the schema and queries as they are designed.

Use next.jdbc library to define a database specification, represented as a map. For the H2 database only the database type and database name are required. No roles or credentials are used to access the database as it is only running locally.

Use the database specification to create a connection

;; Database specification and connection\n\n;; Development environment\n;; H2 in-memory database\n(def db-specification-dev {:dbtype \"h2\" :dbname \"banking-on-clojure\"})\n\n;; Database connection\n(def data-source-dev (jdbc/get-datasource db-specification-dev))\n

The database specification is used to create a database connection. A general name can be used here as only one database will be used for one environment.

Aero for multiple environment configuration

juxt/aero is a library for managing configurations across multiple environments in a single EDN file. aero can be used to hold the details of each database specification for every environment (dev, staging, live).

(read-config (clojure.java.io/resource \"config.edn\")) with the configuration file in the resources directory of the classpath. This is accessible from the Jar and the REPL.

"},{"location":"projects/banking-on-clojure/development-database/#using-connections-effectively","title":"Using connections effectively","text":"

Use the with-open function to manage the database connections and ensure the connects are closed after the sql queries complete.

(with-open [connection (jdbc/get-connection data-source-dev)]\n  (jdbc/execute! connection [\"SQL statement\"]))\n

When multiple SQL queries should be run together, the with-open function enables reuse of the connection and ensure the connection is cleaned up once the SQL statements are complete.

(with-open [connection (jdbc/get-connection data-source-dev)]\n  (jdbc/execute! connection [\"SQL statement\"])\n  (reduce my-fn init-value (jdbc/plan connection [\"SQL statement\"]))\n  (jdbc/execute! connection [\"SQL statement\"]))\n

Close JDBC Connections

next.jdbc uses raw Java JDBC types so it is important to close connections to avoid issues.

"},{"location":"projects/banking-on-clojure/generate-data-from-specs/","title":"Generate Data Using Clojure Spec","text":"

Test data to populate the database can be generated using the specifications previously defined using Clojure Spec.

"},{"location":"projects/banking-on-clojure/honeysql/","title":"HoneySQL","text":"

HoneySQL is a library for writing SQL as Clojure data structures to programmatically query databases (develop and runtime) without string bashing.

"},{"location":"projects/banking-on-clojure/honeysql/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"projects/banking-on-clojure/instrument-next-jdbc-functions/","title":"clojure.spec: Instrument next.jdbc functions","text":"

Clojure specifications are available for all next.jdbc functions contained in namespaces next.jdbc, next.jdbc.connection, next.jdbc.prepare, and next.jdbc.sql.

Instrumenting the functions with their specifications will check the arguments passed to a function conform to the appropriate specification. If the arguments conform, the next.jdbc function will evaluate with those arguments. If the arguments do not conform, then an error is returned.

Instrumenting specifications provided additional details when errors occur, helping diagnose the issue quickly.

"},{"location":"projects/banking-on-clojure/instrument-next-jdbc-functions/#require-the-nextjdbc-specifications","title":"Require the next.jdbc specifications","text":"

Require the next.jdbc.specs namespace in the project, typically in the namespace where next.jdbc is also required.

(ns practicalli.database-access\n  (:require\n    [next.jdbc :as jdbc]\n    [next.jdbc.specs :as jdbc-spec]))\n

In a Rich Comment block, call the instrument function from next.jdbc.specs namespace. This will instrument the specifications for all functions across all the next.jdbc namespaces.

(comment\n\n  (jdbc-spec/instrument)\n)\n
Instrumented functions are typically used during development, not in staging or production. Only calling instrument manually from a rich comment block ensures the developer controls when functions are instrumented.

"},{"location":"projects/banking-on-clojure/instrument-next-jdbc-functions/#runtime-checking","title":"Runtime checking","text":"

With instrumentation enabled, any calls to next.jdbc functions will have the arguments checked to ensure they conform to the specification.

For example, the instrumented execute! function will generate an error if passed an SQL statement as a string, rather than a vector containing a string.

(jdbc/execute! data-source \"SELECT * FROM account_holders\")\n\nCall to #'next.jdbc/execute! did not conform to spec.\n

The :problems section of the instrumented function error includes the :path [:sql :sql-params] and :pred vector? for the :val \"SELECT * FROM account_holders\".

Without the instrumented specification, assistance, the less helpful error message ClassCastException is the only assistance when debugging the issue.

Example from Banking on Clojure

"},{"location":"projects/banking-on-clojure/instrument-next-jdbc-functions/#turning-off-instrumentation-for-nextjdbc","title":"Turning off instrumentation for next.jdbc","text":"

unstrument function removes the instrumentation from the functions. Typically this is called from a rich comment block too, as its not common to run instrumented functions outside of the development environment.

(comment\n\n  (jdbc-spec/instrument)\n  (jdbc-spec/unstrument)\n\n)\n
"},{"location":"projects/banking-on-clojure/namespace-design/","title":"Namespace design","text":"

A common approach to namespace design is to start with the main namespace for the application and migrate code to new namespaces as the codebase grows.

"},{"location":"projects/banking-on-clojure/namespace-design/#basic-principles","title":"Basic Principles","text":"

Basic principles of namespace design include

  • focus namespaces on specific logical areas of the application
  • avoid circular references between namespaces (i.e. two namespaces require each other)
  • abstract code into namespaces to avoid the uber-namespace (unless the application fits into ~100 lines of code)
  • require the minimum number of namespaces
  • use meaningful names for namespace aliases (if naming is hard, think again about splitting a namespace)
  • use comment sections to separate code into logical groupings as its developed, highlighting potential sections of code that could split into its own namespace.
"},{"location":"projects/banking-on-clojure/namespace-design/#example-web-application-namespace-design","title":"Example web application namespace design","text":"

A general design that forms the basis of many web application projects

"},{"location":"projects/banking-on-clojure/namespace-design/#main-application-namespace","title":"Main application namespace","text":"

The main application namespace is typically used for code that manages the system, for example starting the application server, database, etc. These services are often managed by component lifecycle services (mount, integrant, component).

Routing is usually a part of the main application namespace, especially when there are a modest number of routes and a routing library such as compojure is used. If routing becomes more extensive, then a separate routing namespace is warranted.

"},{"location":"projects/banking-on-clojure/namespace-design/#handlers-and-custom-middleware","title":"Handlers and custom middleware","text":"

Handlers define the business logic, data and presentation that turns requests into responses. Start with a single namespace for handlers and segregate if the complexity grows sufficiently.

Middleware used directly with handlers is required in the handler namespace.

Middleware for the overall system may appear in the main application namespace, to wrap the application instance.

"},{"location":"projects/banking-on-clojure/namespace-design/#ui-pages-and-templates","title":"UI pages and templates","text":"

Avoid adding complexity to the handlers by moving common web page / html generation code to its own namespace.

A single namespace provides a focused view for refactoring presentation code into templates and generators.

"},{"location":"projects/banking-on-clojure/namespace-design/#data-queries","title":"Data queries","text":"

A namespace to design all the queries for a data source, which could be database, api's, file systems, etc.

SQL queries to relational database are defined here.

"},{"location":"projects/banking-on-clojure/namespace-design/#data-sources","title":"Data sources","text":"

Details of data sources, from databases, api's or any sources of information to be processed.

"},{"location":"projects/banking-on-clojure/production-database/","title":"Production Database - Heroku Postgres","text":"

Heroku provides a on-demand PostgreSQL service with very good support tooling. The Hobby plan is free to use with a free Heroku account (no credit card needed).

PostgreSQL provides a production grade relational database with support for JSON an other common data types. As its such a feature rich database

PostgreSQL instance runs outside the application code (unlike H2 database).

"},{"location":"projects/banking-on-clojure/production-database/#provision-a-database","title":"Provision a database","text":"

Login to Heroku using your free account.

Two Heroku apps have already been created for the banking on Clojure project pipeline, staging and live, so a database should be provisioned for both apps.

Select the staging app from the pipeline dashboard.

In the Overview section, click Configure Add-ons

Start typing Postgres in the Add-ons text box to see the matching add-ons available. Select Heroku Postgres.

Select the Hobby plan in the pop-up window. The Hobby plan is free and limited to 10,000 rows. The plan can be upgraded ones the app starts making money (or funding is raised).

The Postgres database is immediately provisioned and available for use.

"},{"location":"projects/banking-on-clojure/production-database/#database-configuration-on-heroku","title":"Database Configuration on Heroku","text":"

Provisioning an Heroku postgres database adds a DATABASE_URL Config Var (Heroku Environment Variable) to the application the database is attached to.

In the Heroku dashboard, view the Settings and select Show Config Vars

Click on the pencil icon to see the full connection string, which takes the following form:

postgres://username:password@host:port/database-name\n

This is not a correct JDBC connection string, but it can be used to generate one.

"},{"location":"projects/banking-on-clojure/production-database/#generate-the-jdbc-connection","title":"Generate the JDBC connection","text":"

Use the Heroku CLI tool to get the JDBC connection string for the database.

For the Banking on Clojure app, the following

heroku run echo \\$JDBC_DATABASE_URL --app banking-on-clojure-staging\n

This returns the correct JDBC connection string in the form:

\"jdbc:postgresql://<hostname>:port/<database-name>?user=<username>&password=<password>&sslmode=require\"\n

This jdbc connection string is generated from the DATABASE_URL config var that is added to the heroku app when a database is provisioned.

There are several applications attached to the Git repository, so its required to specify which application to run. The run command runs a container for the app, a Linux system that has the database attached. Once the echo command is complete the container is shut down and discarded automatically.

"},{"location":"projects/banking-on-clojure/production-database/#viewing-the-database-details-on-heroku-dashboard","title":"Viewing the database details on Heroku dashboard","text":"

This will switch over to the data.heroku.com website, so you may be prompted to login again.

"},{"location":"projects/banking-on-clojure/production-database/#adding-postgresql-driver-to-clojure-project","title":"Adding Postgresql driver to Clojure project","text":"

Add the latest postgresql jdbc driver to the deps.edn file in the banking on clojure project

 :deps\n {org.clojure/clojure {:mvn/version \"1.10.1\"}\n\n  ;; Web Application\n  http-kit        {:mvn/version \"2.3.0\"}\n  ring/ring-core  {:mvn/version \"1.8.1\"}\n  ring/ring-devel {:mvn/version \"1.8.1\"}\n  compojure       {:mvn/version \"1.6.1\"}\n  hiccup          {:mvn/version \"2.0.0-alpha2\"}\n\n  ;; Database\n  org.seancorfield/next.jdbc    {:mvn/version \"1.1.569\"}\n  com.h2database/h2         {:mvn/version \"1.4.200\"}\n  org.postgresql/postgresql {:mvn/version \"42.2.16\"}}\n

The Postgresql jdbc driver library will be used by next.jdbc

"},{"location":"projects/banking-on-clojure/production-database/#creating-a-staging-data-source","title":"Creating a staging data source","text":"

Add a JDBC_DATABASE_URL environment variable to hold the JDBC connection string to the Heroku database

(def data-source-postgresql\n    (jdbc/get-datasource (System/getenv \"JDBC_DATABASE_URL\")) )\n

Now the same SQL queries created for the H2 database can be tested on with PostgreSQL.

"},{"location":"projects/banking-on-clojure/production-database/#testing-queries-with-postgresql","title":"Testing queries with PostgreSQL","text":"

In practice is seems there are noticeable differences between H2 and PostgreSQL, especially in terms of schema definitions.

For example, to create a table the table namespace should be supplied, in this case public. The table creation syntax also as an IF clause, so if the database table already exists then the SQL statement does not try to create it and cause an error.

(jdbc/execute!\n    data-source-postgresql\n    [\"CREATE TABLE IF NOT EXISTS public.account_holder (\n      user_id serial PRIMARY KEY,\n      username VARCHAR ( 50 ) UNIQUE NOT NULL,\n      password VARCHAR ( 50 ) NOT NULL,\n      email VARCHAR ( 255 ) UNIQUE NOT NULL,\n      created_on TIMESTAMP NOT NULL,\n      last_login TIMESTAMP\"])\n

The PostgreSQL syntax for creating tables is:

  CREATE TABLE [IF NOT EXISTS] table_name (\n  column1 datatype(length) column_constraint,\n  column2 datatype(length) column_constraint,\n  column3 datatype(length) column_constraint,\n  table_constraints);\n
"},{"location":"projects/banking-on-clojure/production-database/#hintuse-dbeaver-to-generate-sql","title":"Hint::Use DBeaver to generate SQL","text":"

DBeaver is a free and comprehensive database tool that will generate SQL statements from database designs.

"},{"location":"projects/banking-on-clojure/production-database/#resources","title":"Resources","text":"
  • Heroku Postgres Credentials
  • Heroku: Database Connection Pooling with Clojure
"},{"location":"projects/banking-on-clojure/read-records/","title":"Read Database Records","text":"

Using next.jdbc.sql functions provides a Clojure data structures approach, where as next.jdbc/execute! uses specific SQL statement code.

"},{"location":"projects/banking-on-clojure/read-records/#generic-read-record-function","title":"Generic read record function","text":"

Use the generic create function from the database schema design section

(defn read-record\n  \"Insert a single record into the database using a managed connection.\n  Arguments:\n  - table - name of database table to be affected\n  - record-data - Clojure data representing a new record\n  - db-spec - database specification to establish a connection\"\n  [db-spec sql-query]\n  (with-open [connection (jdbc/get-connection db-spec)]\n    (jdbc-sql/query connection sql-query)))\n
"},{"location":"projects/banking-on-clojure/read-records/#read-account_holder-records","title":"Read account_holder records","text":"

Call the read-record function with the development database specification and a Clojure vector containing a string of the SQL select statement.

Return all the records from a specific table

  (read-record db-specification-dev [\"select * from public.account_holders\"])\n

Return records that match a specific where clause

  (read-record db-specification-dev [\"select * from public.account_holders where first_name = ?\" \"Rachel\"])\n
"},{"location":"projects/banking-on-clojure/read-records/#read-account-records","title":"Read account records","text":"

Create a new record in the public.accounts table.

Return all the records from a specific table

  (read-record db-specification-dev [\"select * from public.accounts\"])\n

Return records that match a specific where clause

  (read-record db-specification-dev [\"select * from public.accounts where account_number = ?\" \"1234567890\"])\n
"},{"location":"projects/banking-on-clojure/read-records/#read-transaction-history-records","title":"Read transaction history records","text":"

Create a record in the public.transaction_history table.

  (read-record db-specification-dev [\"select * from public.transaction_history\"])\n

Return records that match a specific where clause

  (read-record db-specification-dev [\"select * from public.transaction_history where transaction_date = ?\" \"2020-09-11\"])\n

Generating example data from Clojure Spec

Clojure Spec: generate mock database data

"},{"location":"projects/banking-on-clojure/refactor-handler/","title":"Refactor to handlers namespace","text":"

Create a new namespace called practicalli.banking-on-clojure.handler which will contain handler functions for the routes to be defined in the application.

Additional libraries will be used to create the responses, which will only be required in the new namespace.

Create a new file called src/practicalli/banking_on_clojure/handler.clj

Move the [ring.util.response :refer [response]] require and the handler function from src/practicalli/banking_on_clojure/service.clj to src/practicalli/banking_on_clojure/handler.clj

src/practicalli/banking_on_clojure/handler.clj
(ns practicalli.banking-on-clojure.handler\n  \"Handler functions to satisfy requests to the service\"\n  (:require\n    [ring.util.response :refer [response]]))\n
src/practicalli/banking_on_clojure/handler.clj
(defn welcome-page\n  \"Main page layout for the service\"\n  [request]\n  (response \"Banking on Clojure\"))\n

Require the practicalli.banking-on-clojure.handler namespace in practicalli.banking-on-clojure namespace, using the alias handler

src/practicalli/banking_on_clojure/service.clj
(ns practicalli.banking-on-clojure\n  (:gen-class)\n  (:require [org.httpkit.server :as app-server]\n            [compojure.core :refer [defroutes GET]]\n            [ring.util.response :refer [response]]\n            [practicalli.handler :as handler]))\n

Update the request routing code to use the new alias for handlers

src/practicalli/banking_on_clojure/service.clj
(defroutes app\n  (GET \"/\" [] handler/welcome-page))\n

Restart the server to pick up the changes

src/practicalli/banking_on_clojure/service.clj
(app-server-restart \"8888\")\n

Check the server is still working by visiting http://localhost:8888/

"},{"location":"projects/banking-on-clojure/spec-generative-testing/","title":"Generative testing","text":"

Specifications define the shape of data used for the application. The specifications are defined across two namespaces, general data specifications in practicalli.specifications and banking specific specs in the practicalli.specifications-banking namespace.

Basic customer details

(spec/def ::first-name string?)\n(spec/def ::last-name string?)\n(spec/def ::email-address string?)\n\n;; residential address values\n(spec/def ::house-name-number (spec/or :string string?\n                                       :number int?))\n(spec/def ::street-name string?)\n(spec/def ::post-code string?)\n(spec/def ::county string?)\n

countries of the world as a set, containing a string for each country defined in the practicalli.specifications namespace

(spec/def ::country :practicalli.specifications/countries-of-the-world)\n
(spec/def ::residential-address (spec/keys :req [::house-name-number ::street-name ::post-code]\n                                           :opt [::county ::country]))\n
(spec/def ::social-security-id-uk string?)\n(spec/def ::social-security-id-usa string?)\n\n(spec/def ::social-security-id (spec/or ::social-security-id-uk\n                                        ::social-security-id-usa))\n
;; composite customer details specification\n(spec/def ::customer-details\n  (spec/keys\n    :req [::first-name ::last-name ::email-address ::residential-address ::social-security-id]))\n
"},{"location":"projects/banking-on-clojure/spec-generative-testing/#banking-data-specifications","title":"Banking data specifications","text":"

The specifications-banking sets the overall context for the specifications defined in the namespace.

account-id is a unique identification across all accounts in the bank. The type of value used is a universally unique identifier (UUID) is a 128-bit number used to identify information in computer systems. Clojure uses a #uuid tag literal

(spec/def ::account-id uuid?)\n\n;; Account holder - composite specification\n(spec/def ::account-holder\n  (spec/keys\n    :req [::account-id\n          ::first-name\n          ::last-name\n          ::email-address\n          ::residential-address\n          ::social-security-id]))\n
"},{"location":"projects/banking-on-clojure/ui-handler-functions/","title":"UI Handler Functions","text":"

Taking an outside-in approach, the main parts of the website user interface will be created using Hiccup and Bulma CSS library. Mock data will be used then wired up to the database as that is designed.

Inside-out development

When writing a web service for an existing database design, taking an inside-out approach may be more effective.

An inside-out approach would include * generating Clojure Specifications for values from the database schema * define database access functions * defin handlers to expose values from the database, validated via specifications * define routes to interact with the values along business functions * create UI elements for use with the handlers to make a functioning and responsive application

The following request handlers will be created for the banking-on-clojure application

  • welcome-page
  • register-account-holder
  • accounts-overview-page
  • account-history
  • money-transfer
  • money-payment

All handlers are very similar to the welcome page, which highlights that some common template should be created for all handlers to use.

The accounts-overview-page is the main page for the application, so will be designed in more detail.

"},{"location":"projects/banking-on-clojure/ui-handler-functions/#account-overview-page-handler","title":"account-overview-page handler","text":"

This is the page customers will view by default when logging in.

src/practicalli/banking_on_clojure/handlers.clj
(defn accounts-overview-page\n  \"Overview of each bank account owned by the current customer.\n\n  Using Bulma media object style\n  https://bulma.io/documentation/layout/media-object/\n\n  Request hash-map is not currently used\"\n\n  [request]\n  (response\n    (html5\n      {:lang \"en\"}\n      [:head\n       (include-css \"https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css\")]\n      [:body\n       [:section {:class \"hero is-info\"}\n        [:div {:class \"hero-body\"}\n         [:div {:class \"container\"}\n          [:h1 {:class \"title\"} \"Banking on Clojure\"]\n          [:p {:class \"subtitle\"}\n           \"Making your money immutable\"]]]]\n\n       [:section {:class \"section\"}\n        (bank-account-media-object {:account-type  \"Current Account\" :account-number    \"123456789\"\n                                    :account-value \"1,234\"           :account-sort-code \"01-02-01\"})\n\n        (bank-account-media-object {:account-type  \"Savings Account\" :account-number    \"123454321\"\n                                    :account-value \"2,000\"           :account-sort-code \"01-02-01\"})\n\n        (bank-account-media-object {:account-type  \"Tax Free Savings Account\" :account-number    \"123454321\"\n                                    :account-value \"20,000\"                   :account-sort-code \"01-02-01\"})\n\n        (bank-account-media-object {:account-type  \"Mortgage Account\" :account-number    \"98r9e8r79wr87e9232\"\n                                    :account-value \"354,000\"          :account-sort-code \"01-02-01\"})\n\n        ]])))\n

This handler uses a helper function to reduce the amount of hiccup code.

src/practicalli/banking_on_clojure/handlers.clj
(defn bank-account-media-object\n  [account-details]\n  [:article {:class \"media\"}\n   [:figure {:class \"media-left\"}\n    [:p {:class \"image is-64x64\"}\n     [:img {:src \"https://raw.githubusercontent.com/jr0cket/developer-guides/master/clojure/clojure-bank-coin.png\"}]]]\n   [:div {:class \"media-content\"}\n    [:div {:class \"content\"}\n     [:h3 {:class \"subtitle\"}\n      (str (:account-type account-details) \" : &lambda;\" (:account-value account-details))]]\n\n    [:div {:class \"field is-grouped\"}\n     [:div {:class \"control\"}\n      [:div {:class \"tags has-addons\"}\n       [:span {:class \"tag\"} \"Account number\"]\n       [:span {:class \"tag is-success is-light\"} (:account-number account-details)]]]\n\n     [:div {:class \"tags has-addons\"}\n      [:span {:class \"tag\"} \"Sort Code\"]\n      [:span {:class \"tag is-success is-light\"} (:account-sort-code account-details)]]]]\n\n   [:div {:class \"media-right\"}\n    (link-to {:class \"button is-primary\"} \"/transfer\" \"Transfer\")\n    (link-to {:class \"button is-info\"} \"/payment\" \"Payment\")]])\n
"},{"location":"projects/banking-on-clojure/unit-testing-the-database/","title":"Unit Testing with the Database","text":""},{"location":"projects/banking-on-clojure/unit-testing-the-database/#unit-testing-using-clojure-spec","title":"Unit testing using clojure spec","text":"

Require the clojure.spec namespaces * [clojure.spec.alpha :as spec] for the core spec functions, including gen for specification generators * [clojure.spec.gen.alpha :as spec-gen] for generate and sample functions to generate values from specifications * [clojure.spec.test.alpha :as spec-test] for running check on instrumented function definitions * [practicalli.specifications-banking] for the banking related specifications

(ns practicalli.database-access-test\n  (:require\n   ;; Unit testing\n   [clojure.test :refer [deftest is testing]]\n\n   ;; Clojure Specifications\n   [clojure.spec.alpha :as spec]\n   [clojure.spec.test.alpha :as spec-test]\n   [clojure.spec.gen.alpha :as spec-gen]\n   [practicalli.specifications-banking]\n\n   ;; System under test\n   [practicalli.database-access :as SUT])\n  )\n

A simpler test to check that a map of generated values is returned when calling new-account-holder

(deftest new-account-holder-test\n  (testing \"Registered account holder is valid specification\"\n    (is (map? (SUT/new-account-holder\n                (spec-gen/generate\n                  (spec/gen :practicalli.specifications-banking/customer-details)))))))\n

This test alone will fail in a CI environment as the application does not create the database tables automatically

"},{"location":"projects/banking-on-clojure/unit-testing-the-database/#in-a-continuous-integration-environment","title":"In a continuous integration environment","text":"

A Continuous Integration environment will be empty to start with, so when using an embedded database the database and database tables need to be created each time the tests run.

Creating the tables each time the tests are run could lead to errors if the database already has those tables defined

Add IF NOT EXISTS to the CREATE TABLE SQL statement so that the create-tables! function returns nil rather than an SQL error.

(def schema-account-holders-table\n  [\"CREATE TABLE IF NOT EXISTS PUBLIC.ACCOUNT_HOLDERS(\n     ACCOUNT_HOLDER_ID UUID DEFAULT RANDOM_UUID() NOT NULL,\n     FIRST_NAME VARCHAR(32),\n     LAST_NAME VARCHAR(32),\n     EMAIL_ADDRESS VARCHAR(32) NOT NULL,\n     RESIDENTIAL_ADDRESS VARCHAR(255),\n     SOCIAL_SECURITY_NUMBER VARCHAR(32),\n     CONSTRAINT ACCOUNT_HOLDERS_PK PRIMARY KEY (ACCOUNT_HOLDER_ID))\"])\n

Longer term: Run migratus scripts to establish the database schema each time.

"},{"location":"projects/banking-on-clojure/unit-tests/","title":"Unit tests","text":"

In Clojure web applications the handler functions are the main focus of the unit tests.

Create the file test/practicalli/request-handler-test.clj to contain the unit tests.

Define the request-handler-test namespace, including other namespaces that should be required.

The ring.mock.request library is added to simulate request calls to the handlers.

(ns practicalli.request-handler-test\n  (:require [practicalli.request-handler :as SUT]\n            [clojure.test :refer [deftest is testing]]\n            [ring.mock.request :as mock]))\n

Add unit tests for each handler that will be created in practicalli.request-handler.

A simple starting point for tests is to check the correct HTTP status code is being returned.

ring.mock.request library contains a request function that will generate a mock request hash-map from a given HTTP protocol (:get :post etc.) and a address.

(deftest welcome-page-test\n  (testing \"Testing elements on the welcome page\"\n    (is (= 200\n           (:status (SUT/welcome-page (mock/request :get \"/\")))))))\n
"},{"location":"projects/banking-on-clojure/unit-tests/#ensuring-a-status-code-in-handlers","title":"Ensuring a status code in handlers","text":"

The ring.util.response namespace is added to the practicalli.request-handlers namespace. This provide the response function which wraps the body (i.e. web page content) with a correctly formed response hash-map.

(response \"Web page content to be added to the response hash-map\")\n

The practicalli.request-handlers namespace definition

(ns practicalli.request-handler\n  (:require [ring.util.response :refer [response]]\n            [hiccup.core :refer [html]]\n            [hiccup.page :refer [html5 include-js include-css]]\n            [hiccup.element :refer [link-to]]))\n

The welcome page (GET \"/\") content is defined with hiccup code to generate a HTML page (html5). This content is wrapped by the response function to return a response hash-map.

(defn welcome-page\n  [request]\n  (response\n    (html5\n      {:lang \"en\"}\n      [:head\n       (include-css \"https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css\")]\n      [:body\n       [:section {:class \"hero is-info\"}\n        [:div {:class \"hero-body\"}\n         [:div {:class \"container\"}\n          [:h1 {:class \"title\"} \"Banking on Clojure\"]\n          [:p {:class \"subtitle\"}\n           \"Making your money immutable\"]]]]\n\n       [:section {:class \"section\"}\n        [:div {:class \"container\"}\n         (link-to {:class \"button is-primary\"} \"/accounts\"    \"Login\")\n         (link-to {:class \"button is-danger\"}  \"/register\" \"Register\")\n         [:p {:class \"content\"}\n          \"Manage your money without unexpected side-effects using a simple made easy banking service\"]\n         [:img {:src \"https://raw.githubusercontent.com/jr0cket/developer-guides/master/clojure/clojure-piggy-bank.png\"}]]]])))\n
"},{"location":"projects/banking-on-clojure/unit-tests/#using-kaocha-to-run-tests","title":"Using Kaocha to run tests","text":"

Kaocha was added as part of the continuous integration configuration as a local binary. This kaocha binary can be called to run the tests and check that all the handlers are returning the right status code.

bin/kaocha\n

kaocha should return the results of running all unit tests in the project.

If a handler does not return the correct status code, then kaocha will highlight the error in the unit test results.

kaocha will show a summary of the results when all the tests are successful.

"},{"location":"projects/banking-on-clojure/update-records/","title":"Update Records in the database","text":"

Several options were explored when designing database query functions. Using next.jdbc.sql functions provides a Clojure data structures approach, where as next.jdbc/execute! uses specific SQL statement code.

Take the SQL approach if generating SQL statements directly.

Take the Clojure approach if to generate SQL statements from Clojure data structures.

"},{"location":"projects/banking-on-clojure/update-records/#generic-update-record-function","title":"Generic update record function","text":"

Use the generic create function from the database schema design section

(defn update-record\n  \"Insert a single record into the database using a managed connection.\n  Arguments:\n  - table - name of database table to be affected\n  - record-data - Clojure data representing a new record\n  - db-spec - database specification to establish a connection\n  - where-clause - column and value to identify a record to update\"\n  [db-spec table record-data where-clause]\n  (with-open [connection (jdbc/get-connection db-spec)]\n    (jdbc-sql/update! connection table record-data where-clause)))\n
"},{"location":"projects/banking-on-clojure/update-records/#update-an-existing-account_holder-record","title":"Update an existing account_holder record","text":"

Call the update-record function with the development database specification, the account holder table name and a Clojure hash-map of the record data.

Each key in the map represents a column name and the value associated with the key is the value to be inserted in the record for its column.

  (update-record db-specification-dev\n                 :public.account_holders\n                 {:EMAIL_ADDRESS \"rachel+update@rockketpack.org\"}\n                 {:account_holder_id \"f6d6c3ba-c5cc-49de-8c85-21904f8c5b4d\"})\n

If the update is successful then :update-count 1 value is returned

  ;; => #:next.jdbc{:update-count 1}\n
"},{"location":"projects/banking-on-clojure/update-records/#update-an-existing-account-record","title":"Update an existing account record","text":"

Update an existing record in the public.accounts table, providing new values for current_balance and last_updated columns.

(update-record db-specification-dev\n               \"public.accounts\"\n               {:current_balance   242\n                :last_updated      \"2020-09-12\"}\n               {:account_number \"1234567890\"})\n
"},{"location":"projects/banking-on-clojure/update-records/#update-an-existing-transaction-record","title":"Update an existing transaction record","text":"

Update an existing record in the public.transaction_history table.

(update-record db-specification-dev\n               \"public.transaction_history\"\n               {:transaction_reference \"Salary bonus\"\n                :transaction_date      \"2020-09-12\"}\n               {:transaction_id  \"8ac89cfc-6874-4ebe-9ee4-59b8c5e971ff\"})\n

Generating example data from Clojure Spec

Clojure Spec: generate mock database data

"},{"location":"projects/game-scoreboard-api/","title":"Game Scoreboard API","text":"

Building an API from scratch using

  • Reitit
  • muuntaja
  • reitit-ring middleware
  • Integrant REPL - reloaded workflow
  • Integrant - runtime component lifecycle
  • mulog - event logging
  • Auth0 - authentication and authorisation
"},{"location":"projects/leiningen/todo-app/","title":"TODO web application - Leiningen project","text":"

A simple todo list server side web application

The TODO work for the todo project includes:

  • Leiningen - project configuration and build (add Clojure CLI tools)
  • ring - jetty application server and request/response management (review)
  • compojure - routing of requests (review)
  • hiccup - HTML content written using Clojure (review)
  • Postgresql - relational database for todo items (add next.jdbc library)
  • CircleCI - continuous testing and integration
  • Heroku - deployment to staging and production (pipeline)
"},{"location":"projects/leiningen/todo-app/#todocontent-update-through-winter-2020","title":"TODO::Content update through Winter 2020","text":"

This is the original project example developed several years ago, so some of the library versions may be out of date.

Practicalli recommends using next.jdbc to talk to postgresql and examples of this are covered in the new content being created for the banking on clojure project.

"},{"location":"projects/leiningen/todo-app/compojure/","title":"Compojure","text":"

To make our webapp more useful we will add more functionality, which will require more routes.

Compojure is a library that works with Ring to manage

  • routing - running different code depending on the URL path received
  • http method switching - running different code based on the HTTP method (GET, POST, PUT, DELETE)

Compojure also has convenience functions that make ring responses easier to generate.

In this section we will update our project to use Compojure.

"},{"location":"projects/leiningen/todo-app/compojure/#leiningen-templates","title":"Leiningen Templates","text":"

Templates can be used to create a project with a given set of dependencies as well as Clojure code.

There is a compojure template that gives you a basic running web application. To use this template to create a new project use the following command, substituting your own project-name

lein new compojure project-name\n

This project contains ring and compojure. The dependency for ring is ring/site-defaults which includes some sensible default settings for your application, eg security settings such as anti-forgery.

See the definition of ring/site-defaults for further information.

"},{"location":"projects/leiningen/todo-app/compojure/#resources","title":"Resources","text":"
  • Learn X in minutes - Compojure - using httpkit (performance & scalability)
  • Webapps with Compojure & OM
  • StackOverflow - What's the \u201cbig idea\u201d behind compojure routes?
"},{"location":"projects/leiningen/todo-app/compojure/about/","title":"About route","text":""},{"location":"projects/leiningen/todo-app/compojure/about/#note-write-an-about-route-and-handler-that-gives-you-information-about-the-app","title":"Note:: Write an about route and handler that gives you information about the app.","text":"
(defn about\n  \"Information about the website developer\"\n  [request]\n  {:status 200\n   :headers {}\n   :body \"I am an awesome Clojure developer, well getting there...\"})\n

Add a route to call the about handler function

(defroutes app\n  (GET \"/\"        [] welcome)\n  (GET \"/goodbye\" [] goodbye)\n  (GET \"/about\"   [] about)\n  (not-found \"<h1>This is not the page you are looking for</h1>\n              <p>Sorry, the page you requested was not found!</p>\"))\n

"},{"location":"projects/leiningen/todo-app/compojure/adding-dependency/","title":"Add Compojure as a dependency","text":"

Edit your project configuration project.clj and add the current version of Compojure

The project.clj file should look as follows:

(defproject todo-list \"0.1.0-SNAPSHOT\"\n  :description \"A Todo List server-side webapp using Ring & Compojure\"\n  :url \"https://github.com/practicalli/clojure-todo-list-example\"\n  :license {:name \"Creative Commons Attribution Share-Alike 4.0 International\"\n            :url  \"https://creativecommons.org\"}\n  :dependencies [[org.clojure/clojure \"1.10.1\"]\n                 [ring \"1.8.0\"]\n                 [compojure \"1.6.1\"]]\n  :repl-options {:init-ns todo-list.core}\n  :main todo-list.core\n  :profiles {:dev\n             {:main todo-list.core/-dev-main}})\n

As we are adding a library to the project we need to restart the web server.

Ctrl-c in the terminal to stop the server and lein run 8000 to restart the web server

"},{"location":"projects/leiningen/todo-app/compojure/adding-dependency/#hintsearch-clojarsorg-for-dependency-versions","title":"Hint::Search clojars.org for dependency versions","text":"

The current version of Compojure or any other Clojure library can be found via Clojars.org.

"},{"location":"projects/leiningen/todo-app/compojure/adding-goodbye-route/","title":"Adding a goodbye route","text":""},{"location":"projects/leiningen/todo-app/compojure/adding-goodbye-route/#noteadd-another-route-to-display-a-goodbye-message","title":"Note::Add another route to display a goodbye message","text":"
(defroutes app\n  (GET \"/\" [] welcome)\n  (GET \"/goodbye\" [] goodbye)\n  (not-found \"Sorry, page not found\"))\n
"},{"location":"projects/leiningen/todo-app/compojure/adding-goodbye-route/#notewrite-the-handler-function-for-the-goodbye-route","title":"Note::Write the handler function for the goodbye route","text":"
(defn goodbye\n  \"A song to wish you goodbye\"\n  [request]\n  {:status 200\n   :headers {}\n   :body \"<h1>Walking back to happiness</h1>\n          <p>Walking back to happiness with you</p>\n          <p>Said, Farewell to loneliness I knew</p>\n          <p>Laid aside foolish pride</p>\n          <p>Learnt the truth from tears I cried</p>\"})\n

Now test your new route.

As we have wrap-reload around app then no restart needed

"},{"location":"projects/leiningen/todo-app/compojure/code-so-far/","title":"Code so far","text":"

The code and configuration we have created so far are in the clojure-todo-list-example repository github repository.

Code for this section is in the branch called 04-compojure

If something is not working or you want to speed up, simply clone the project into a new directory using the command:

git clone https://github.com/practicalli/clojure-todo-list-example\n
Once you have cloned the project, checkout the 04-compojure branch

git checkout 04-compojure\n
"},{"location":"projects/leiningen/todo-app/compojure/defroutes/","title":"Theory: defroutes is a Clojure macro","text":"

The Compojure function defroutes is actually a Clojure macro. The defroutes macro provides a simple syntax for defining routes and associating handler functions.

"},{"location":"projects/leiningen/todo-app/compojure/defroutes/#what-is-a-macro","title":"What is a macro?","text":"

Clojure has a programmatic macro system which allows the Clojure community to extend the language, rather than wait for the language designers. This macro approach also helps keep the language very compact, with a minimum of primitives.

We have already used several macros in our code. In our project.clj configuration we use the defproject macro to make it easy to define our Clojure project. In our code we have used the defn macro to define names (symbols) for functions.

"},{"location":"projects/leiningen/todo-app/compojure/defroutes/#peeking-under-the-covers","title":"Peeking under the covers","text":"

You can always look at what a macro is doing by using the macroexpand or macroexpand-all functions. These functions show you what the code looks like after the macro-reader has processed the macro.

To expand a macro, require the clojure.walk library in your namespace

[clojure.walk :as walk]\n

Then wrap the macro you wish to explore with the macroexpand-all function.

(walk/macroexpand-all\n '(defroutes myapp\n    (GET \"/\" [] \"Show something\")))\n\n\n(def myapp\n  (compojure.core/routes\n   (compojure.core/make-route\n    :get #clout.core.CompiledRoute{:source \"/\", :re #\"/\", :keys [], :absolute? false}\n    (fn* ([request__13075__auto__] (let* [] \"Show something\"))))))\n

You can see that the defroutes function expands to a make-route function that creates the details of the route and associates it with a handler or response map. The routes function join multiple routes together.

For further examples, see http://learnxinyminutes.com/docs/clojure-macros/

"},{"location":"projects/leiningen/todo-app/compojure/lisp-calculator/","title":"Lisp style Calculator","text":"

Lets create a very simple lisp based calculator that works with two numbers as another example of using variable path elements. As its a Lisp calculator, then we will use prefix notation (the 'operator' comes first)

"},{"location":"projects/leiningen/todo-app/compojure/lisp-calculator/#create-a-route-for-the-calculator","title":"Create a route for the calculator","text":"
(defroutes app\n  (GET \"/\" [] greet)\n  (GET \"/goodbye\" [] goodbye)\n  (GET \"/about\" [] about)\n  (GET \"/request-info\" [] handle-dump)\n  (GET \"/hello/:name\" [] hello)\n  (GET \"/calculator/:op/:a/:b\" [] calculator)\n  (not-found \"Sorry, page not found\"))\n
"},{"location":"projects/leiningen/todo-app/compojure/lisp-calculator/#create-a-handler-function-to-add-subtract-divide-or-multiply-two-numbers","title":"Create a handler function to add, subtract, divide or multiply two numbers","text":"
(defn calculator\n  \"A very simple calculator that can add, divide, subtract and multiply.  This is done through the magic of variable path elements.\"\n  [request]\n  (let [a  (Integer. (get-in request [:route-params :a]))\n        b  (Integer. (get-in request [:route-params :b]))\n        op (get-in request [:route-params :op])\n        f  (get operands op)]\n    (if f\n      {:status 200\n       :body (str \"Calculated result: \" (f a b))\n       :headers {}}\n      {:status 404\n       :body \"Sorry, unknown operator.  I only recognise + - * : (: is for division)\"\n       :headers {}})))\n
"},{"location":"projects/leiningen/todo-app/compojure/lisp-calculator/#create-a-dictionary-to-look-up-clojure-function-names","title":"Create a dictionary to look up Clojure function names","text":"

Define a hash-map called operands to look up the names of the mathematical operations (operands) to the actual functions in Clojure

(def operands {\"+\" + \"-\" - \"*\" * \":\" /})\n

Try the calculator out like follows http://localhost:8000/calculator/*/6/7

"},{"location":"projects/leiningen/todo-app/compojure/lisp-calculator/#the-namespace-with-changes-made","title":"The namespace with changes made","text":"

With all the changes from above, the code should look as follows

(def operands {\"+\" + \"-\" - \"*\" * \":\" /})\n\n(defn calculator\n  \"A very simple calculator that can add, divide, subtract and multiply.  This is done through the magic of variable path elements.\"\n  [request]\n  (let [a  (Integer. (get-in request [:route-params :a]))\n        b  (Integer. (get-in request [:route-params :b]))\n        op (get-in request [:route-params :op])\n        f  (get operands op)]\n    (if f\n      {:status 200\n       :body (str (f a b))\n       :headers {}}\n      {:status 404\n       :body \"Sorry, unknown operator.  I only recognise + - * : (: is for division)\"\n       :headers {}})))\n\n(defroutes app\n  (GET \"/\" [] welcome)\n  (GET \"/goodbye\" [] goodbye)\n  (GET \"/about\" [] about)\n  (GET \"/request-info\" [] handle-dump)\n  (GET \"/yo/:name\" [] yo)\n  (GET \"/calculator/:op/:a/:b\" [] calculator)\n  (not-found \"Sorry, page not found\"))\n
"},{"location":"projects/leiningen/todo-app/compojure/show-request-info/","title":"Show request info","text":"

We can see the details of the requests being send to our Clojure webapp by looking at the request object.

"},{"location":"projects/leiningen/todo-app/compojure/show-request-info/#request-info-route","title":"request-info route","text":"

Add a request-info route and handler to view the request information

(defn request-info\n  \"View the information contained in the request, useful for debugging\"\n  [request]\n  {:status 200\n   :body (pr-str request)\n   :headers {}})\n\n(defroutes app\n  (GET \"/\" [] welcome)\n  (GET \"/goodbye\" [] goodbye)\n  (GET \"/about\" [] about)\n  (GET \"/request-info\" [] request-info)\n  (not-found \"<h1>This is not the page you are looking for</h1> <p>Sorry, the page you requested was not found!</p>\"))\n

Visit http://localhost:8000/request-info to see the results.

"},{"location":"projects/leiningen/todo-app/compojure/show-request-info/#using-compojure-request-dump-function","title":"Using Compojure request dump function","text":"

Compojure has a request dump function that gives a much nicer output than our initial request-info function. The dump function also separates the default response keys with any additional keys provided by the URL.

"},{"location":"projects/leiningen/todo-app/compojure/show-request-info/#include-handle-dump-in-the-namespace","title":"Include handle-dump in the namespace","text":"
(ns webdev.core\n  (:require [ring.adapter.jetty :as jetty]\n            [ring.middleware.reload :refer [wrap-reload]]\n            [compojure.core :refer [defroutes GET]]\n            [compojure.route :refer [not-found]]\n            [ring.handler.dump :refer [handle-dump]]))\n
"},{"location":"projects/leiningen/todo-app/compojure/show-request-info/#remove-request-info-function","title":"Remove request-info function","text":"

Delete the request-info function we defined previously and update the /request-info route to use handle-dump as the handler

(defroutes app\n  (GET \"/\" [] welcome)\n  (GET \"/goodbye\" [] goodbye)\n  (GET \"/about\" [] about)\n  (GET \"/request-info\" [] handle-dump)\n  (not-found \"<h1>This is not the page you are looking for</h1> <p>Sorry, the page you requested was not found!</p>\"))\n

Now the output is much nicer http://localhost:8000/request-info

"},{"location":"projects/leiningen/todo-app/compojure/theory-local-name-bindings/","title":"Theory local name bindings","text":""},{"location":"projects/leiningen/todo-app/compojure/theory-local-name-bindings/#theory-local-binding-with-let","title":"Theory: Local binding with let","text":"

The let function binds a name to a value within the scope of the let function. The name is used to represent the value it is bound to, especially useful if the value is complex or the result of an expression.

(let [name value])\n

Binding values to names can be used to remove duplicate code, making the code more efficient.

"},{"location":"projects/leiningen/todo-app/compojure/theory-local-name-bindings/#binding-any-value","title":"Binding any value","text":"

A let expression can bind a name to any Clojure value, from a simple number or string, to a collection or result of an expression.

In our example we are pulling out a value from a map and using the let function to create a name we can use to reference that value. The name is used to in the body of the response map, so when the response map is returned the page is displayed with the name.

(let [name (get-in request [:route-params :name])]\n    {:status 200\n     :body (str \"Hello \" name \".  I got your name from the web URL\")\n     :headers {}})\n

The Ring adaptor creates a Clojure hash-map from the browser request which is called the request map. The request map is passed to handler functions.

"},{"location":"projects/leiningen/todo-app/compojure/theory-local-name-bindings/#a-binding-is-immediately-available","title":"A binding is immediately available","text":"

A name is available for use as soon as it is bound, even within the name/value bindings section of the let expression.

(let [apples  10\n      oranges 15\n      total-fruit (+ apples oranges)]\n  (str \"Total fruit: \" total-fruit))\n
"},{"location":"projects/leiningen/todo-app/compojure/theory-local-name-bindings/#hintuse-meaningful-names-or-avoid-local-names","title":"Hint::Use meaningful names or avoid local names","text":"

Use meaningful names in let expressions to effectively communicate the purpose of the code.

If it is hard to find a meaningful name, either the problem space is not understood enough or local names may not be necessary.

"},{"location":"projects/leiningen/todo-app/compojure/theory-routing/","title":"Theory: routing","text":"

In compojure, each route is a combination offer a HTTP method paired with a URL-matching pattern, an argument list, and a handler. The handler is typically the name of function.

(defroutes myapp\n  (GET     \"/\" [] show-something)\n  (POST    \"/\" [] create-something)\n  (PUT     \"/\" [] replace-something)\n  (PATCH   \"/\" [] modify-something)\n  (DELETE  \"/\" [] annihilate-something)\n  (OPTIONS \"/\" [] appease-something)\n  (HEAD    \"/\" [] preview-something))\n

A handler is a functions which accept request maps and return response maps.

(defn show-something\n  \"A simple handler function\"\n  [request]\n  {:status 200\n   :headers {\"Content-Type\" \"text/html; charset=utf-8}\n   :body \"<h1>I am a simple handler function</h1>\"})\n

These handler functions can be called by passing a Clojure hash-map. The result is another Clojure hash-map that contains values for :status, :headers and :body.

(show-something {:uri \"/\" :request-method :post})\n;; => {:status 200\n;;     :headers {\"Content-Type\" \"text/html; charset=utf-8}\n;;     :body \"<h1>I am a simple handler function</h1>\"}\n

The body may be a function, which must accept the request as a parameter:

(defroutes myapp\n  (GET \"/\" [] (fn [req] \"Do something with req\")))\n

Or, you can just use the request directly:

(defroutes myapp\n  (GET \"/\" req \"Do something with req\"))\n

Route patterns may include named parameters:

(defroutes myapp\n  (GET \"/hello/:name\" [name] (str \"Hello \" name)))\n

You can adjust what each parameter matches by supplying a regex:

(defroutes myapp\n  (GET [\"/file/:name.:ext\" :name #\".*\", :ext #\".*\"] [name ext]\n    (str \"File: \" name ext)))\n
"},{"location":"projects/leiningen/todo-app/compojure/theory-using-hash-maps/","title":"Theory: Accessing hash-maps","text":"

The request is a Clojure hash-map made up of key / value pairs, referred to as the request map. The keys are Clojure keywords. The values are typically strings or Clojure collections (vectors, hash-maps).

Here is an example of a request map

{:request-params {:name \"John\"}}\n

Using the get function to return the value for a particular keyword in the request-map

(get request-map :keyword)\n
"},{"location":"projects/leiningen/todo-app/compojure/theory-using-hash-maps/#using-hash-map-as-a-function","title":"Using hash-map as a function","text":"

A hash-map can be evaluated as a function call to the map with the key as an argument. Any type of key can be used in this expression.

(request-map :keyword)\n(request-map \"key as string\")\n
"},{"location":"projects/leiningen/todo-app/compojure/theory-using-hash-maps/#nested-hash-maps","title":"Nested hash-maps","text":"

Two get expressions could be used to return a particular value when accessing a nested hash-map. The inner get expression returns a hash-map and the outer get expression returns the value.

(get (get outer-map :outer-keyword) :inner-keyword)\n

With many nested maps, the get function can lead to code that is harder to read. Using the get-in function provides a simpler syntax for traversing nested maps

get-in walks through the nested hash-map along the path defined by the vector of keys.

(get-in request-map [:outer-keyword :inner-keyword])\n
"},{"location":"projects/leiningen/todo-app/compojure/theory-using-hash-maps/#using-keywords-and-hash-maps","title":"Using keywords and hash-maps","text":"

Keywords can be evaluated as a function call with a hash-map as an argument and return their associated value in that hash-map.

(def response-map {:name \"john\" :path \"/hello\"}\n

You can get the value from this map using the keyword

(response-map :name)\n\n=> \"john\"\n\n(response-map :path)\n\n=> \"/hello\"\n

Other types of keys do not work as function calls. Either use the map as a function with the key as an argument or use the get and get-in functions as appropriate.

"},{"location":"projects/leiningen/todo-app/compojure/using-compojure/","title":"Using Compojure in the project","text":"

The Compojure defroute function provides a syntax for defining routes and associating handlers.

"},{"location":"projects/leiningen/todo-app/compojure/using-compojure/#add-compojure-to-the-namespace","title":"Add Compojure to the namespace","text":"

Add the defroutes function, GET protocol and notfound route from Compojure to the namespace

(ns todo-list.core\n  (:require [ring.adapter.jetty :as jetty]\n            [ring.middleware.reload :refer [wrap-reload]]\n            [compojure.core :refer [defroutes GET]]\n            [compojure.route :refer [not-found]]))\n
"},{"location":"projects/leiningen/todo-app/compojure/using-compojure/#refactor-the-welcome-function-to-just-say-hello","title":"Refactor the welcome function to just say Hello","text":"

The welcome function should just do one simple thing, return a welcome message.

(defn welcome\n  \"A ring handler to respond with a simple welcome message\"\n  [request]\n  {:status 200\n     :body \"<h1>Hello, Clojure World</h1>\n     <p>Welcome to your first Clojure app, I now update automatically</p>\"\n     <p>I now use defroutes to manage incoming requests</p>\n   :headers {}})\n
"},{"location":"projects/leiningen/todo-app/compojure/using-compojure/#add-a-defroutes-function","title":"Add a defroutes function","text":"

Add a defroutes function called app to manage our routes. Add routes for / and send all other requests to the Compojure not-found function.

(defroutes app\n  (GET \"/\" [] welcome)\n  (not-found \"<h1>This is not the page you are looking for</h1>\n              <p>Sorry, the page you requested was not found!</p>\"))\n
"},{"location":"projects/leiningen/todo-app/compojure/using-compojure/#update-dev-main-and-main-functions","title":"Update -dev-main and -main functions","text":"

Change the -dev-main and -main functions to call the app function, instead of the welcome function

(defn -main\n  \"A very simple web server using Ring & Jetty\"\n  [port-number]\n  (webserver/run-jetty app\n     {:port (Integer. port-number)}))\n\n(defn -dev-main\n  \"A very simple web server using Ring & Jetty that reloads code changes via the development profile of Leiningen\"\n  [port-number]\n  (webserver/run-jetty (wrap-reload #'app)\n     {:port (Integer. port-number)}))\n

As we have changed the -dev-main and -main functions, we need to restart the server again - Ctrl-c then lein run 8000

Now test out your updated web app by visiting http://localhost:8000 and http://localhost:8000/not-there

"},{"location":"projects/leiningen/todo-app/compojure/variable-path-elements/","title":"Variable Path Elements","text":"

A simple way to affect the behaviour of a web app is to add extra text (elements) to the web address (URL). For example, you can add your name to the end of the web address and the returned web page will include your name.

By adding an element to the route path, we can take that element from the URL as it is part of the request. We can then get that value from the request map and use it in our body content.

"},{"location":"projects/leiningen/todo-app/compojure/variable-path-elements/#hello-handler-example","title":"Hello handler example","text":"

Create a simple personalised hello message by adding a route for /hello with /:name as a path element.

Create a hello function as the handler that pulls out the :name element from the request and adds it to the response.

(defn hello\n  \"A simple personalised greeting showing the use of variable path elements\"\n  [request]\n  (let [name (get-in request [:route-params :name])]\n    {:status 200\n     :body (str \"Hello \" name \".  I got your name from the web URL\")\n     :headers {}}))\n\n(defroutes app\n  (GET \"/\" [] greet)\n  (GET \"/goodbye\" [] goodbye)\n  (GET \"/about\" [] about)\n  (GET \"/request-info\" [] handle-dump)\n  (GET \"/hello/:name\" [] hello)\n  (not-found \"Sorry, page not found\"))\n

Now you can test this route out by also including a name to the URL path http://localhost:8000/hello/john

"},{"location":"projects/leiningen/todo-app/connect-to-postgres/","title":"Connecting to Heroku PostgreSQL from Clojure","text":"
  • Add dependencies
  • Define a database connection (Heroku posgres)
  • Migrations (TODO)
"},{"location":"projects/leiningen/todo-app/connect-to-postgres/#using-jdbc-for-relational-databases","title":"Using JDBC for Relational Databases","text":"

Java Database connectivity is a common way to connect to a relational database and has very widespread database support.

next.jdbc is a Clojure library to send SQL statements over jdbc or use a DSL such as HoneySQL) to work with these databases.

"},{"location":"projects/leiningen/todo-app/connect-to-postgres/add-database-dependencies/","title":"Add Dependency","text":"

Our application will use JDBC (Java database connectivity) to connect to the Postgres database. So we need to add the JDBC library along with a a specific JDBC driver for Postgres.

Edit the project configuration file, project.clj and add the following dependencies

[org.clojure/java.jdbc \"0.7.10\"]\n[org.postgresql/postgresql \"42.2.9\"]\n

The project.clj file should now look as follows:

(defproject todo-list \"0.1.0-SNAPSHOT\"\n  :description \"A Todo List server-side webapp using Ring & Compojure\"\n  :url \"https://github.com/practicalli/clojure-todo-list-example\"\n  :license {:name \"Creative Commons Attribution Share-Alike 4.0 International\"\n            :url  \"https://creativecommons.org\"}\n\n  :dependencies [[org.clojure/clojure \"1.10.1\"]\n                 [ring \"1.8.0\"]\n                 [compojure \"1.6.1\"]\n                 [org.clojure/java.jdbc \"0.7.10\"]\n                 [org.postgresql/postgresql \"42.2.9\"]]\n  :min-lein-version \"2.0.0\"\n  :repl-options {:init-ns todo-list.core}\n  :main todo-list.core\n  :profiles {:dev\n             {:main todo-list.core/-dev-main}\n             :uberjar {:aot :all}}\n  :uberjar-name \"todo-list.jar\"\n  :auto-clean false)\n
"},{"location":"projects/leiningen/todo-app/connect-to-postgres/add-database-dependencies/#note-add-dependencies-to-the-project-for-the-heroku-postgres-database","title":"Note:: Add Dependencies to the project for the Heroku Postgres database","text":""},{"location":"projects/leiningen/todo-app/connect-to-postgres/define-db-connection/","title":"Define a Database Connection","text":""},{"location":"projects/leiningen/todo-app/connect-to-postgres/define-db-connection/#hintoutdated-use-nextjdbc-approach","title":"Hint::Outdated: Use next.jdbc approach","text":"

next.jdbc provides a simple way to connect to a range of databases

Heroku provides a way to generate the connection string. The Heroku build process sets an environment variable called JDBC_DATABASE_URL which can be used with next.jdbc.

"},{"location":"projects/leiningen/todo-app/connect-to-postgres/define-db-connection/#outdated-under-review","title":"Outdated - under review","text":"

View the Database_URL configuration variable for the Heroku Database and define a name to represent that in Clojure

Use the Heroku Toolbelt to view the configuration variables

heroku config\n

Edit the file src/todo_list/core.clj file and add the following definition towards the top of the file. Substitute your own database connection values for :subname, user and password.

(def postgres {:subprotocol \"postgresql\"\n               :subname \"//node.domain.com:5432/database-name\"\n               :user \"username\"\n               :password \"password\"\n               :ssl true\n               :sslmode true\n               :sslfactory \"org.postgresql.ssl.NonValidatingFactory\"})\n

Breaking down the Heroku Postgres connection string into a map allows us to easily add options to the connection string whilst keeping it readable.

Also, a JDBC connection string has a slightly different form to the Heroku string. Heroku Posgres creates a configuration variable in the form of postgres://[user]:[password]@[host]:[port]/[database] whereas the JDBC connection string is of the form `jdbc:postgres://[host]:[port]/[database]?user=[user]&password=[pass]

"},{"location":"projects/leiningen/todo-app/connect-to-postgres/define-db-connection/#jdbc-connection-string-for-heroku-postgres","title":"JDBC connection string for Heroku Postgres","text":"

jdbc:postgresql://[host]:[port]/[database]?user=[user]&password=[password]&ssl=true&sslfactory=org.postgresql.ssl.NonValidatingFactory.

Converting the map back to a JDBC connection string

(defn remote-heroku-db-spec [host port database username password]\n  {:connection-uri (str \"jdbc:postgresql://\" host \":\" port \"/\" database \"?user=\" username \"&password=\" password \"&ssl=true&sslfactory=org.postgresql.ssl.NonValidatingFactory\")})\n
"},{"location":"projects/leiningen/todo-app/connect-to-postgres/define-db-connection/#from-heroku","title":"From Heroku","text":"

JDBC_DATABASE_URL environment variable should be used for the Heroku database connection

The DATABASE_URL environment variable from the Heroku Postgres add-on follows this naming convention:

postgres://<username>:<password>@<host>/<dbname>\n

However the Postgres JDBC driver uses the following convention:

jdbc:postgresql://<host>:<port>/<dbname>?user=<username>&password=<password>\n

Notice the additional ql at the end of jdbc:postgresql.

"},{"location":"projects/leiningen/todo-app/create-a-handler-function/","title":"Create a handler function","text":"

So far we have just sent back the same response map. To make our webapp more useful then we should have functions that return different web pages and resources (like JSON for API's).

In Ring terminology, these functions are referred to as a handler. They handler a request and return a response.

When you send a request to the webapp, the ring adaptor converts this request to a map and sends it to the specified handler.

A handler function takes the request map as its argument and returns a response map.

"},{"location":"projects/leiningen/todo-app/create-a-handler-function/#noteadd-separate-handler-function","title":"Note::Add separate handler function","text":"

Refactor the code in the src/todo-list/core.clj file to create a separate welcome handler function that processes all requests

(defn welcome\n  \"A ring handler to process all requests sent to the webapp\"\n  [request]\n  {:status  200\n   :headers {}\n   :body    \"<h1>Hello, Clojure World</h1>\n             <p>Welcome to your first Clojure app.\n             This message is returned regardless of the request, sorry<p>\"})\n

Update the -main function to call the welcome function

(defn -main\n  \"A very simple web server using Ring & Jetty\"\n  [port-number]\n  (webserver/run-jetty\n    welcome\n    {:port  (Integer. port-number)\n     :join? false}))\n
"},{"location":"projects/leiningen/todo-app/create-a-handler-function/#run-the-server-again","title":"Run the server again","text":"

Save code changes and run the web server (use Control-c if you need to stop the server first)

lein run 8000\n

Your webapp should behave exactly as it did before, check by visiting http://localhost:8000.

"},{"location":"projects/leiningen/todo-app/create-a-handler-function/#hintautomatically-reloading","title":"Hint::Automatically reloading","text":"

In the middleware section the wrap-reload ring middleware component is used to automatically reload code changes into the running application, so no need to restart the webserver unless we have to add a dependency.

"},{"location":"projects/leiningen/todo-app/create-a-handler-function/add-not-found/","title":"Add Error message when request not found","text":"

So far our app has responded with the same message, regardless of the web address (route) requested in the browser. The webapp will be more useful if it responds differently to different routes.

(defn welcome\n  \"A ring handler to process all requests for the web server.\n  If a request is for something other than `/` then an error message is returned\"\n  [request]\n  (if (= \"/\" (:uri request))\n    {:status 200\n     :body \"<h1>Hello, Clojure World</h1>\n            <p>Welcome to your first Clojure app.</p>\"\n     :headers {}}\n    {:status 404\n     :body \"<h1>This is not the page you are looking for</h1>\n            <p>Sorry, the page you requested was not found!></p>\"\n     :headers {}}))\n

If the route matches / then a response map with the welcome message is returned. For any other route, a response map containing our error message is returned.

"},{"location":"projects/leiningen/todo-app/create-a-handler-function/add-not-found/#noteadd-error-message-to-handler","title":"Note::Add error message to handler","text":"

Change the code to only respond with content when requesting the default route, that is http://localhost:8000/. Anything else we will return an error.

Edit the welcome function in src/todo-list/core.clj and use an if function to check if the request is valid or not.

"},{"location":"projects/leiningen/todo-app/create-a-handler-function/add-not-found/#run-the-new-version-of-your-code","title":"Run the new version of your code","text":"

If your server is still running, kill it first using Ctrl-c keyboard shortcut. Then run the server again, this time with the new code using the same command as before:

lein run 8000\n

Open http://localhost:8000 in your browser and try out different pages, such at /hello, /goodbye or /complete-indifference.

Only http://localhost:8000 will return the welcome message, everything else should return the error message.

"},{"location":"projects/leiningen/todo-app/create-a-handler-function/code-so-far/","title":"Code so far","text":"

The code and configuration we have created so far are in the clojure-webapps-example github repository.

Code for this section is in the branch called 02-create-a-handler-function

If something is not working or you want to speed up, simply clone the project into a new directory using the command:

git clone https://github.com/practicalli/clojure-webapps-example\n
Once you have cloned the project, checkout the 02-create-a-handler-function branch

git checkout 02-create-a-handler-function\n
"},{"location":"projects/leiningen/todo-app/create-a-handler-function/if-function/","title":"Theory: if function","text":"

Clojure has an if function that evaluates an expresssion. If that expression is true, then the first value is returned, if false then the second argument is returned.

In pseudo-code, the if function in Clojure works as follows

  If (this expression is true ?)\n    then return this value\n    else return this value\n

In the project code an if function checks the web address by returning the value associated with :uri in the request map.

If the :url value is equal to / then the first response map with the hello message is returned.

If the :uri value is not equal to / then the second resource map with an error message is returned.

  (if (= \"/\" (:uri request))\n    {:status 200\n     :body \"<h1>Hello, Clojure World</h1>\n            <p>Welcome to your first Clojure app.</p>\"\n     :headers {}}\n    {:status 404\n     :body \"<h1>This is not the page you are looking for</h1>\n            <p>Sorry, the page you requested was not found!></p>\"\n     :headers {}}))\n
"},{"location":"projects/leiningen/todo-app/create-a-handler-function/if-function/#hintsingle-path-if-function","title":"Hint::Single path if function","text":"

In the case where an if expression is defined with only one value and the expression is false, then the value nil is returned. when function is the idiomatic choice over a single path if function.

"},{"location":"projects/leiningen/todo-app/create-a-handler-function/if-function/#multiple-expression-if-function-with-do","title":"Multiple expression if function with do","text":"

Each of the two possible values the if function returns can come from only evaluating one expression. For example

(if (true)\n  (str \"I am the truth\")\n  (str \"I am the path to darkness\")\n

If you need multiple expressions they can be wrapped in the do function

(if (true)\n  (do (some-function)\n      (another-function))\n  (else-function))\n

The do function calls each function evaluation in turn, returning the result of the last function called.

"},{"location":"projects/leiningen/todo-app/create-a-handler-function/if-function/#hintcompojure-for-managing-routes","title":"Hint::Compojure for managing routes","text":"

The if function is a very simplistic way to define routes in our web application. Compojure is a library for elegantly managing routing for our Clojure server-side applications.

"},{"location":"projects/leiningen/todo-app/create-a-handler-function/maps-and-keywords/","title":"Maps and keywords","text":""},{"location":"projects/leiningen/todo-app/create-a-handler-function/maps-and-keywords/#theory-maps-and-keywords","title":"Theory: maps and keywords","text":"

When a request is received by our application, it is converted from by Jetty to a servlet request. Ring then converts this to a Clojure map called request. All handlers in our application take a request map as an argument.

A map in Clojure contains one or more key / value pairs, you may be familiar with the term hash map. The keys in these maps are often defined using a Clojure keyword. A keyword is a symbol that points to itself and so therefore is unique within a specific scope. A keyword makes it very easy to get a value from a map and acts as a function on the map to return its associated value

So, assume we have defined a map called request. This map contains a key defined with the :uri keyword. We can get the value associated with the key using the keyword as a function

(def request {:uri \"/\"})\n\n(:uri request)\n\n;; As a map can also act as a function to get its elements, you can also use the following form to get the same value\n(request :uri)\n

The function get is functions that helps us get data from maps. The function get-in helps us get data from nested levels of maps.

"},{"location":"projects/leiningen/todo-app/create-a-project/","title":"Create a project","text":"

Create a project called todo-list using Leiningen, the build automation tool for Clojure. This project will run the simplest possible webserver.

On the command line:

lein new todo-list\n

"},{"location":"projects/leiningen/todo-app/create-a-project/#take-a-look-at-the-project-structure","title":"Take a look at the project structure","text":"

Change into the todo-list directory created by the Leiningen command and see the project structure that has been created.

  • project.clj - the project configuration, written in Clojure
  • src for all the source code
  • test for unit test code

Using the tree command is a simple way to see the project structure (alternatively use ls -R or a graphical file browser).

"},{"location":"projects/leiningen/todo-app/create-a-project/#hint-file-names-and-the-java-class-path","title":"Hint:: File names and the Java class path","text":"

The src and test directories both contain a directory named todo_list even though our project is todo-list.

Unfortunately the Java classpath does not like dashes '-' in directory or file names, so Leiningen changes the directory names to src/todo_list & test/todo_list and the initial test to src/todo_list/core_test.clj.

"},{"location":"projects/leiningen/todo-app/create-a-project/code-so-far/","title":"The code so far","text":"

The code and configuration we have created so far are in the clojure-todo-list-example repository github repository.

Code for this section is in the branch called master

If something is not working or you want to speed up, simply clone the project into a new directory using the command:

git clone https://github.com/practicalli/clojure-todo-list-example\n
Once you have cloned the project, checkout the master branch

git checkout master\n
"},{"location":"projects/leiningen/todo-app/create-a-project/update-project-details/","title":"Update project details","text":"

Adding project details to the project.clj file helps every developer that works with the code to have a basic understanding of the projects purpose.

"},{"location":"projects/leiningen/todo-app/create-a-project/update-project-details/#noteupdate-project-details","title":"Note::Update project details","text":"

Edit the project.clj file and make the following changes.

  • Add a description
  • Add the URL of the project, eg. the github repository
  • Update the licence (optional)
  • Update the dependencies to the latest Clojure version

The project.clj file for Practicalli projects is as follows:

(defproject todo-list \"0.1.0-SNAPSHOT\"\n  :description \"A Todo List server-side webapp using Ring & Compojure\"\n  :url \"https://github.com/practicalli/clojure-todo-list-example\"\n  :license {:name \"Creative Commons Attribution Share-Alike 4.0 International\"\n            :url  \"https://creativecommons.org\"}\n  :dependencies [[org.clojure/clojure \"1.10.1\"]]\n  :repl-options {:init-ns todo-list.core})\n
"},{"location":"projects/leiningen/todo-app/create-a-project/update-project-details/#licence-change","title":"Licence change","text":"

All code by Practicalli is under the Creative Commons Attribution Share-alike.

As well as changing the project.clj file :licence declaration, the LICENCE file created by the Leiningen template has been deleted as it refers to another licence.

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/","title":"Use the Ring Library to create a webserver","text":"

The Ring library can start an embedded Java server (eg. Jetty, Tomcat) to listen for requests from a browser. Each browser request received is converted into a request map, a Clojure map with keys and values. This request map is passed to a handler function, which returns a response map.

In the section you will discover how to:

  • Add the Ring library as a dependency
  • Including Ring in the namespace
  • Add a main function to run a Jetty webserver
  • Configure the project's main namespace
  • Run webserver
"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/#related-theory","title":"Related Theory","text":"

We will cover some related theory on Coercing types (also known as casting types) to help us deal with Java interoperability.

We will also cover how to manage the scope of your Clojure code with Namespaces.

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/#hintring-details","title":"Hint::Ring details","text":"

Ring is covered in more detail in the next section, once you have your first webserver up and running.

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/add-a-jetty-webserver/","title":"Run Jetty web server","text":"

The Ring Jetty adaptor is used to run an instance of Jetty. The -main function contains an anonymous function that takes any request and returns a response map.

The -main function takes a port number as an argument which we pass when running the application.

A response map contains the following key / value pairs * :status - the result of the request, eg. 200 OK, 401 Not Found, etc * :body - the content to be returned (web page, json, etc) * :headers - a map of standard headers included in any web browser response

Add a function called -main to the src/todo_list/core.clj file.

(defn -main\n  \"A very simple web server using Ring & Jetty\"\n  [port-number]\n  (webserver/run-jetty\n    (fn [request]\n      {:status  200\n       :headers {}\n       :body    \"<h1>Hello, Clojure World</h1>\n                 <p>Welcome to your first Clojure app.\n                 This message is returned regardless of the request, sorry</p>\"})\n    {:port  (Integer. port-number)\n     :join? false}))\n
"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/add-a-jetty-webserver/#explaining-the-new-function","title":"Explaining the new function","text":"

Using a - at the start of the -main function is a naming convention, helping you see which function is the entry point to your program. Leiningen also looks for this -main function by default when running your application.

The webserver/run-jetty function takes two arguments. In our example, the first argument is an anonymous function that returns a map (the response to the browser request); the second argument is a port number to run the jetty server on expressed as a Java Integer object.

The Integer. function is a call to java.lang.Integer. The . is a special form that tells Clojure to treat this name as a call to Java. See coercing types and java.lang

The :join? false setting enables the REPL prompt to run after the web server starts. By default the join setting is true and the running server would block access to the REPL prompt.

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/add-ring-dependency/","title":"Add Ring Dependency","text":"

Add the ring library as a dependency of the todo-list project.

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/add-ring-dependency/#noteadd-ring-dependency","title":"Note::Add ring dependency","text":"

Edit the project.clj file and add [ring \"1.8.0\"] to the :dependencies section, after the Clojure library dependency.

(defproject todo-list \"0.1.0-SNAPSHOT\"\n  :description \"A Todo List server-side webapp using Ring & Compojure\"\n  :url \"https://github.com/practicalli/clojure-todo-list-example\"\n  :license {:name \"Creative Commons Attribution Share-Alike 4.0 International\"\n            :url  \"https://creativecommons.org\"}\n  :dependencies [[org.clojure/clojure \"1.10.1\"]\n                 [ring \"1.8.0\"]]\n  :repl-options {:init-ns todo-list.core})\n
"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/add-ring-dependency/#hintdependencies-with-leiningen","title":"Hint::Dependencies with Leiningen","text":"

Read the dependencies section of the Leiningen documentation to learn more about adding libraries.

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/add-ring-dependency/#looking-up-libraries-current-versions","title":"Looking up Libraries & current versions","text":"

Libraries created by the Clojure community can be found on Clojars.org, an online repository similar to Maven Central.

Use the Clojars.org website to search for the latest version of Ring.

The dependency notation for Leiningen is documented for each library, making it easy to add the library to your project.

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/code-so-far/","title":"The code so far","text":"

The code created so far is in the clojure-webapps-example github repository, specifically the branch called 01-create-a-webserver

If something is not working or you want to speed up, simply clone the project (if you have not already done so) into a new directory using the command:

git clone https://github.com/practicalli/clojure-webapps-example\n

Checkout the 01-create-a-webserver branch to see the relevant version of the code

git checkout 01-create-a-webserver-with-ring\n
"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/coersing-types-and-java-lang/","title":"Theory: Specifying Types & java.lang","text":"

Clojure has types that are created dynamically when the code is compiled, with everything being represented by Java objects as its compiled to Java byte code.

Clojure simply infers the type of a value, so types do not need to be specified in code.

The built in collections (list, map, vector & set) also support mixed types too.

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/coersing-types-and-java-lang/#calling-java-code","title":"Calling Java code","text":"

The Clojure project uses Jetty, a web application server written in Java. When calling the run-jetty function an Integer type must be passed to the Java object for the port number.

When running the Clojure project, the argument supplied for the port number on the command line is treated as a String object. Therefore we need to explicitly cast the port number from a Java String type to an Java Integer type.

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/coersing-types-and-java-lang/#javalang-library","title":"java.lang library","text":"

The java.lang. library is part of all Clojure projects, so as we are going to create a Java Integer it makes sense to simply use the Integer constructor with a String argument which returns a new Integer object.

(Integer. port-number) calls the java.lang.Integer constructor.

The . is actually a macro in Clojure that provides a simple way to work with Java, allowing you to call Java objects as if they were Clojure functions. In Java you would have to use the form Type instance-name = new Type(argument). In our example you would write this in Java as String port = new String(port-number)

From the Java 8 docs for Integer class: Integer(String s) - constructs a newly allocated Integer object that represents the int value indicated by the String parameter.

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/coersing-types-and-java-lang/#theory-its-java-objects-underneath-strings-numbers","title":"Theory: Its Java Objects underneath strings & numbers","text":"

Strings and numbers are represented by Java objects underneath, so its convenient to use Java Classes to manipulate these simple data structures on the rare occasion you need a specific type.

You can see the underlying Java types in Clojure using the type or class function. In the following example you can see the Java types for strings and numbers

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/configure-main-namespace/","title":"Configure main namespace","text":"

Setting the default namespace will automatically call a function called -main when the Clojure project is run, i.e. via lein run

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/configure-main-namespace/#noteadd-main-namespace","title":"Note::Add main namespace","text":"

Edit the project.clj file and add :main todo-list.core configuration option.

(defproject todo-list \"0.1.0-SNAPSHOT\"\n  :description \"A Todo List server-side webapp using Ring & Compojure\"\n  :url \"https://github.com/practicalli/clojure-todo-list-example\"\n  :license {:name \"Creative Commons Attribution Share-Alike 4.0 International\"\n            :url  \"https://creativecommons.org\"}\n  :dependencies [[org.clojure/clojure \"1.10.1\"]\n                 [ring \"1.8.0\"]]\n  :repl-options {:init-ns todo-list.core}\n  :main todo-list.core)\n
"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/include-ring-library/","title":"Including Ring in the Namespace","text":"

Add the ring-adaptor-jetty namespace from the ring library, so we can use the functions from that library.

The ns expression defines the current namespace as todo-list.core, providing a scope for all the functions and data structures we define within it.

The :require expression makes the ring.adaptor.jetty namespace accessible within the todo-list.core namespace. We can now call any of the public functions in the ring.adaptor.jetty namespace.

In ring.adapter.jetty namespace is bound to the webserver alias, providing a short name to refer to functions from that namespace.

For example, the run-jetty function is called using webserver/run-jetty rather than the fully qualified namespace of ring.adaptor.jetty/run-jetty

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/include-ring-library/#noterequire-the-ring-adaptor","title":"NOTE::Require the ring-adaptor","text":"

Delete all code in src/todo_list/core.clj and replace it with the following code.

(ns todo-list.core\n  (:require [ring.adapter.jetty :as webserver]))\n
"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/include-ring-library/#hintusing-aliases-for-namespaces","title":"Hint::Using aliases for namespaces","text":"

Using :require we can use the :as keyword to specify an alias for a namespace, a short-hand way of referring to a library. You can specify any valid Clojure name for a namespace alias, however please consider the readability of your code and choose a meaningful alias name.

Later in the workshop we will show other options for including functions from other namespaces.

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/namespaces/","title":"Theory: Namespaces","text":"

A namespace in Clojure is used to manage the logical separation of code, usually along features of the application. A namespace limits the scope of functions and names of data structures to a specific namespace.

The names bound to function definitions using the defn function can be used elsewhere in the namespace just by using the name. The same goes for any values bound to a name using the def function.

To use a function outside the namespace, you need to use its namespace and its name, for example clojure.string/reverse

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/namespaces/#hintclojure-order-of-evaluation","title":"Hint::Clojure order of evaluation","text":"

The code in a Clojure namespace is evaluated only once and from top to bottom. To call a named function or data structure, it must have its definition evaluated first.

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/namespaces/#include-another-namespace-in-the-repl","title":"Include another namespace in the REPL","text":"

The require function will provide access functions and names from specific namespaces and an alias for the namespace can also be specified with the :as directive.

A function from that namespace can then be used by prefixing its name with the alias specified in the require expression.

Here is an example of including the clojure.string namespace and calling its reverse function

(require '[clojure.string :as string])\n\n(string/reverse \"RedRum\")\n
"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/namespaces/#including-another-namespace-in-source-code","title":"Including another namespace in source code","text":"

Instead of the require function, add the :require keyword in the namespace definition, ns.

(ns todo-list.core\n (:require '[clojure.string :as string])\n\n(string/reverse \"RedRum\")\n

If a function will be used many times in the namespace, you can :refer a function so you can call it just by name, as if it had been defined in the current namespace.

(ns todo-list.core\n (:require '[clojure.string :refer [reverse]]))\n\n(reverse \"RedRum\")\n
"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/namespaces/#hintdependency-conflicts-avoid-the-use-function","title":"Hint::Dependency conflicts - avoid the use function","text":"

The use function and :use within an ns definition are seen as a bad practice and should be avoided.

The use function includes all the functions as if they had been written in the including a great many unused functions into the namespace. It will also pull in all the other namespace functions that each namespace included.

As Clojure is typically composed of many libraries, its prudent to only include the specific things you need from another namespace. This also helps reduce conflicts when including multiple libraries in your project.

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/namespaces/#namespaces-outside-the-project","title":"Namespaces outside the project","text":"

To use a namespace from a library that is not part of the project, you also need to include it as a dependency. We saw in add ring dependency how to add a library as a :dependency in the Leiningen project.clj file.

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/run-webserver/","title":"Run webserver","text":"

Run the webserver we use Leiningen, the Clojure build automation tool.

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/run-webserver/#run-the-webserver","title":"Run the webserver","text":"

In a command line terminal, navigate to the root of your project and type the following command

lein run 8000\n

This command will start an embedded Jetty web server that listens on http://localhost:8000.

Open http://localhost:8000 in your browser and try out different pages, such at http://localhost:8000/hello, /goodbye or /makes-no-difference. It should not matter what page you visit, you should get the same response.

To stop the server, press Control-c in the terminal used to run the lein command.

"},{"location":"projects/leiningen/todo-app/database-model/","title":"Creating a database model","text":"

Our tasks are quite simple and so its easy to represent them as a single table

  • id (auto-generated)
  • name of task
  • description of task
  • type of task

Each task will have a unique ID, automatically generated when a new record is created.

The name, description and type of task are all strings.

"},{"location":"projects/leiningen/todo-app/database-model/#hinttodo-complete-the-database-model","title":"Hint::TODO: Complete the database model","text":"

The examples in Database model section are not finished, although hopefully you have learned enough to be able to continue working on this for homework.

Please ask questions and share your approaches in the Practicalli Contact channels

These examples will be updated to use next.jdbc over Winter 2020

"},{"location":"projects/leiningen/todo-app/database-model/#hint-the-type-of-task-could-be-managed-by-a-second-table-that-lists-all-the-tasks-however-this-is-only-meant-to-be-a-simple-app-at-this-stage","title":"Hint:: The type of task could be managed by a second table that lists all the tasks. However, this is only meant to be a simple app at this stage.","text":""},{"location":"projects/leiningen/todo-app/database-model/#namespace-design","title":"Namespace design","text":"

We need to decide what namespace to put our data model in. It seems to make sense to create a new namespace, to help keep our code clean and to separate concerns. So we will create a namespace todo-list.list namespace.

We will need to decide whether to add the items namespace to core or to the handlers... or maybe create another handler namespace for handlers that just access the database

(:require [todo-list.items :as items]\n
"},{"location":"projects/leiningen/todo-app/database-model/alternative-approaches/","title":"Alternative approaches","text":""},{"location":"projects/leiningen/todo-app/database-model/alternative-approaches/#hint-here-is-an-alterative-approach-to-the-code-just-created-for-comparison-purposes-only-there-is-no-need-to-implement-any-of-the-following-code-unless-you-prefer-this-approach","title":"Hint:: Here is an alterative approach to the code just created, for comparison purposes only. There is no need to implement any of the following code (unless you prefer this approach)","text":""},{"location":"projects/leiningen/todo-app/database-model/alternative-approaches/#using-uuid-ossp-postgres-plugin","title":"Using UUID-OSSP Postgres plugin","text":"

The UUID-OSSP extension to our Heroku postgres database to autogenerate universal ID's (UUID). These UUID's are managed by postgres and therefore not resistant to braking from code. The database memory overhead for UUID's is typically less than using text based ID's

(defn create-table [db]\n  (db/execute!\n   db\n   [\"CREATE EXTENSION IF NOT EXISTS \\\"UUID-OSSP\\\"\" ])\n  (db/execute!\n   db\n   [\"CREATE TABLE IF NOT EXISTS items\n      (id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),\n       name TEXT NOT NULL,\n       description BOOLEAN NOT NULL DEFAULT FALSE,\n       date_created TIMESTAMPTZ NOT NULL DEFAULT now()\"]))\n

Fixme What is the clojure.java.jdbc version of the above ?

"},{"location":"projects/leiningen/todo-app/database-model/alternative-approaches/#add-more-database-functions","title":"Add more database functions","text":"
(defn create-item [db name description]\n  (:id (first (db/query\n               db [\"INSERT INTO items (name, description)\n                    VALUES (?, ?)\n                    RETURN id\"\n                   name\n                   description]))))\n\n(defn update-item [db id checked]\n  (= [1] (db/execute!\n          db\n          [\"UPDATE items\n            SET checked = ?\n            WHERE id = ?\"\n           checked\n           id])))\n\n(defn delete-item [db id]\n  (= [1] (db/execute!\n          db\n          [\"DELETE FROM items\n            WHERE id = ?\"\n           id])))\n\n(defn read-items [db]\n  (db/query\n   db\n   [\"SELECT id, name, description, checked, date_created\n     FROM items\n     ORDER BY date_created\"]))\n
"},{"location":"projects/leiningen/todo-app/database-model/create-table/","title":"Create table","text":"

We have our database model for tasks, so lets create write some code that will create a database table in Postgres, assuming that table is not there already.

"},{"location":"projects/leiningen/todo-app/database-model/create-table/#hintrecommend-using-nextjdbc","title":"Hint::Recommend using next.jdbc","text":"

next.jdbc is the next generation of clojure.java.jdbc and is recommended instead. The API is very similar, although with many improvements

"},{"location":"projects/leiningen/todo-app/database-model/create-table/#create-items-namespace","title":"Create items namespace","text":"

Create a new Clojure file src/todo_list/items.clj and add the following code

First add a dependency for Clojure.java.jdbc

[clojure.java.jdbc :as sql]\n

You items.clj should look like

(ns todo-list.items\n  (:require [clojure.java.jdbc :as sql]))\n

We only want to create the database if it does not already exist, so we can check if the table is already part of the schema

(defn db-schema-migrated?\n  \"Check if the schema has been migrated to the database\"\n  []\n  (-> (sql/query postgres\n                 [(str \"select count(*) from information_schema.tables \"\n                       \"where table_name='tasks'\")])\n      first :count pos?))\n

Then add a condition to check if the table exists and if not then create the database table

(defn apply-schema-migration\n  \"Apply the schema to the database\"\n  []\n  (when (not (db-schema-migrated?))\n    (sql/db-do-commands postgres\n                        (sql/create-table-ddl\n                         :tasks\n                         [:id :serial \"PRIMARY KEY\"]\n                         [:body :varchar \"NOT NULL\"]\n                         [:created_at :timestamp\n                          \"NOT NULL\" \"DEFAULT CURRENT_TIMESTAMP\"]))))\n
"},{"location":"projects/leiningen/todo-app/database-model/create-table/#what-heroku-does-when-you-create-a-database","title":"What Heroku does when you create a database","text":"

Heroku Postgres users are granted all non-superuser permissions on their database. These include SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER, CREATE, CONNECT, TEMPORARY, EXECUTE, and USAGE.

Heroku runs the SQL below to create a user and database for you.

You cannot create or modify databases and roles on Heroku Postgres. The SQL below is for reference only.

CREATE ROLE user_name;\nALTER ROLE user_name WITH LOGIN PASSWORD 'password' NOSUPERUSER NOCREATEDB NOCREATEROLE;\nCREATE DATABASE database_name OWNER user_name;\nREVOKE ALL ON DATABASE database_name FROM PUBLIC;\nGRANT CONNECT ON DATABASE database_name TO database_user;\nGRANT ALL ON DATABASE database_name TO database_user;\n
"},{"location":"projects/leiningen/todo-app/database-model/create-task/","title":"Create a task","text":"

Write a function to create tasks in the database

(defn create-task [task-name]\n  (sql/insert! postgres\n               :tasks [:body] [task-name])\n  (println task-name))\n
"},{"location":"projects/leiningen/todo-app/database-model/delete-task/","title":"Delete task","text":""},{"location":"projects/leiningen/todo-app/database-model/show-all-task/","title":"Show all tasks","text":"

Write a function to list all the tasks in the database, limited to the first 128 items

(defn all-tasks []\n  (into [] (sql/query postgres [\"select * from tasks order by id desc limit 128\"])))\n
"},{"location":"projects/leiningen/todo-app/heroku/","title":"Deploying to Heroku","text":"

Heroku is a developer-focused Platform as a Service, using the tools developers know well. You can simply push your projects to Heroku using Git and your application is deployed for you automatically.

  • Create a free Heroku account
  • Download the Heroku Toolbelt
"},{"location":"projects/leiningen/todo-app/heroku/#identify-your-laptop-to-heroku","title":"Identify your laptop to Heroku","text":"

To be able to deploy your app to Heroku, you first need establish a trusted connection between your laptop and Heroku. Run the following command (from the Heroku Toolbelt)

heroku login\n

Enter your username and password for Heroku. Also enter your 2-factor authorisation code if you enabled that on your Heroku account.

Credentials are cached so heroku login should only need to be run once per computer and user account.

"},{"location":"projects/leiningen/todo-app/heroku/code-so-far/","title":"Code so far","text":"

The code and configuration we have created so far are in the clojure-todo-list-example repository github repository,

Code for this section is in the branch called ``

If something is not working or you want to speed up, simply clone the project into a new directory using the command:

git clone https://github.com/practicalli/clojure-todo-list-example\n
Once you have cloned the project, checkout the `` branch

git checkout\n
"},{"location":"projects/leiningen/todo-app/heroku/deploy/","title":"Deploy to Heroku","text":"

First we need to create an Heroku app to deploy our Clojure webapp to. This adds a remote repository we can push our code to using Git.

In a command line terminal, navigate to the root of your project (where your project.clj file is)

heroku create\n

If we have changes in our source code files, then we should first add and then commit them to our local repository.

git add .\ngit commit -m \"meaningful commit message\"\n

Now push the Clojure webapp code to Heroku and wait a few moments for it to deploy

git push heroku master\n
"},{"location":"projects/leiningen/todo-app/heroku/deploy/#note-create-an-heroku-app-using-the-heroku-dashboard-or-using-the-following-heroku-toolbelt-command","title":"Note:: Create an Heroku app using the Heroku dashboard or using the following Heroku toolbelt command","text":""},{"location":"projects/leiningen/todo-app/heroku/procfile/","title":"Add Procfile","text":"

The Procfile is a simple text file that instructs Heroku how to build and run an application.

Using the web: directive, we tell Heroku that our application will listen for web traffic (https). Heroku sets a value for the port our application can listen to using the PORT configuration variable (ports are dynamically assigned).

Create a new file called Procfile with the following text

web: java $JVM_OPTS -cp target/todo-list.jar clojure.main -m todo-list.core $PORT\n
"},{"location":"projects/leiningen/todo-app/heroku/procfile/#hintget-webserver-port-from-heroku","title":"Hint::Get webserver PORT from Heroku","text":"

Heroku dynamically assigns a port number for each web: application deployed. The Heroku port is set in the PORT environment variable within Heroku each time the application is deployed.

\"$PORT\" should be an argument to any service that runs a web server (Jetty, HTTPkit server) or the value should be obtained from the Heroku environment from the Clojure code.

"},{"location":"projects/leiningen/todo-app/heroku/procfile/#theory-running-clojure-as-a-java-application","title":"Theory: Running Clojure as a Java application","text":"

When you run a Clojure project with Leiningen, two Java virtual machines (JVM's) are started. One JVM is to run Leiningen and the second JVM is to run your application. By using Leiningen to run your application in production you are using use extra resources and also risk pulling in unnecessary development libraries & configuration only needed during development.

When you run your application in production you can save resources by only running a JVM for your application. This is done by running a Clojure application just like a Java application, using the java command in the Heroku Procfile.

"},{"location":"projects/leiningen/todo-app/heroku/procfile/#theory-building-clojure","title":"Theory: Building Clojure","text":"

Building a Clojure project with Leiningen generates a Java jar file, a packaged version of your application. A jar file generated from Java can be run using java -jar jar-file-name.jar. However to run your Clojure jar file as a Java application you also need to include the the Clojure core library.

Leiningen can also generate an Uberjar. The uberjar is a jar file that also includes the Clojure library as well as your application and libraries. As the uberjar contains Clojure, you can run an uberjar in any Java environment.

By adding an :uberjar entry to the project.clj then the Leiningen command lein uberjar is run during the build and an uberjar is created.

When deploying on Heroku your application is built from your codebase using Leiningen, pulling in all the libraries your application depends on. When your application is run it is done so as a Java application using only the uberjar, starting just the one JVM.

Further reading: https://devcenter.heroku.com/articles/clojure-support

"},{"location":"projects/leiningen/todo-app/heroku/procfile/#theory-identifying-an-entry-point-for-your-application","title":"Theory: Identifying an entry point for your application","text":"

If your main namespace doesn\u2019t have a :gen-class then you can use clojure.main as your entry point and indicate your app\u2019s main namespace using the -m argument in your Procfile:

web: java $JVM_OPTS -cp target/project-standalone.jar clojure.main -m myproject.web $PORT\n
"},{"location":"projects/leiningen/todo-app/heroku/update-project/","title":"Update the project","text":"

Specify how Leiningen builds the project in more detail and tell Heroku how to run the application.

"},{"location":"projects/leiningen/todo-app/heroku/update-project/#configure-the-leiningen-build","title":"Configure the Leiningen Build","text":"

Update the Clojure project file with a minimum version number for Leiningen and a name for the jar file that Leiningen will build

Edit the project.clj file and add the following lines, usually after the dependencies declarations

:min-lein-version \"2.0.0\"\n:uberjar-name \"todo-list.jar\"\n

The update project file should look as follows

(defproject todo-list \"0.1.0-SNAPSHOT\"\n  :description \"A simple webapp using Ring\"\n  :url \"http://example.com/FIXME\"\n  :license {:name \"Eclipse Public License\"\n            :url \"http://www.eclipse.org/legal/epl-v10.html\"}\n  :dependencies [[org.clojure/clojure \"1.6.0\"]\n                 [ring \"1.4.0-beta2\"]\n                 [compojure \"1.3.4\"]]\n  :main todo-list.core\n  :min-lein-version \"2.0.0\"\n  :uberjar-name \"todo-list.jar\"\n  :profiles {:dev\n              {:main todo-list.core/-dev-main}})\n
"},{"location":"projects/leiningen/todo-app/hiccup/","title":"Hiccup - HTML Library","text":"

Hiccup is a library for generating HTML from Clojure, keeping your code consistent and easier to write and maintain.

Using HTML, a heading would be written as:

<h1 class='heading'>I am a heading</h1>\n

Hiccup uses vectors to define HTML tags and maps to represent styles and other attributes. So the same heading in Hiccup would be written as:

[:h1 {:class \"heading\"} \"I am a heading\"]\n
"},{"location":"projects/leiningen/todo-app/hiccup/#using-hiccup","title":"Using Hiccup","text":"

Add the hiccup dependency to your project.clj file

[hiccup \"1.0.5\"]\n

In the REPL, require the hiccup.core library

user=> (require '[hiccup.core :as markup])\n;; => nil\n

Or add hiccup to the namespace definition in your Clojure code file.

(ns my-namespace.core\n  (require '[hiccup.core :as markup]))\n
"},{"location":"projects/leiningen/todo-app/hiccup/#writing-hiccup","title":"Writing Hiccup","text":"

Here is a basic example of Hiccup syntax:

(markup/html [:span {:class \"foo\"} \"bar\"])\n;; => \"<span class=\\\"foo\\\">bar</span>\"\n

The first element of the vector is used as the element name. The second attribute can optionally be a map, in which case it is used to supply the element's attributes. Every other element is considered part of the tag's body.

Hiccup is intelligent enough to render different HTML elements in different ways, in order to accommodate browser quirks:

(markup/html [:script])\n;; => \"<script></script>\"\n\n(html [:p])\n;; =>  \"<p />\"\n

And provides a CSS-like shortcut for denoting id and class attributes:

(markup/html [:div#foo.bar.baz \"bang\"])\n;; =>  \"<div id=\\\"foo\\\" class=\\\"bar baz\\\">bang</div>\"\n

When writing multiple lines of hiccup markup, wrap them in either a [:div ] or a (list ) expression.

[:div\n  [:h1 \"My Picture Album\"]\n  [:img {:src seaside.png} \"A sunny seaside view\"]\n  [:img {:src pier.png} \"A walk along the pier\"]]\n

If the body of the element is a seq, its contents will be expanded out into the element body. This makes working with forms like map and for more convenient:

(html [:ul\n  (for [x (range 1 4)]\n    [:li x])])\n;; => \"<ul><li>1</li><li>2</li><li>3</li></ul>\"\n
(html\n  [:ul\n  (for [x (range 1 4)]\n    [:li x])])\n;; => \"<ul><li>1</li><li>2</li><li>3</li></ul>\"\n

The parent tag will still be rendered in the above example, so

"},{"location":"projects/leiningen/todo-app/hiccup/#hint-hiccup-reference-and-guides","title":"Hint:: Hiccup reference and guides","text":"
  • Hiccup API
  • Hiccup Tips - Lisp Cast
"},{"location":"projects/leiningen/todo-app/hiccup/code-so-far/","title":"Code so far","text":""},{"location":"projects/leiningen/todo-app/hiccup/create-new-handler/","title":"Create a new handler","text":"
(defn trying-hiccup\n  [request]\n  (html5 {:lang \"en\"}\n         [:head (include-js \"myscript.js\") (include-css \"mystyle.css\")]\n         [:body\n           [:div [:h1 {:class \"info\"} \"This is Hiccup\"]]\n           [:div [:p \"Take a look at the HTML generated in this page, compared to the about page\"]]\n           [:div [:p \"Style-wise there is no difference between the pages as we haven't added anything in the stylesheet, however the hiccup page generates a more complete page in terms of HTML\"]]]))\n
"},{"location":"projects/leiningen/todo-app/hiccup/create-new-handler/#hintnamed-content-sections","title":"Hint::Named content sections","text":"

As content grows, refactor it into def expressions to give content sections names. Pages can use names in the handler code that represent the content, simplifying the handler code.

As the project grows, break code into a view namespace with layouts and specific views defined in their own namespace.

"},{"location":"projects/leiningen/todo-app/hiccup/updating-handlers-with-hiccup/","title":"Updating handlers with hiccup","text":"

Instead of including fiddly html code (and having to make sure you close tags), we will write our markup in Clojure syntax using hiccup.

"},{"location":"projects/leiningen/todo-app/hiccup/updating-handlers-with-hiccup/#add-dependencies","title":"Add dependencies","text":"
[hiccup \"1.0.5\"]\n
"},{"location":"projects/leiningen/todo-app/hiccup/updating-handlers-with-hiccup/#note-add-hiccup-dependencies","title":"Note:: Add hiccup dependencies","text":""},{"location":"projects/leiningen/todo-app/hiccup/updating-handlers-with-hiccup/#require-hiccup","title":"Require hiccup","text":"
[hiccup.core :refer :all]\n[hiccup.page :refer :all]\n

Your core.clj file should look like this

(ns todo-list.core\n  (:require [ring.adapter.jetty :as jetty]\n            [ring.middleware.reload :refer [wrap-reload]]\n            [compojure.core :refer [defroutes GET]]\n            [compojure.route :refer [not-found]]\n            [ring.handler.dump :refer [handle-dump]]\n            [hiccup.core :refer :all]\n            [hiccup.page :refer :all]))\n
"},{"location":"projects/leiningen/todo-app/hiccup/updating-handlers-with-hiccup/#note-add-hiccup-to-your-namespace","title":"Note:: Add hiccup to your namespace","text":""},{"location":"projects/leiningen/todo-app/hiccup/updating-handlers-with-hiccup/#update-the-welcome-handler","title":"Update the welcome handler","text":"
(defn welcome\n  \"A ring handler to respond with a simple welcome message\"\n  [request]\n  (html [:h1 \"Hello, Clojure World\"]\n        [:p \"Welcome to your first Clojure app, I now update automatically\"]))\n

The html function create html code based on the keywords used. However, the html function does not create a full html web page.

"},{"location":"projects/leiningen/todo-app/hiccup/updating-handlers-with-hiccup/#note-change-the-welcome-handler-to-use-hiccup-rather-than-html-code","title":"Note:: Change the welcome handler to use hiccup rather than html code","text":""},{"location":"projects/leiningen/todo-app/hiccup/updating-handlers-with-hiccup/#update-the-goodbye-handler","title":"Update the goodbye handler","text":"
(defn goodbye\n  \"A song to wish you goodbye\"\n  [request]\n    (html5 {:lang \"en\"}\n           [:head (include-js \"myscript.js\") (include-css \"mystyle.css\")]\n           [:body\n            [:div [:h1 {:class \"info\"} \"Walking back to happiness\"]]\n            [:div [:p \"Walking back to happiness with you\"]]\n            [:div [:p \"Said, Farewell to loneliness I knew\"]]\n            [:div [:p \"Laid aside foolish pride\"]]\n            [:div [:p \"Learnt the truth from tears I cried\"]]]))\n

Using the html5 function a complete html page is created, with a header and body section.

See the hiccup.page API documentation.

"},{"location":"projects/leiningen/todo-app/hiccup/updating-handlers-with-hiccup/#note-change-the-goodbye-handler-to-use-hiccup-rather-than-html","title":"Note:: Change the goodbye handler to use hiccup rather than html","text":""},{"location":"projects/leiningen/todo-app/introducing-ring/","title":"Introducing Ring","text":"

Web applications typically run on a web or application server, such as Tomcat or Jetty that provide a Java Servlet Container.

The Ring library provides a way to use these servers without being tied to any specific implementation. Ring provides a common way to

  • Write your application using Clojure functions and maps
  • Run your application in an auto-reloading development server (wrap-reload)
  • Compile your application into a Java Servlet application
  • Package your application into a Java war file
  • Use a selection of middleware functions
  • Deploy your application in cloud environments like Heroku

In essence, Ring converts the requests that come from the browser into a Clojure map, the request map. The request map may be passed through one or more middleware functions before being converted to a response map by a handler. The response map may be processed by one or more middleware functions before being converted by Ring to a web server response.

In the following sections you will get a better understanding of

  • how to represent a handler
  • what do requests and responses look like
  • how to separate query parameters from the design of your web app

Ring is the current de facto standard library used to write web applications in Clojure. Higher level frameworks such as Compojure use Ring as a common basis.

"},{"location":"projects/leiningen/todo-app/postgres/","title":"Postgres Database","text":"

Postgres is a modern and powerful relational database that also supports storing of json, xml and object relationships. Postgres has a strong open source community behind it and is actively maintained. Postgres is also highly scalable database with drivers for all the major programming languages.

In this workshop we are going to use Heroku Postgres, a database on demand service that requires no local installation.

"},{"location":"projects/leiningen/todo-app/postgres/#todocontent-may-be-a-little-dated-sorry","title":"TODO::Content may be a little dated, sorry","text":"

Content update during the Winter of 2020

"},{"location":"projects/leiningen/todo-app/postgres/#hint-alternatively-you-can-use-a-local-instance-of-postgres-if-you-are-happy-to-run-it-on-your-laptop","title":"Hint:: Alternatively, you can use a local instance of Postgres if you are happy to run it on your laptop.","text":""},{"location":"projects/leiningen/todo-app/postgres/#nextjdbc-a-modern-approach-to-relational-databases-with-clojure","title":"next.jdbc - a modern approach to relational databases with Clojure","text":"

See the next.jdbc getting started guide for lots of useful information on writing SQL queries in Clojure.

"},{"location":"projects/leiningen/todo-app/postgres/#postgresql-resources","title":"PostgreSQL Resources","text":"
  • Heroku Postgres
  • Amazon Relational Database Service (RDS) - Amazon Aurora, PostgreSQL, MySQL, MariaDB, Oracle Database, and SQL Server
  • IBM Cloud: Postgres services
  • What is PostgreSQL - postgresqltutorial.com
"},{"location":"projects/leiningen/todo-app/postgres/connect-to-heroku-postgres-from-clients/","title":"Connecting to Heroku Postgres from Postgres Clients","text":"
  • Command Line
  • GUI tools
  • Operations tools

Heroku Postgres databases are accessible from anywhere via a secure http connection, so you can connect your favourite Postgres client.

"},{"location":"projects/leiningen/todo-app/postgres/connect-to-heroku-postgres-from-clients/#view-provisioned-postgres-data-stores","title":"View Provisioned Postgres Data stores","text":"

Login to https://data.heroku.com/ to see all the provisioned Heroku Postgres data stores (postgres, redis, etc.) and data clips.

"},{"location":"projects/leiningen/todo-app/postgres/connect-to-heroku-postgres-from-clients/#view-datastore-add-on","title":"View Datastore add-on","text":"

Login to https://heroku.com/ to see the dashboard of Heroku Applications created for that account. Select a specific Application to see what Datastore add-on is attached

"},{"location":"projects/leiningen/todo-app/postgres/connect-to-heroku-postgres-from-clients/#database_url-configuration-variable","title":"DATABASE_URL Configuration Variable","text":"

Provisioning an Heroku Postgres add-on automatically adds a DATABASE_URL environment variable to the Heroku app. Use this value to connect consistently throughout the life of your database from the Heroku application

To see the value of the DATABSAE_URL use the Heroku Toolbelt command heroku config, specifying the app name if you created multiple Heroku apps for the current project

heroku config\n\nheroku config --app my-app-name\n

"},{"location":"projects/leiningen/todo-app/postgres/connect-to-heroku-postgres-from-clients/#hintdatabase-clients-and-other-services","title":"Hint::Database Clients and other Services","text":"

The value of the DATABASE_URL can be used to connect remote database clients (DBeaver, PGAdmin) as well as other services that require a data store.

"},{"location":"projects/leiningen/todo-app/postgres/dataclips/","title":"Heroku Dataclips","text":"

Its very easy to create quick reports on your Heroku Postgres database using Dataclips and share the results with your company.

Heroku Dataclips allow you to write SQL queries that run on Heroku Postgres. You can then share these queries along with the results via a web address.

You can give people the ability to create their own query based on yours and share their version of the query and results. Its kind of Github or Gists for databases.

"},{"location":"projects/leiningen/todo-app/postgres/dataclips/#finding-tables-in-heroku-postgres","title":"Finding tables in Heroku Postgres","text":"
SELECT * FROM pg_catalog.pg_tables WHERE schema-name != 'pg_catalog' AND schema-name != 'information_schema'\n
"},{"location":"projects/leiningen/todo-app/postgres/environment-variables/","title":"Environment Variables","text":"

Add an Heroku Postgres database to the Heroku app creates a DATABASE_URL configuration variable, an environment variable manged by Heroku. This configuration variable can be used to avoid including database connection details in the code repository.

"},{"location":"projects/leiningen/todo-app/postgres/environment-variables/#note-check-your-heroku-app-has-a-database_url-configuration-variable","title":"Note:: Check your Heroku app has a DATABASE_URL configuration variable","text":"

List all the configuration variables for your app using the command:

heroku config\n

The DATABASE_URL contains your username and password for the database, as well as the hostname and database name in the form of:

\"postgres://username:password@hostname/database-name\"\n
"},{"location":"projects/leiningen/todo-app/postgres/environment-variables/#using-postgresql-client-applications","title":"Using PostgreSQL client applications","text":"

The Heroku Postgres database is available via an secure connection from anywhere on the Internet, so you can use these details with your favourite postgres client. The postgres client must connect over SSL, or the connection will be rejected by Heroku postgres.

"},{"location":"projects/leiningen/todo-app/postgres/install/","title":"Postgres install","text":"

Using infrastructure or software as a service databases are provisioned, usually by issuing a simple command or using a web based dashboard for that service.

Using the Heroku app created previously, a Posgres database will be provisioned.

In the root of your Clojure project, run the following Heroku toolbelt command to add a Postgres database to your existing Heroku app

heroku addons:create heroku-postgresql\n
"},{"location":"projects/leiningen/todo-app/postgres/install/#local-postgresql-install","title":"Local PostgreSQL Install","text":"
  • Install Postgresql locally - postgresql.org
  • Ubuntu documentation: PostgreSQL
  • Install and use postgresql on Ubuntu 20.04 - digitalocean
  • Ubuntu Linux PostgreSQL downloads - postgresql.org
"},{"location":"projects/leiningen/todo-app/postgres/jira-ticket/","title":"Jira ticket","text":"

get-connection requires that provided URIs be structured like so:

dbtype://user:password@host:port/database

This is often sufficient, but many PostgreSQL URIs require the use of URI parameters to further configure connections. For example, postgresql.heroku.com provides JDBC URIs like this:

jdbc:postgresql://ec2-22-11-231-117.compute-1.amazonaws.com:5432/d1kuttup5cbafl6?user=pcgoxvmssqabye&password=PFZXtxaLFhIX-nCA0Vi4UbJ6lH&ssl=true

...which, when used outside of Heroku's network, require a further sslfactory=org.postgresql.ssl.NonValidatingFactory parameter.

The PostgreSQL JDBC driver supports a number of different URI parameters, and recommends putting credentials into parameters rather than using the user:password@ convention. Peeking over at the Oracle thin JDBC driver's docs, it appears that it expects credentials using its own idiosyncratic convention, user/password@.

This all leads me to think that get-connection should pass URIs along to DriverManager without modification, and leave URI format conventions up to the drivers involved. For now, my workaround is to do essentially that, using a map like this as input to with-connection et al.:

{:factory #(DriverManager/getConnection (:url %)) :url \"jdbc:postgresql://ec2-22-11-231-117.compute-1.amazonaws.com:5432/d1kuttup5cbafl6?user=pcgoxvmssqabye&password=PFZXtxaLFhIX-nCA0Vi4UbJ6lH&ssl=true\"}

That certainly works, but I presume that such a workaround won't occur to many users, despite the docs/source.

I don't think I've used java.jdbc enough (or RDMBS' enough of late) to comfortably provide a patch (or feel particularly confident in the suggestion above). Hopefully the report is helpful in any case. Activity

All\nComments\nHistory\nActivity\n

Sean Corfield added a comment - 14/Jun/12 1:36 AM - edited

How about an option that takes a map like:

{:connection-uri \"jdbc:postgresql://ec2-22-11-231-117.compute-1.amazonaws.com:5432/d1kuttup5cbafl6?user=pcgoxvmssqabye&password=PFZXtxaLFhIX-nCA0Vi4UbJ6lH&ssl=true\"}

Essentially as a shorthand for the workaround you've come up with? Sean Corfield added a comment - 15/Jun/12 10:20 PM

Try 0.2.3-SNAPSHOT which has support for :connection-uri and let me know if that is a reasonable solution for you? Chas Emerick added a comment - 18/Jun/12 3:35 PM

Yup, 0.2.3-SNAPSHOT's :connection-uri works fine. I've since moved on to using a pooled datasource, but this will hopefully be a more obvious path to newcomers than having to learn about :factory and DriverManager. Sean Corfield added a comment - 18/Jun/12 3:52 PM

Resolved by adding :connection-uri option. Carlos Cunha added a comment - 28/Jul/12 8:09 PM

accessing an heroku database outside heroku, \"sslfactory=org.postgresql.ssl.NonValidatingFactory\" doesn't work. i get \"ERROR: syntax error at or near \"user\" Position: 13 - (class org.postgresql.util.PSQLException\". this happens whether adding it to :subname or :connection-uri Strings

another minor issue - why the documentation of \"with-connection\" (0.2.3) refers the following format for the connection string URI: \"subprotocol://user:password@host:post/subname An optional prefix of jdbc: is allowed.\" but the URI which can actually be parsed successfully is like the one above: jdbc:postgresql://ec2-22-11-231-117.compute-1.amazonaws.com:5432/d1kuttup5cbafl6?user=pcgoxvmssqabye&password=PFZXtxaLFhIX-nCA0Vi4UbJ6lH&ssl=true \"subprotocol://user:password@host:post/subname\" (format like the DATABASE environment variables on heroku) will not be parsed correctly. why the format for the URI that is used on heroku is not supported by the parser?

maybe i'm doing something wrong here

thanks in advance Sean Corfield added a comment - 29/Jul/12 4:57 PM

Carlos, the :connection-uri passes the string directly to the driver with no parsing. The exception you're seeing is coming from inside the PostgreSQL driver so you'll have to consult the documentation for the driver.

The three \"URI\" styles accepted by java.jdbc are:

:connection-uri - passed directly to the driver with no parsing or other logic in java.jdbc,\n:uri - a pre-parsed Java URI object,\na string literal - any optional \"jdbc:\" prefix is ignored, then the string is parsed by logic in java.jdbc, based on the pattern shown (subprotocol://user:password@host:port/subname).\n

If you're using :connection-uri (which is used on its own), you're dealing with the JDBC driver directly.

If you're using :uri or a bare string literal, you're dealing with java.jdbc's parsing (implemented by Phil Hagelberg - of Heroku).

Hope that clarifies? Carlos Cunha added a comment - 29/Jul/12 8:36 PM

Sean, thank you for such comprehensive explanation.

Still, it didn't work with any of the options. I used before a postgres JDBC driver to export to the same database (in an SQL modeller - SQLEditor for the MAC) and it worked (though it would connect some times, but others not). The connection String used was like \"jdbc:postgresql://host:port/database?user=xxx&password=yyy&ssl=true&sslfactory=org.postgresql.ssl.NonValidatingFactory\". The driver name was \"org.postgresql.Driver\" (JDBC4). Anyway, time to give up. I will just use a local database.

Thank you! Carlos Cunha added a comment - 31/Jul/12 7:20 PM

Sean, JDBC combinations were working after. i was neglecting an insert operation in a table with a reserved sql keyword \"user\", so i was getting a \"ERROR: syntax error at or near \"user\" Position: 13\", and therefore the connection was already established at the time.

i'm sorry for all the trouble answering the question (_

thank you Sean Corfield added a comment - 31/Jul/12 7:46 PM

Glad you got to the bottom of it and confirmed that it wasn't a problem in java.jdbc!

"},{"location":"projects/leiningen/todo-app/postgres/lobo-table-creation/","title":"How to use Lobos with Heroku","text":"

http://pupeno.com/2011/08/20/how-to-use-lobos-with-heroku/

Lobos is a Clojure library to create and alter tables which also supports migrations similar to what Rails can do. I like where Lobos is going but it\u2019s a work in progress, so the information here might be out of date soon, beware!

Let\u2019s imagine a project called px (for Project X of course) with the usual Leiningen structure. In the src directory you you need to create a lobos directory and inside there let\u2019s get started with config.clj which contains the credentials and other database information:

(ns lobos.config)\n\n(def db\n  {:classname \"org.postgresql.Driver\"\n   :subprotocol \"postgresql\"\n   :subname \"//localhost:5432/px\"})\n

then we create a simple migration in lobos/migrations.clj that creates the users table:

(ns lobos.migrations\n  (:refer-clojure :exclude [alter defonce drop bigint boolean char double float time])\n  (:use (lobos [migration :only [defmigration]] core schema) lobos.config))\n\n(defmigration create-users\n  (up [] (create (table :users\n                   (integer :id :primary-key)\n                   (varchar :email 256 :unique))))\n  (down [] (drop (table :users))))\n

You run a REPL, load the migrations and run them (using the joyful Clojure example code convention):

(require 'lobos.migrations)\n;=> nil\n(lobos.core/run)\n;=> java.lang.Exception: No such global connection currently open: :default-connection, only got [] (NO_SOURCE_FILE:0)\n

and you get an error because you didn\u2019t open the connection yet, so, let\u2019s do that:

(require 'lobos.connectivity)\n;=> nil\n(lobos.connectivity/open-global lobos.config/db)\n;=> {:default-connection {:connection #<Jdbc4Connection org.postgresql.jdbc4.Jdbc4Connection@2ab600af>, :db-spec {:classname \"org.postgresql.Driver\", :subprotocol \"postgresql\", :subname \"//localhost:5432/px\"}}}\n

and now it works:

(lobos.core/run)\n; create-users\n;=> nil\n

and you can also rollback:

(lobos.core/rollback)\n; create-users\n;=> nil\n

You might be tempted to open the global connection in your config.clj and that might be fine for some, but I found it problematic that the second time I load the file, I get an error: \u201cjava.lang.Exception: A global connection by that name already exists (:default-connection) (NO_SOURCE_FILE:0)\u201d.

My solution was to write a function called open-global-when-necessary that will open a global connection only when there\u2019s none or when the database specification changed, and will close the previous connection in that case, leaving a config.clj that looks like:

(ns lobos.config\n  (:require lobos.connectivity))\n\n(defn open-global-when-necessary\n  \"Open a global connection only when necessary, that is, when no previous\n  connection exist or when db-spec is different to the current global\n  connection.\"\n  [db-spec]\n  ;; If the connection credentials has changed, close the connection.\n  (when (and (@lobos.connectivity/global-connections :default-connection)\n             (not= (:db-spec (@lobos.connectivity/global-connections :default-connection)) db-spec))\n    (lobos.connectivity/close-global))\n  ;; Open a new connection or return the existing one.\n  (if (nil? (@lobos.connectivity/global-connections :default-connection))\n    ((lobos.connectivity/open-global db-spec) :default-connection)\n    (@lobos.connectivity/global-connections :default-connection)))\n\n(def db\n  {:classname \"org.postgresql.Driver\"\n   :subprotocol \"postgresql\"\n   :subname \"//localhost:5432/px\"})\n\n(open-global-when-necessary db)\n

That works fine locally, so let\u2019s move to Heroku. To get started with Clojure on Heroku I recommend you read:

Getting Started With Clojure on Heroku/Cedar\nBuilding a Database-Backed Clojure Web Application\n

I took the code used to extract the database specification from DATABASE_URL but I modified it so I don\u2019t depend on that environment variable existing on my local computer and I ended up with the following config.clj:

(ns lobos.config\n  (:require [clojure.string :as str] lobos.connectivity)\n  (:import (java.net URI)))\n\n(defn heroku-db\n  \"Generate the db map according to Heroku environment when available.\"\n  []\n  (when (System/getenv \"DATABASE_URL\")\n    (let [url (URI. (System/getenv \"DATABASE_URL\"))\n          host (.getHost url)\n          port (if (pos? (.getPort url)) (.getPort url) 5432)\n          path (.getPath url)]\n      (merge\n       {:subname (str \"//\" host \":\" port path)}\n       (when-let [user-info (.getUserInfo url)]\n         {:user (first (str/split user-info #\":\"))\n          :password (second (str/split user-info #\":\"))})))))\n\n(defn open-global-when-necessary\n  \"Open a global connection only when necessary, that is, when no previous\n  connection exist or when db-spec is different to the current global\n  connection.\"\n  [db-spec]\n  ;; If the connection credentials has changed, close the connection.\n  (when (and (@lobos.connectivity/global-connections :default-connection)\n             (not= (:db-spec (@lobos.connectivity/global-connections :default-connection)) db-spec))\n    (lobos.connectivity/close-global))\n  ;; Open a new connection or return the existing one.\n  (if (nil? (@lobos.connectivity/global-connections :default-connection))\n    ((lobos.connectivity/open-global db-spec) :default-connection)\n    (@lobos.connectivity/global-connections :default-connection)))\n\n(def db\n  (merge {:classname \"org.postgresql.Driver\"\n          :subprotocol \"postgresql\"\n          :subname \"//localhost:5432/px\"}\n         (heroku-db)))\n\n(open-global-when-necessary db)\n

After you push to Heroku, you can run heroku run lein repl, load lobos.config and run the migrations just as if they were local.

"},{"location":"projects/leiningen/todo-app/postgres/pg-admin/","title":"pgAdmin","text":""},{"location":"projects/leiningen/todo-app/postgres/postgres-cli/","title":"Postgres CLI","text":"

Heroku toolbelt has many commands for viewing information and querying the Heroku Postgres database. Here is a breakdown of the most commonly used commands.

Postgres Command Line Client required

Heroku Toolbelt pg commands require a working postgres command line client to be installed and available on your operating system path.

Ubuntu documentation: PostgreSQL has details on installing postgresql clients.

"},{"location":"projects/leiningen/todo-app/postgres/postgres-cli/#postgres-information","title":"Postgres Information","text":"

To see all PostgreSQL databases provisioned by your application and the identifying characteristics of each (db size, status, number of tables, PG version, creation date etc\u2026) use the heroku pg:info command.

$ heroku pg:info\n=== HEROKU_POSTGRESQL_BROWN_URL (DATABASE_URL)\nPlan:        Hobby-dev\nStatus:      available\nConnections: 0\nPG Version:  9.3.3\nCreated:     2014-03-20 23:33 UTC\nData Size:   6.5 MB\nTables:      1\nRows:        4/10000 (In compliance)\nFork/Follow: Unsupported\nRollback:    Unsupported\n

To continuously monitor the status of your database, pass pg:info through the unix watch command:

watch heroku pg:info\n

"},{"location":"projects/leiningen/todo-app/postgres/postgres-cli/#running-queries-on-postgres","title":"Running Queries on Postgres","text":"

psql is the native PostgreSQL interactive terminal and is used to execute queries and issue commands to the connected database. To establish a psql session with your remote database use heroku pg:psql. You must have PostgreSQL installed on your system to use heroku pg:psql.

$ heroku pg:psql\n---> Connecting to HEROKU_POSTGRESQL_BROWN_URL (DATABASE_URL)\npsql (9.2.6, server 9.3.3)\nWARNING: psql version 9.2, server version 9.3.\n         Some psql features might not work.\nSSL connection (cipher: DHE-RSA-AES256-SHA, bits: 256)\nType \"help\" for help.\n\nheroku-app-name::BROWN=> \\dt\n               List of relations\n Schema |     Name     | Type  |     Owner\n--------|--------------|-------|----------------\n public | pl0_programs | table | moiwgreelvvujc\n(1 row)\n\nheroku-app-name::BROWN=>\nheroku-app-name::BROWN=> SELECT * FROM pl0_programs;\n  name  |           source\n--------|-----------------------------\n 3m2m1  |                     3-2-1\\r+\n        |\n ap1tb  | a+1*b\\r                    +\n        |\n test   |                     a+1*b\\r+\n        |           \\r               +\n        |\n lolwut |                     3-2-1\\r+\n        |\n(4 rows)\n

If you have more than one database, specify the database to connect to as the first argument to the command (the database located at DATABASE_URL is used by default).

$ heroku pg:psql HEROKU_POSTGRESQL_GRAY\nConnecting to HEROKU_POSTGRESQL_GRAY... done\n
"},{"location":"projects/leiningen/todo-app/postgres/postgres-cli/#reset-your-database","title":"Reset your database","text":"

To drop and recreate your database use heroku pg:reset

$ heroku pg:reset DATABASE\n\n !    WARNING: Destructive Action\n !    This command will affect the app: heroku-app-name\n !    To proceed, type \"pegjspl0\" or re-run this command with --confirm heroku-app-name\n\n> heroku-app-name\nResetting HEROKU_POSTGRESQL_BROWN_URL (DATABASE_URL)... done\n

Then restart the server

$ heroku ps:restart\nRestarting dynos... done\n

There are many more Heroku toolbelt commands you can use for postgres. [TODO: Link to postgres command]

"},{"location":"projects/leiningen/todo-app/postgres/postgres-cli/#resources","title":"Resources","text":"
  • Accessing a Database - postgresql.org
  • Ubuntu documentation: PostgreSQL - client and server install
"},{"location":"projects/leiningen/todo-app/postgres/postgres-commands/","title":"pg:info","text":"

To see all PostgreSQL databases provisioned by your application and the identifying characteristics of each (db size, status, number of tables, PG version, creation date etc\u2026) use the heroku pg:info command.

heroku pg:info\n=== HEROKU_POSTGRESQL_BROWN_URL (DATABASE_URL)\nPlan:        Hobby-dev\nStatus:      available\nConnections: 0\nPG Version:  9.3.3\nCreated:     2014-03-20 23:33 UTC\nData Size:   6.5 MB\nTables:      1\nRows:        4/10000 (In compliance)\nFork/Follow: Unsupported\nRollback:    Unsupported\n\nTo continuously monitor the status of your database, pass pg:info through the unix watch command:\n\nwatch heroku pg:info\n-bash: watch: no se encontr\u00f3 la orden\nbrew install watch\nwatch heroku pg:info\n...\n
"},{"location":"projects/leiningen/todo-app/postgres/postgres-commands/#pgpsql","title":"pg:psql","text":"

psql is the native PostgreSQL interactive terminal and is used to execute queries and issue commands to the connected database.

To establish a psql session with your remote database use heroku pg:psql. You must have PostgreSQL installed on your system to use heroku pg:psql.

heroku pg:psql\n---> Connecting to HEROKU_POSTGRESQL_BROWN_URL (DATABASE_URL)\npsql (9.2.6, server 9.3.3)\nWARNING: psql version 9.2, server version 9.3.\n         Some psql features might not work.\nSSL connection (cipher: DHE-RSA-AES256-SHA, bits: 256)\nType \"help\" for help.\n\npegjspl0::BROWN=> \\dt\n               List of relations\n Schema |     Name     | Type  |     Owner\n--------|--------------|-------|----------------\n public | pl0_programs | table | moiwgreelvvujc\n(1 row)\n\npegjspl0::BROWN=>\npegjspl0::BROWN=> SELECT * FROM pl0_programs;\n  name  |           source\n--------|-----------------------------\n 3m2m1  |                     3-2-1\\r+\n        |\n ap1tb  | a+1*b\\r                    +\n        |\n test   |                     a+1*b\\r+\n        |           \\r               +\n        |\n lolwut |                     3-2-1\\r+\n        |\n(4 rows)\n

If you have more than one database, specify the database to connect to as the first argument to the command (the database located at DATABASE_URL is used by default).

heroku pg:psql HEROKU_POSTGRESQL_GRAY\nConnecting to HEROKU_POSTGRESQL_GRAY... done\n...\n
"},{"location":"projects/leiningen/todo-app/postgres/postgres-commands/#pgreset","title":"pg:reset","text":"

To drop and recreate your database use pg:reset:

heroku pg:reset DATABASE\n\n !    WARNING: Destructive Action\n !    This command will affect the app: pegjspl0\n !    To proceed, type \"pegjspl0\" or re-run this command with --confirm pegjspl0\n\n> pegjspl0\nResetting HEROKU_POSTGRESQL_BROWN_URL (DATABASE_URL)... done\n\nheroku ps:restart\nRestarting dynos... done\n
"},{"location":"projects/leiningen/todo-app/postgres/postgres-commands/#pgpull","title":"pg:pull","text":"

pg:pull can be used to pull remote data from a Heroku Postgres database to a database on your local machine. The command looks like this:

pg_ctl -D /usr/local/var/postgres -l /usr/local/var/postgres/server.log start\nserver starting\n
"},{"location":"projects/leiningen/todo-app/postgres/postgres-commands/#create-local-database","title":"Create local database","text":"
heroku pg:pull HEROKU_POSTGRESQL_MAGENTA mylocaldb --app sushi\n

Create a new local database named mylocaldb, then pull data from database at DATABASE_URL from the app sushi.

In order to prevent accidental data overwrites and loss, the local database must not exist. You will be prompted to drop an already existing local database before proceeding.

"},{"location":"projects/leiningen/todo-app/postgres/postgres-commands/#pgpush","title":"pg:push","text":"

Like pull but in reverse, pg:push will push data from a local database into a remote Heroku Postgres database. The command looks like this:

heroku pg:push mylocaldb HEROKU_POSTGRESQL_MAGENTA --app sushi\n

This command will take the local database mylocaldb and push it to the database at DATABASE_URL on the app sushi. In order to prevent accidental data overwrites and loss, the remote database must be empty. You will be prompted to pg:reset an already a remote database that is not empty.

"},{"location":"projects/leiningen/todo-app/postgres/postgres-commands/#backups","title":"Backups","text":"

Heroku Postgres Backups service automates backup of the database pointed to by the DATABASE_URL environment variable in the Heroku app. Ensure the database is promoted

heroku pg:promote HEROKU_POSTGRESQL_VIOLET --app your-app\n
"},{"location":"projects/leiningen/todo-app/postgres/postgres-commands/#monitoring-database-provisioning","title":"Monitoring database provisioning","text":"

When provisioning larger databases, they may take several minutes to become available. Using the heroku pg:wait command you can see when the database provisioning is complete.

You may also want to use heroku pg:wait when putting your application into maintenenace mod [TODO: expand on this]

heroku help pg:wait\n\n\nUsage: heroku pg:wait [DATABASE]\n\n monitor database creation, exit when complete\n\n defaults to all databases if no DATABASE is specified\n\n --wait-interval SECONDS      # how frequently to poll (to avoid rate-limiting)\n
"},{"location":"projects/leiningen/todo-app/postgres/postgres-commands/#setting-a-name-for-a-new-database","title":"Setting a name for a new database","text":"

Once Heroku Postgres has been added a HEROKU_POSTGRESQL_COLOR_URL setting will be available in the app configuration and will contain the URL used to access the newly provisioned Heroku Postgres service. This can be confirmed using the heroku config command.

heroku config -s | grep HEROKU_POSTGRESQL\nHEROKU_POSTGRESQL_RED_URL=postgres://username:password@hostname.domain.com:1234/database-name\n

You can choose the alias that the add-on uses on the application using the --as flag. This will affect the name of the variable the add-on adds to the application:

heroku addons:create heroku-postgresql:hobby-dev --as USERS_DB\nAdding heroku-postgresql:hobby-dev to sushi... done, v69 (free)\nAttached as USERS_DB\nDatabase has been created and is available\n\n heroku config -s | grep USERS_DB\nUSERS_DB_URL=postgres://postgres://username:password@hostname.domain.com:1234/database-name\n
"},{"location":"projects/leiningen/todo-app/postgres/postgres-performance-analytics/","title":"Performance Analytics","text":"

Performance Analytics is the visibility suite for Heroku Postgres. It enables you to monitor the performance of your database and to diagnose potential problems. It consists of several components:

"},{"location":"projects/leiningen/todo-app/postgres/postgres-performance-analytics/#expensive-queries","title":"Expensive Queries","text":"

The leading cause of poor database performance are queries that are not optimised . Expensive Queries reports, available through the Heroku dashboard helps to identify and understand the queries that take the most time in your database.

"},{"location":"projects/leiningen/todo-app/postgres/postgres-performance-analytics/#logging","title":"Logging","text":"

If your service emits logs on database access, you will be able to retrieve them through Heroku\u2019s log-stream:

 heroku logs -t\n

To see logs from the database service itself you can also use heroku logs but with the -p postgres flag indicating that you only wish to see the logs from PostgreSQL.

 heroku logs -p postgres -t\n

In order to have minimal impact on database performance, logs are delivered on a best-effort basis.

Read more about Heroku Postgres log statements here.

"},{"location":"projects/leiningen/todo-app/postgres/postgres-performance-analytics/#pgdiagnose","title":"pg:diagnose","text":"

pg:diagnose performs a number of useful health and diagnostic checks that help analyst and optimize the performance of a database. The report that can be shared with others on your team or with Heroku Support.

Before taking any action based on a report, be sure to carefully consider the impact to your database and application.

 heroku pg:diagnose --app sushi\nReport 1234abc\u2026 for sushi::HEROKU_POSTGRESQL_MAROON_URL\navailable for one month after creation on 2014-07-03 21:29:40.868968+00\n\nGREEN: Connection Count\nGREEN: Long Queries\nGREEN: Idle in Transaction\nGREEN: Indexes\nGREEN: Bloat\nGREEN: Hit Rate\nGREEN: Blocking Queries\nGREEN: Load\n
"},{"location":"projects/leiningen/todo-app/postgres/postgres-performance-analytics/#check-connection-count","title":"Check: Connection Count","text":"

Each Postgres connection requires memory. And database plans have a limit on the number of connections they can accept. If you are using too many connections you may want to consider using a connection pool such as PgBouncer or migrating to a larger plan with more RAM.

"},{"location":"projects/leiningen/todo-app/postgres/postgres-performance-analytics/#long-running-queries-idle-in-transaction","title":"Long Running Queries, Idle in Transaction","text":"

Long-running queries and transactions can cause problems with bloat that prevents auto vacuuming and causes followers to lag behind. They also create locks on your data which can prevent other transactions from running. You may want to consider killing the long running query with pg:kill.

"},{"location":"projects/leiningen/todo-app/postgres/postgres-performance-analytics/#check-indexes","title":"Check: Indexes","text":"

The Indexes check includes three classes of indexes.

Never Used Indexes have not been used (since the last manual database statistics refresh). These indexes are typically safe to drop, unless they are in use on a follower.

Low Scans, High Writes indexes are used, but infrequently relative to their write volume. Indexes are updated on every write, so are especially costly on a high write table. Consider the cost of slower writes against the performance improvements that these indexes provide.

Seldom used Large Indexes are not used often and take up a significant space both on disk and in cache (RAM). These indexes may still be important to your application for example if they are used by periodic jobs or infrequent traffic patterns.

Index usage is only tracked on the database receiving the query. If you use followers for reads, this check will not account for usage made against the follower and is likely inaccurate.

"},{"location":"projects/leiningen/todo-app/postgres/postgres-performance-analytics/#check-bloat","title":"Check: Bloat","text":"

Because Postgres uses MVCC, old versions of updated or deleted rows are simply made invisible rather than modified in place.

Under normal operation an auto vacuum process goes through and asynchronously cleans these up. However sometimes it cannot work fast enough or otherwise cannot prevent some tables from becoming bloated.

High bloat can slow down queries, waste space, and even increase load as the database spends more time looking through dead rows.

You can manually vacuum a table with the VACUUM (VERBOSE, ANALYZE); command in psql. If this occurs frequently you may want to make auto-vacuum more aggressive.

"},{"location":"projects/leiningen/todo-app/postgres/postgres-performance-analytics/#check-hit-rate","title":"Check: Hit Rate","text":"

This checks the overall index hit rate, the overall cache hit rate, and the individual index hit rate per table. It is very important to keep hit rates in the 99+% range. Databases with lower hit rates perform significantly worse as they have to hit disk instead of reading from memory. Consider migrating to a larger plan for low cache hit rates, and adding appropriate indexes for low index hit rates. Check: Blocking Queries

Some queries can take locks that block other queries from running. Normally these locks are acquired and released very quickly and do not cause any issues. In pathological situations however some queries can take locks that cause significant problems if held too long. You may want to consider killing the query with pg:kill.

"},{"location":"projects/leiningen/todo-app/postgres/postgres-performance-analytics/#check-load","title":"Check: Load","text":"

There are many, many reasons that load can be high on a database: bloat, CPU intensive queries, index building, and simply too much activity on the database. Review your access patterns, and consider migrating to a larger plan which would have a more powerful processor.

"},{"location":"projects/leiningen/todo-app/postgres/postgres-toolbelt-commands/","title":"Postgres toolbelt commands","text":""},{"location":"projects/leiningen/todo-app/postgres/postgres-toolbelt-commands/#heroku-toolbelt-for-postgres","title":"Heroku Toolbelt for Postgres","text":"

Heroku Postgres is integrated directly into the Heroku toolbelt and offers several commands that automate many common tasks associated with managing a database-backed application.

psql required for some commands

Some commands require a postgres client to be installed on your computer to work

"},{"location":"projects/leiningen/todo-app/postgres/postgres-toolbelt-commands/#pginfo","title":"pg:info","text":"

To see all PostgreSQL databases provisioned by your application and the identifying characteristics of each (db size, status, number of tables, PG version, creation date etc\u2026) use the heroku pg:info command.

 heroku pg:info\n=== HEROKU_POSTGRESQL_RED\nPlan         Standard 0\nStatus       available\nData Size    82.8 GB\nTables       13\nPG Version   9.1.3\nCreated      2012-02-15 09:58 PDT\n=== HEROKU_POSTGRESQL_GRAY\nPlan         Standard 2\nStatus       available\nData Size    82.8 GB\n

To continuously monitor the status of your database, pass pg:info through the unix watch command:

 watch heroku pg:info\n
"},{"location":"projects/leiningen/todo-app/postgres/postgres-toolbelt-commands/#pgpsql","title":"pg:psql","text":"

psql is the native PostgreSQL interactive terminal and is used to execute queries and issue commands to the connected database.

To establish a psql session with your remote database use heroku pg:psql.

You must have PostgreSQL installed on your system to use heroku pg:psql.

 heroku pg:psql\nConnecting to HEROKU_POSTGRESQL_RED... done\npsql (9.1.3, server 9.1.3)\nSSL connection (cipher: DHE-RSA-AES256-SHA, bits: 256)\nType \"help\" for help.\n\nrd2lk8ev3jt5j50=> SELECT * FROM users;\n\nIf you have more than one database, specify the database to connect to (just the color works as a shorthand) as the first argument to the command (the database located at DATABASE_URL is used by default).\n\n heroku pg:psql gray\nConnecting to HEROKU_POSTGRESQL_GRAY... done\n
"},{"location":"projects/leiningen/todo-app/postgres/postgres-toolbelt-commands/#pgpush-and-pgpull","title":"pg:push and pg:pull","text":"

pg:pull can be used to pull remote data from a Heroku Postgres database to a database on your local machine. The command looks like this:

 heroku pg:pull HEROKU_POSTGRESQL_MAGENTA mylocaldb --app sushi\n

This command will create a new local database named \u201cmylocaldb\u201d and then pull data from database at DATABASE_URL from the app \u201csushi\u201d. In order to prevent accidental data overwrites and loss, the local database must not exist. You will be prompted to drop an already existing local database before proceeding.

If providing a Postgres user or password for your local DB is necessary, use the appropriate environment variables like so:

PGUSER=postgres PGPASSWORD=password heroku pg:pull HEROKU_POSTGRESQL_MAGENTA mylocaldb --app sushi

Note: like all pg:* commands you can use the shorthand identifiers here, so to pull data from HEROKU_POSTGRESQL_RED on the app \u201csushi\u201d you could do heroku pg:pull sushi::RED mylocaldb. pg:push

Like pull but in reverse, pg:push will push data from a local database into a remote Heroku Postgres database. The command looks like this:

 heroku pg:push mylocaldb HEROKU_POSTGRESQL_MAGENTA --app sushi\n

This command will take the local database \u201cmylocaldb\u201d and push it to the database at DATABASE_URL on the app \u201csushi\u201d. In order to prevent accidental data overwrites and loss, the remote database must be empty. You will be prompted to pg:reset an already a remote database that is not empty.

Usage of the PGUSER and PGPASSWORD for your local database is also supported for pg:push, just like for the pg:pull commands. Troubleshooting

These commands rely on the pg_dump and pg_restore binaries that are included in a Postgres installation. It is somewhat common, however, for the wrong binaries to be loaded in $PATH. Errors such as

!    createdb: could not connect to database postgres: could not connect to server: No such file or directory\n!      Is the server running locally and accepting\n!      connections on Unix domain socket \"/var/pgsql_socket/.s.PGSQL.5432\"?\n!\n!    Unable to create new local database. Ensure your local Postgres is working and try again.\n
and

pg_dump: server version: 9.3.1; pg_dump version: 9.1.5\npg_dump: aborting because of server version mismatch\npg_dump: *** aborted because of error\npg_restore: [archiver] input file is too short (read 0, expected 5)\n

are both often a result of this incorrect $PATH problem. This problem is especially common with Postgres.app users, as the post-install step of adding /Applications/Postgres.app/Contents/MacOS/bin to $PATH is easy to forget.

"},{"location":"projects/leiningen/todo-app/postgres/postgres-toolbelt-commands/#pgps-pgkill-pgkillall","title":"pg:ps pg:kill pg:killall","text":"

These commands give you view and control over currently running queries.

The pg:ps command queries the pg_stat_statements table in postgres to give a concise view into currently running queries.

 heroku pg:ps\n procpid |         source            |   running_for   | waiting |         query\n---------|---------------------------|-----------------|---------|-----------------------\n   31776 | psql                      | 00:19:08.017088 | f       | <IDLE> in transaction\n   31912 | psql                      | 00:18:56.12178  | t       | select * from hello;\n   32670 | Heroku Postgres Data Clip | 00:00:25.625609 | f       | BEGIN READ ONLY; select 'hi'\n(3 rows)\n

The procpid column can then be used to cancel or terminate those queries with pg:kill. Without any arguments pg_cancel_backend is called on the query which will attempt to cancel the query. In some situations that can fail, in which case the --force option can be used to issue pg_terminate_backend which drops the entire connection for that query.

 heroku pg:kill 31912\n pg_cancel_backend\n-------------------\n t\n(1 row)\n\n heroku pg:kill --force 32670\n pg_terminate_backend\n----------------------\n t\n(1 row)\n

pg:killall is similar to pg:kill except it will cancel or terminate every query on your database.

"},{"location":"projects/leiningen/todo-app/postgres/postgres-toolbelt-commands/#pgpromote","title":"pg:promote","text":"

In setups where more than one database is provisioned (common use-cases include a master/slave high-availability setup or as part of the database upgrade process) it is often necessary to promote an auxiliary database to the primary role. This is accomplished with the heroku pg:promote command.

 heroku pg:promote HEROKU_POSTGRESQL_GRAY_URL\nPromoting HEROKU_POSTGRESQL_GRAY_URL to DATABASE_URL... done\n

pg:promote works by setting the value of the DATABASE_URL config var (which your application uses to connect to the primary database) to the newly promoted database\u2019s URL and restarting your app. The old primary database location is still accessible via its HEROKU_POSTGRESQL_COLOR_URL setting.

After a promotion, the demoted database is still provisioned and incurring charges. If it\u2019s no longer need you can remove it with

heroku addons:destroy HEROKU_POSTGRESQL_COLOR.\n
"},{"location":"projects/leiningen/todo-app/postgres/postgres-toolbelt-commands/#pgcredentials","title":"pg:credentials","text":"

Heroku Postgres provides convenient access to the credentials and location of your database should you want to use a GUI to access your instance.

The database name argument must be provided with pg:credentials command. Use DATABASE for your primary database.

 heroku pg:credentials DATABASE\nConnection info string:\n   \"dbname=dee932clc3mg8h host=ec2-123-73-145-214.compute-1.amazonaws.com port=6212 user=user3121 password=98kd8a9 sslmode=require\"\n

It is a good security practice to rotate the credentials for important services on a regular basis. On Heroku Postgres this can be done with heroku pg:credentials --reset.

 heroku pg:credentials HEROKU_POSTGRESQL_GRAY_URL --reset\n

New credentials are created for the database and the related config vars on your Heroku application are updated.

On Standard, Premium, and Enterprise tier databases the old credentials are not removed immediately.

All of the open connections remain open until the currently running tasks complete, then those credentials are updated. This is to make sure that any background jobs or other workers running on your production environment aren\u2019t abruptly terminated, potentially leaving the system in an inconsistent state.

"},{"location":"projects/leiningen/todo-app/postgres/postgres-toolbelt-commands/#pgreset","title":"pg:reset","text":"

The PostgreSQL user your database is assigned doesn\u2019t have permission to create or drop databases. To drop and recreate your database use pg:reset.

 heroku pg:reset DATABASE\n
"},{"location":"projects/leiningen/todo-app/refactor-namespace/","title":"Refactor Core Namespace","text":"

The todo-list.core namespace is getting quite full and only going to get more code, unless we refactor the design and create some additional namespaces.

A namespace is a way to group behaviour (functions) and data (data structures / defs) in one scope. Functions can be defined as private to that scope, so only other functions in the same namespace can call them.

"},{"location":"projects/leiningen/todo-app/refactor-namespace/base-routes/","title":"Base routes","text":"

handlers.clj should look as follows:

(ns todo-list.handlers\n   (:use\n     [hiccup.core]\n     [hiccup.page]))\n\n\n(defn welcome\n  \"A ring handler to respond with a simple welcome message\"\n  [request]\n  (html [:h1 \"Hello, Clojure World\"]\n        [:p \"Welcome to your first Clojure app, I now update automatically\"]))\n\n(defn goodbye\n  \"A song to wish you goodbye\"\n  [request]\n    (html5 {:lang \"en\"}\n           [:head (include-js \"myscript.js\") (include-css \"mystyle.css\")]\n           [:body\n            [:div [:h1 {:class \"info\"} \"Walking back to happiness\"]]\n            [:div [:p \"Walking back to happiness with you\"]]\n            [:div [:p \"Said, Farewell to loneliness I knew\"]]\n            [:div [:p \"Laid aside foolish pride\"]]\n            [:div [:p \"Learnt the truth from tears I cried\"]]]))\n\n(defn about\n  \"Information about the website developer\"\n  [request]\n  (html [:h1 \"About the Website\"]\n        [:p \"I am an awesome Clojure developer, well getting there... trying some Hiccup now\"]))\n\n(defn hello\n  \"A simple personalised greeting showing the use of variable path elements\"\n  [request]\n  (let [name (get-in request [:route-params :name])]\n    {:status 200\n     :body (str \"Hello \" name \".  I got your name from the web URL\")\n     :headers {}}))\n\n(def operands {\"+\" + \"-\" - \"*\" * \":\" /})\n\n(defn calculator\n  \"A very simple calculator that can add, divide, subtract and multiply.  This is done through the magic of variable path elements.\"\n  [request]\n  (let [a  (Integer. (get-in request [:route-params :a]))\n        b  (Integer. (get-in request [:route-params :b]))\n        op (get-in request [:route-params :op])\n        f  (get operands op)]\n    (if f\n      {:status 200\n       :body (str \"<h1>Result = \" (f a b) \"</h1>\")\n       :headers {}}\n      {:status 404\n       :body \"Sorry, unknown operator.  I only recognise + - * : (: is for division)\"\n       :headers {}})))\n\n(defn trying-hiccup\n  [request]\n  (html5 {:lang \"en\"}\n         [:head (include-js \"myscript.js\") (include-css \"mystyle.css\")]\n         [:body\n           [:div [:h1 {:class \"info\"} \"This is Hiccup\"]]\n           [:div [:p \"Take a look at the HTML generated in this page, compared to the about page\"]]\n           [:div [:p \"Style-wise there is no difference between the pages as we haven't added anything in the stylesheet, however the hiccup page generates a more complete page in terms of HTML\"]]]))\n
"},{"location":"projects/leiningen/todo-app/refactor-namespace/base-routes/#note-create-a-new-file-called-srctodo_listhandlersbase-routesclj-and-move-all-the-handler-code-into-this-file-from-srctodo_listcore-make-sure-you-also-move-the-hiccup-libraries-into-the-new-handlers-namespace","title":"Note:: Create a new file called src/todo_list/handlers/base-routes.clj and move all the handler code into this file from src/todo_list/core. Make sure you also move the hiccup libraries into the new handlers namespace.","text":""},{"location":"projects/leiningen/todo-app/refactor-namespace/code-so-far/","title":"Code so far","text":""},{"location":"projects/leiningen/todo-app/refactor-namespace/core/","title":"Refactored Core","text":"

Refactor the core namespace to contain the code that starts up our server and join up all the route handlers.

Edit the src/todo_list/core.clj file and update the namespace definition to include the new handlers namespace, including the whole namespace in core.

(ns todo-list.core\n  (:require [compojure.core          :refer [routes]]\n            [todo-list.handlers.play :refer [play-routes]]\n            [todo-list.handlers.task :refer [task-routes]]\n            [todo-list.handlers.base :refer [base-routes]]\n            [ring.middleware.reload  :refer [wrap-reload]]\n            [ring.adapter.jetty      :as    jetty]))\n

Change app from a defroutes to a def and use the route function to merge all the defroutes into one

(def app\n  (routes #'play-routes #'base-routes #'task-routes))\n

The routes function takes the names of all the other defroutes and merges into one list of handlers.

app is now just a name we give to reference the handlers for all routes. We can continue to add more defroutes to app as our application grows, along with any middleware we wish to apply to our handlers.

core should be much smaller, containing only the route definition and the main app (plus the middleware around the app)

(ns todo-list.core\n  (:require [compojure.core          :refer [routes]]\n            [todo-list.handlers.play :refer [play-routes]]\n            [todo-list.handlers.task :refer [task-routes]]\n            [todo-list.handlers.base :refer [base-routes]]\n            [ring.middleware.reload  :refer [wrap-reload]]\n            [ring.adapter.jetty      :as    jetty]))\n\n(def app\n  (routes #'play-routes #'base-routes #'task-routes))\n\n(defn -main\n  \"A very simple web server using Ring & Jetty\"\n  [port-number]\n  (jetty/run-jetty app\n    {:port (Integer. port-number)}))\n\n(defn -dev-main\n  \"A very simple web server using Ring & Jetty that reloads code changes via the development profile of Leiningen\"\n  [port-number]\n  (jetty/run-jetty (wrap-reload #'app)\n                   {:port (Integer. port-number)}))\n
"},{"location":"projects/leiningen/todo-app/refactor-namespace/play-routes/","title":"Play routes","text":""},{"location":"projects/leiningen/todo-app/refactor-namespace/task-routes/","title":"Task routes","text":""},{"location":"projects/leiningen/todo-app/reloading-the-application/","title":"Automatic reloading with wrap-reload middleware","text":"

wrap-reload is a ring middleware function that will push all our code changes to the application each time we save.

"},{"location":"projects/leiningen/todo-app/reloading-the-application/#note-include-wrap-reload-in-the-namespace-of-our-project","title":"Note:: Include wrap-reload in the namespace of our project","text":"

Require the wrap-reload directly into the namespace

(ns todo-list.core\n  (:require [ring.adapter.jetty :as jetty]\n            [ring.middleware.reload :refer [wrap-reload]]))\n
"},{"location":"projects/leiningen/todo-app/reloading-the-application/#define-a-function-to-use-wrap-reload","title":"Define a function to use wrap-reload","text":"

A function called -dev-main will run the reloading web server when we are developing, ensuring we only use the wrap-reload function during development.

"},{"location":"projects/leiningen/todo-app/reloading-the-application/#note-create-a-dev-main-function","title":"Note:: Create a -dev-main function","text":"

The -dev-main function is the same as -main, except we use the wrap-reload middleware around the welcome function. Each time you change the welcome function definition it will be reloaded.

Using the quote reader macro, #' in front of the welcome function name tells Clojure to skip evaluation of the function and reference the name of the function instead. This allows the wrap-reload middleware to decide when to evaluate the welcome function.

(defn -dev-main\n  \"A very simple web server using Ring & Jetty,\n  called via the development profile of Leiningen\n  which reloads code changes using ring middleware wrap-reload\"\n  [port-number]\n  (webserver/run-jetty\n    (wrap-reload #'welcome)\n    {:port  (Integer. port-number)\n     :join? false}))\n
"},{"location":"projects/leiningen/todo-app/reloading-the-application/#notetweak-the-main-function-for-production","title":"Note::Tweak the -main function for production","text":"

The -main function is typically called on the command line when run in production, so we want to be connected to the output of the webserver.

Remove the :join? false option for the embedded Jetty server, so the output of the server is displayed

(defn -main\n  \"A very simple web server using Ring & Jetty\n  Production mode operation, no reloading.\"\n  [port-number]\n  (webserver/run-jetty\n    welcome\n    {:port (Integer. port-number)}))\n
"},{"location":"projects/leiningen/todo-app/reloading-the-application/#configure-the-dev-profile-in-your-project","title":"Configure the dev profile in your project","text":"

When you start your Clojure webapp with lein run it looks for main class to run in the :dev profile first. So we need to create a :dev profile.

:dev profile that sets -dev-main to be the starting point of our application. This

:profiles {:dev\n            {:main todo-list.core/-dev-main}}\n

The project.clj file should look like the following:

(defproject todo-list \"0.1.0-SNAPSHOT\"\n\n  :description \"A Todo List server-side webapp using Ring & Compojure\"\n  :url \"https://github.com/practicalli/clojure-todo-list-example\"\n\n  :license {:name \"Creative Commons Attribution Share-Alike 4.0 International\"\n            :url  \"https://creativecommons.org\"}\n\n  :dependencies [[org.clojure/clojure \"1.10.1\"]\n                 [ring \"1.8.0\"]]\n\n  :repl-options {:init-ns todo-list.core}\n\n  :main todo-list.core\n\n  :profiles {:dev\n             {:main todo-list.core/-dev-main}})\n
"},{"location":"projects/leiningen/todo-app/reloading-the-application/#noteadd-profile-to-project-configuration","title":"Note::Add profile to project configuration","text":"

Edit the project.clj and create a :dev profile to define the initial function to call when starting our webapp.

"},{"location":"projects/leiningen/todo-app/reloading-the-application/code-so-far/","title":"The code so far","text":"

The code and configuration we have created so far are in the clojure-todo-list-example repository github repository.

Code for this section is in the branch called 03-reloading-the-application

If something is not working or you want to speed up, simply clone the project into a new directory using the command:

git clone https://github.com/practicalli/clojure-todo-list-example\n
Once you have cloned the project, checkout the 03-reloading-the-application branch

git checkout 03-reloading-the-application\n
"},{"location":"projects/leiningen/todo-app/reloading-the-application/middleware/","title":"Middleware in Ring","text":"

Middleware in ring is a way to modify the incoming requests or outgoing responses.

Middleware can also wrap handlers or other middleware, affecting their behaviour. For example the wrap-reload middleware enables live reloading by detecting file changes and reloading affected functions into their namespace, before the request is passed to the relevant handler function

Middleware in ring/ring-core

  • wrap-cookies (ring.middleware.cookies)
  • wrap-file (ring.middleware.file)
  • wrap-file-info (ring.middleware.file-info)
  • wrap-flash (ring.middleware.flash)
  • wrap-keyword-params (ring.middleware.keyword-params)
  • wrap-multipart-params (ring.middleware.multipart-params
  • wrap-nested-params (ring.middleware.nested-params
  • wrap-params (ring.middleware.params)
  • wrap-session (ring.middleware.session)

Middleware in ring/ring-devel

  • wrap-lint (ring.middleware.lint)
  • wrap-reload (ring.middleware.reload)
  • wrap-stacktrace (ring.middleware.stacktrace)
"},{"location":"projects/leiningen/todo-app/reloading-the-application/test-your-code-reloads/","title":"Test the wrap-reload middleware","text":"

Make a change to the welcome function code and check that it automatically reloads.

Change the default response text in the welcome function

Open the webapp in the browser http://localhost:8000.

Make a change to the code in the welcome function, altering the text of the :body in the default request.

(defn welcome\n  \"A ring handler to process all requests for the web server.\n  If a request is for something other than `/` then an error message is returned\"\n  [request]\n  (if (= \"/\" (:uri request))\n    {:status  200\n     :headers {}\n     :body    \"<h1>Hello, Clojure World</h1>\n               <p>Welcome to your first Clojure app.</p>\n               <p>I now reload changes automatically</p> \"}\n    {:status  404\n     :headers {}\n     :body    \"<h1>This is not the page you are looking for</h1>\n               <p>Sorry, the page you requested was not found!</p>\"}))\n

Save the code change and refresh your browser page, you should now see the updated message.

"},{"location":"projects/leiningen/todo-app/task-handlers/","title":"Task handlers","text":"

Now we have functions that create the database and add / remove tasks, we can provide handlers to call these functions and therefore enable the user to add and remove tasks.

In the following pages we will create handlers and use hiccup for the html markup.

"},{"location":"projects/leiningen/todo-app/task-handlers/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"projects/leiningen/todo-app/task-handlers/add-a-task/","title":"Add a task","text":"

which namespace are these going in

(ns todo-list.handlers.tasks :requires models....)

"},{"location":"projects/leiningen/todo-app/task-handlers/add-a-task/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"projects/leiningen/todo-app/task-handlers/delete-a-task/","title":"Delete a task","text":""},{"location":"projects/leiningen/todo-app/task-handlers/delete-a-task/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"projects/leiningen/todo-app/task-handlers/show-task/","title":"Show tasks","text":""},{"location":"projects/leiningen/todo-app/task-handlers/show-task/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"projects/leiningen/todo-app/unit-test-handler-function/","title":"Unit Test handler functions","text":"

Handler functions can be tested with unit tests as they are just pure functions. All handlers take a request hash-map and return a response hash-map. So its easy to give each handler a hash-map as an argument and test that we get the expected response hash-map in return.

There is no need to mock the framework until we do integration level testing, where we are testing the full lifecycle of request-response.

It is useful to have separate unit and integration tests to quickly narrow down the root cause of issues.

"},{"location":"projects/leiningen/todo-app/unit-test-handler-function/#unit-test-branch","title":"Unit test branch","text":"

The unit tests are placed under test/full_namespace_path/ and reside in files with the same names as the source code filenames, with -test postfixed to the end.

src/practicalli/simple_webapp/handlers.clj\ntest/practicalli/simple_webapp/handlers-test.clj\n
"},{"location":"projects/leiningen/todo-app/unit-test-handler-function/#writing-unit-tests","title":"Writing unit tests","text":"

clojure.test is used to write unit tests for handlers, as we are just treating them as functions.

"},{"location":"projects/leiningen/working-example/","title":"A working example","text":"

As we don't have time to build a full web app in this workshop, here is one that was built early.

"},{"location":"projects/leiningen/working-example/#note-checkout-the-shouter-code-from-gitub","title":"Note:: Checkout the Shouter code from Gitub","text":""},{"location":"projects/slack-app/","title":"Clojure powered Slack Application","text":"

Slack applications provide a way to extend the functionality of Slack and provide integration with other services, e.g. GitHub, Atlassian Jira & Confluence, etc.

"},{"location":"projects/slack-app/#development-process-overview","title":"Development process Overview","text":"
  • create a slack account and slack space (to test the application)
  • create a slack app
  • deploy the slack app in the slack test workspace created previously
  • Create a clojure project and interact with the Slack API
  • Setup ngrok domain for Slack app to access a locally running Clojure web service (to respond to interaction with the app - assuming the slack isnt just push driven)
  • alternatively, webservices connection can be configured between the locally running Clojure app and slack (desktop app required? - how does this work...)

  • deploy clojure application to public cloud (AWS, Render.com, etc)

"},{"location":"projects/slack-app/create-slack-app/","title":"Create Slack App","text":"

Create a Slack account and follow the prompts to create a new workspace. The workspace will be used to deploy the Slack App for testing purposes.

Use an existing Slack workspace if sufficient administration privelleges are available (and you wont affect other peoples use of Slack)

Slack Quickstart describes how to create a Slack app.

Create a new Slack app with the Slack UI

Select From Scratch

Enter App Name and select the Development Workspace to experiment and build the app.

Regardless of development workspace, the app can be distributed to any other workspaces.

The Slack Web UI displays the basic information for the newly created Slack app.

"},{"location":"projects/slack-app/create-slack-app/#configure-scopes","title":"Configure scopes","text":"

Add a scope to post messages to a channel

Sidebar > OAuth & Permissions > Scopes > Add an OAuth Scope

Add the following Bot Token Scopes

  • chat:write scope to the Bot Token to allow the app to post messages
  • channels:read scope too so your app can gain knowledge about public Slack channels

Reisntall the app if changing scopes and other features

"},{"location":"projects/slack-app/create-slack-app/#update-display-information","title":"Update Display information","text":"

This feels like it should have been done before installing the app

Provide a short & long description of the app and set background colour. Optionally add an app icon (between 512 and 2000 px in size).

  • Short description: A random function from the Clojure Standard Library
  • Long description: A random function is selected from the Clojure Standard Library and displayed along with the documentation (doc string) to explain what the function does. A bonus feature will be to provide examples of function use.
"},{"location":"projects/slack-app/create-slack-app/#install-app-in-workspace","title":"Install app in workspace","text":"

Install your app to your Slack workspace to test it and generate the tokens you need to interact with the Slack API. You will be asked to authorize this app after clicking an install option.

Sidebar > Settings > Basic Information > Install your app

"},{"location":"projects/slack-app/create-slack-app/#authorization-token","title":"Authorization token","text":"

The Authorization token for the workspace is in the app management web page for the specific app

Sidebar > OAuth & Permissions > OAuth Tokens for Your Workspace

Create an environment variable to hold the authorization token with the value of the Bot User OAuth Token

export SLACK_AUTHENTICATION_TOKEN=xxxx-123412341234-12345123451245-...   \n

Environment variables used with Clojure must be set before running the REPL, so the variables are available to the Java Virtual Machine process.

Add the environment variables to .bashrc for Bash or .zshenv for Zsh

Access tokens represent the permissions delegated to the app by the installing user.

Avoid checking access tokens into version control

"},{"location":"projects/slack-app/create-slack-app/#test-app-save-for-later","title":"Test app - save for later...","text":"

Add the app to a public channel and test its (as yet unconfigured slash command)

Confirm the Slack App should be added to the selected workspace

"},{"location":"projects/slack-app/create-slack-app/#local-development","title":"Local Development","text":"

Use Socket Mode to route the app interactions and events over a WebSockets connection instead sending payloads to Request URLs, the public HTTP endpoints.

Socket mode is intended for internal apps that are in development or need to be deployed behind a firewall. It is not intended for widely distributed apps.

Alternatively, use ngrock to redirect requests to the local app.

"},{"location":"projects/slack-app/create-slack-app/#install-app-into-workspace","title":"Install app into workspace","text":"

Install Slack app into a workspace (not the development workspace)

Sidebar > Install App > Install App To Workspace > Slack OAuth UI

"},{"location":"projects/slack-app/slack-api-methods/","title":"Slack API Methods","text":"

An access token allows the calling of the methods described by the scopes requested during a Slack App installation, e.g., the chat:write scope allows an app to post messages.

The app isn't a member of any channels when installed. Choose a channel suitable for testing purposes and /invite the Slack app to the channel.

You can find the corresponding id for the channel that your app just joined by looking through the results of the conversations.list method:

curl https://slack.com/api/conversations.list -H \"Authorization: Bearer xoxb-1234...\"\n

You'll receive a list of conversation objects.

Now, post a message to the same channel your app just joined with the chat.postMessage method:

curl -X POST -F channel=C1234 -F text=\"Reminder: we've got a softball game tonight!\" https://slack.com/api/chat.postMessage -H \"Authorization: Bearer xoxb-1234...\"\n

Voila! We're already well on our way to putting a full-fledged Slack app on the table.

Web API Guide

API Methods list

Interactive Workflows

"},{"location":"projects/slack-app/slack-scopes/","title":"Slack Scopes Overview","text":"

Scopes give the app permission to carry out actions, e.g. post messages, in the development workspace.

Open the development workspace, either in a web page or in the Slack desktop app.

Sidebar > OAuth & Permissions > Scopes > Add an OAuth Scope

  • chat:write scope to the Bot Token to allow the app to post messages
  • channels:read scope too so your app can gain knowledge about public Slack channels
  • commands scope to build a Slash command.
  • incoming-webhook scope to use Incoming Webhooks.
  • chat:write.public scope to gain the ability to post in all public channels, without joining. Otherwise, you'll need to use conversations.join, or have your app invited by a user into a channel, before you can post.
  • chat:write.customize scope to adjust the app's message authorship to make use of the username, icon_url, and icon_emoji parameters in chat.postMessage.

Add scopes to the Bot Token.

Only add scopes to the User Token when the app needs to act as a specific user (e.g. post message as user, set user status, etc.)

If the scopes applied to a Slack App are changed, the Slack App must be redeployed to the workspace (and Slack App Directory) for the changes to take effect.

"},{"location":"projects/status-monitor-deps/","title":"Status Monitor project with Clojure tools","text":"

A status monitor dashboard to show operational status of a range of services.

A server-side web application using - ring and compojure for webapp request management - bulma CSS library for styling - hiccup for writing html in Clojure syntax - SVG graphics for status graphics - clojure.spec for validating functions and svg definitions in Clojure

"},{"location":"projects/status-monitor-deps/#creating-a-project","title":"Creating a project","text":"deps-newManual

Create a project using the app template and called practicalli/status-monitor. The :project/create alias from Practicalli Clojure CLI Config uses the deps-new project to create Clojure projects

clojure -T:project/create :template app :name practicalli/status-monitor\n
Some minor tweaks are made to the project before starting the application development

  • describe the project and how it can be used in the README
  • delete the LICENSE file and use a Creative Commons in the README
  • format the deps.edn file for readability

Create a project in a directory called status-monitor, with a deps.end file in the root of that directory deps.edn

{:paths [\"src\"]\n :deps {org.clojure/clojure {:mvn/version \"1.11.3\"}}}\n
Create a src/practicalli/status_monitor.clj file src/practicalli/status_monitor.clj
(ns practicalli.status-monitor)\n
Create a test/practicalli/status_monitor_test.clj file src/practicalli/status_monitor.clj
(ns practicalli.status-monitor-test\n  (:require [clojure.test :refer [deftest is testing]]))\n

practicalli/status-monitor

The code for this project can be found at practicalli/status-monitor

"},{"location":"projects/status-monitor-deps/application-server/","title":"Application Server","text":""},{"location":"projects/status-monitor-deps/application-server/#add-an-embedded-web-application-server","title":"Add an embedded web application server","text":"

The status monitor service runs on top of an application server which handles the infrastructure of messaging over https and other Internet protocols.

There are several libraries to provide this, ring and http-kit being the most common.

"},{"location":"projects/status-monitor-deps/application-server/#add-dependencies-for-application-server-and-routing","title":"Add dependencies for application server and routing","text":"

Edit the deps.edn file for the project.

Add the http-kit library library for the application server

Add the compojure library for routing of requests

 :deps\n {org.clojure/clojure {:mvn/version \"1.10.1\"}\n  http-kit            {:mvn/version \"2.3.0\"}\n  compojure           {:mvn/version \"1.6.1\"}}\n
"},{"location":"projects/status-monitor-deps/application-server/#add-code-to-start-the-application-server","title":"Add code to start the application server","text":"

Edit the src/practicalli/status_monitor_service.clj file

Include the http-kit server namespace and the compojure core namespace as requires in the ns definition.

(ns practicalli.status-monitor-service\n  (:gen-class)\n  (:require [org.httpkit.server :as app-server]\n            [compojure.core :refer [defroutes GET]]))\n

Add an atom to hold a reference to the running application server. When the server is not running, the atom contains nil.

(defonce app-server-instance (atom nil))\n

Update the -main function to start the application server using http-kit run-server, optionally setting the port number the server should run on.

(defn -main\n  \"Start the application server and run the application\"\n  [port]\n  (println \"INFO: Starting server on port: \" port)\n\n  (reset! app-server-instance\n          (app-server/run-server #'status-monitor {:port (Integer/parseInt port)})))\n
"},{"location":"projects/status-monitor-deps/application-server/#define-a-default-route-and-handler","title":"Define a default route and handler","text":"

Using the compojure defroutes function, define the default route and response when the status-monitor app received a request on the main URL, (eg. http://localhost:8888/)

(defroutes status-monitor\n  (GET \"/\" [] {:status 200 :body \"Status Monitor Dashboard\"}))\n

The route returns a hash-map that is the form of a response map. http-kit server transforms all response maps into https responses that are sent back to the requesting web browser.

"},{"location":"projects/status-monitor-deps/application-server/#stop-and-restart-server-from-repl","title":"Stop and restart server from REPL","text":"

Add functions to stop and restart the server, so change to the application code can be loaded in without having to stop the Clojure REPL.

Use the value in the app-server-instance atom to determine if the app-server is already running. If so, then send the instance the :timeout key with a value of time to shut itself down.

(defn stop-app-server\n  \"Gracefully shutdown the server, waiting 100ms\"\n  []\n  (when-not (nil? @app-server-instance)\n    (@app-server-instance :timeout 100)\n    (reset! app-server-instance nil)\n    (println \"INFO: Application server stopped\")))\n

With a REPL running the project, the server is started calling (-main) and stopped by calling (stop-app-server). A restart function is simply calling the stop and start functions.

(defn restart-app-server\n  \"Convenience function to stop and start the application server\"\n  []\n  (stop-app-server)\n  (-main))\n

Component lifecycle service

This approach is the essence of component lifecycle services such as mount, component and integrant.

Use the mount library if you are starting with component lifecycle services or require a clean and simple approach. Try integrant to take a data centric approach to such a service.

"},{"location":"projects/status-monitor-deps/application-server/#repl-experiment-section","title":"REPL experiment section","text":"

To help use the code during development a comment body has been included with calls to start, stop and restart the application.

(comment\n\n  ;; start application\n  (-main)\n\n  ;; stop application\n  (stop-app-server)\n\n  ;; restart application\n  (restart-app-server)\n\n  )\n
"},{"location":"projects/status-monitor-deps/continuous-integration/","title":"Continuous Integration","text":"

To assist in the development of the application, CircleCI, a continuous integration service will be used.

Initially this will run all the unit tests for the application and report on the results. In another chapter, CircleCI will be used to package the application and deploy the application.

"},{"location":"projects/status-monitor-deps/continuous-integration/#alias-for-test-runner","title":"Alias for test runner","text":"

Edit deps.edn file and add an alias called :test/run that calls kaocha test runner on the code

  :test/run\n  {:extra-paths [\"test\"]\n   :extra-deps {lambdaisland/kaocha {:mvn/version \"1.71.1119\"}}\n   :main-opts   [\"-m\" \"kaocha.runner\"]\n   :exec-fn kaocha.runner/exec-fn\n   :exec-args {:randomize? false\n               :fail-fast? true}}\n

Check the test runner is working by running the clojure command with the :test/run alias in a terminal at the root of the Clojure project

clojure -X:test/run\n
"},{"location":"projects/status-monitor-deps/continuous-integration/#add-circleci-configuration","title":"Add CircleCI configuration","text":"

Edit .circleci/config.yml and add a configuration to build and test the Clojure application. The cimg/clojure:1.10.0 image contains OpenJDK 17 and the latest version of Clojure CLI, Leiningen and Babashka.

Run the clojure commands in the root of the project before adding the configuration, to ensure the commands work locally first.

version: 2.0\njobs:\n  build:\n    working_directory: ~/build\n    docker:\n      - image: cimg/clojure:1.10\n    environment:\n      JVM_OPTS: -Xmx3200m\n    steps:\n      - checkout\n      - restore_cache:\n          key: status-monitor-service-{{ checksum \"deps.edn\" }}\n      - cache-dependencies\n      - run: clojure -P\n      - save_cache:\n          paths:\n            - ~/.m2\n            - ~/.gitlibs\n          key: status-monitor-service-{{ checksum \"deps.edn\" }}\n      - Unit-testing\n      - run: clojure -X:test/run\n
"},{"location":"projects/status-monitor-deps/continuous-integration/#add-project-on-circleci","title":"Add project on CircleCI","text":"

Visit the CircleCI dashboard and select Add Projects. Find the status-monitor-service repository and select Set Up Project button.

Choose the Add Manual install and Start Building

"},{"location":"projects/status-monitor-deps/debugging-requests/","title":"Debug ring requests","text":"

Requests are Clojure hash-maps so are easy to extract data from in a meaningful way.

If getting unexpected results, checking the details received in the request is a fast way to diagnose issues by seeing the data. The ring/ring-devel library contains a handle-dump function which displays the request parameters in a web page.

"},{"location":"projects/status-monitor-deps/debugging-requests/#add-ring-development-library","title":"Add ring development library","text":"

Add the :env/dev alias to include the ring/ring-devel library as a dependency. The ring-devel library includes functions for developing and debugging ring applications.

 :deps\n {org.clojure/clojure {:mvn/version \"1.10.3\"}\n  http-kit/http-kit   {:mvn/version \"2.5.3\"}\n  ring/ring-core      {:mvn/version \"1.9.5\"}\n  compojure/compojure {:mvn/version \"1.6.2\"}}\n\n :aliases\n {:env/dev\n  {:extra-deps {ring/ring-devel {:mvn/version \"1.8.1\"}}}}\n
"},{"location":"projects/status-monitor-deps/debugging-requests/#restart-repl","title":"Restart REPL","text":"

Dependencies are only added to the classpath when the REPL process starts, unless using the unofficial dependency hotload approach

Quit the REPL if it is already running

Start the REPL including the alias :env/dev. For example, run a rich terminal UI using rebel readline which also starts an nREPL sever:

clojure -M:env/dev:repl/rebel\n

:repl/rebel is defined in the user level configuration practicalli/clojure-deps-edn

Hotload libraries into a running REPL

Clojure CLI Hotload Libraries can add libraries to the class path without having to restart the REPL

"},{"location":"projects/status-monitor-deps/debugging-requests/#require-the-ringhandlerdump-namespace","title":"Require the ring.handler.dump namespace","text":"

Require the ring.handler.dump namespace in the ns form of practicalli.status-monitor-server namespace and refer the specific handle-dump function.

(ns practicalli.status-monitor-service\n  (:gen-class)\n  (:require\n   [org.httpkit.server       :as    app-server]\n   [compojure.core           :refer [defroutes GET]]\n   [compojure.route          :refer [not-found]]\n   [ring.handler.dump        :refer [handle-dump]]\n   [ring.util.response       :refer [response]]\n   [practicalli.helpers-http :refer [http-status-code]]))\n
"},{"location":"projects/status-monitor-deps/debugging-requests/#add-a-route-to-show-the-request-map","title":"Add a route to show the request map","text":"

Add a route that shows the request information using the handle-dump function.

(defroutes status-monitor\n  (GET \"/\" [] {:status (:OK http-status-code) :body \"Status Monitor Dashboard\"})\n  (GET \"/request-dump\" [] handle-dump))\n

(re)start the application server and visit the URL http://localhost:8080/request-dump

The request map details are also printed to the REPL buffer

{:remote-addr \"127.0.0.1\",\n :params {},\n :route-params {},\n :headers\n {\"accept\"\n  \"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\",\n  \"accept-encoding\" \"gzip, deflate\",\n  \"accept-language\" \"en-US,en;q=0.5\",\n  \"connection\" \"keep-alive\",\n  \"cookie\"\n  \"_ga=GA1.1.1141619352.1582159249; ring-session=0b3dc210-278e-4011-bc03-d8c2292b2c17; JSESSIONID=5RNxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxB; _gid=GA1.1.111111111.3333333333\",\n  \"host\" \"localhost:8080\",\n  \"upgrade-insecure-requests\" \"1\",\n  \"user-agent\"\n  \"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0\"},\n :async-channel\n #object[org.httpkit.server.AsyncChannel 0x359a37c8 \"/127.0.0.1:8080<->/127.0.0.1:38190\"],\n :server-port 8080,\n :content-length 0,\n :compojure/route [:get \"/request-dump\"],\n :websocket? false,\n :content-type nil,\n :character-encoding \"utf8\",\n :uri \"/request-dump\",\n :server-name \"localhost\",\n :query-string nil,\n :body nil,\n :scheme :http,\n :request-method :get}\n
"},{"location":"projects/status-monitor-deps/deployment-via-ci/","title":"Deployment Via Continuous Integration","text":"

Building on the CircleCI build pipeline created so far, the application will be deployed on Heroku if all the tests pass.

A workflow is added to the CircleCI configuration that deploys the application on Heroku from the source code. Heroku packages the application into an uberjar and then runs the application from that uberjar.

When commits in the Clojure project code are pushed to GitHub they are detected by CircleCI and the tests run. If the tests pass then the Heroku deployment stage starts.

TODO: Convert to tools.build approach

The depstar project has been retired (although still works) in favour of the official tools.build approach

"},{"location":"projects/status-monitor-deps/deployment-via-ci/#add-depstar-to-build-an-uberjar","title":"Add depstar to build an uberjar","text":"

Use the depstar tool to create a Java archive (jar) package of the application. The deps.edn configuration in the root of the project already contains an uberjar alias for this tool.

:project/uberjar\n{:replace-deps {com.github.seancorfield/depstar {:mvn/version \"2.1.303\"}}\n :exec-fn      hf.depstar/uberjar\n :exec-args    {:jar \"status-monitor-service.jar\"\n                :aot true}}\n

To try this on the command line:

clojure -X:project/uberjar\n

This will be the same command used in the build script

"},{"location":"projects/status-monitor-deps/deployment-via-ci/#create-a-custom-build-behaviour","title":"Create a custom build behaviour","text":"

Heroku build scripts use Leiningen by default. Configure Heroku to build with Clojure Tools, create a custom build file which will run instead of Leiningen.

Create a file called bin/build script in the root of the project

#!/usr/bin/env bash\nclojure -X:project/uberjar\n

Create an empty project.clj file so that Heroku recognized the project as Clojure.

"},{"location":"projects/status-monitor-deps/deployment-via-ci/#define-how-to-run-the-application","title":"Define how to run the application","text":"

Create a Procfile file in the root of the project directory containing the command to run the application.

Use the $PORT as an argument to the command. Heroku automatically assigns a port number for an application to listen upon when creating a contain in which the application will run. This port number is set using the PORT environment variable and is available to the application on startup. Using the PORT environment variable ensures the Clojure application will receive requests.

web: java -jar status-monitor-service.jar $PORT\n
"},{"location":"projects/status-monitor-deps/deployment-via-ci/#specifying-a-java-version","title":"Specifying a Java version","text":"

Create a system.properties and specify the Java version to use for the application. Java 1.8 is the default version use on Heroku, however, our development environment is Java 17, so add a property to set the Java runtime to version 17.

java.runtime.version=17\n
"},{"location":"projects/status-monitor-deps/deployment-via-ci/#heroku-configuration","title":"Heroku configuration","text":"

Login to the Heroku dashboard and create a new application.

In the Heroku dashboard, open the application Settings and add a Config Vars using the name CLOJURE_CLI_VERSION with a value of 1.10.1.727

"},{"location":"projects/status-monitor-deps/deployment-via-ci/#circleci-configuration-with-heroku-orb","title":"CircleCI configuration with Heroku Orb","text":"

Edit the .circleci/config.yml file and add the heroku orb and a workflow to call the orb task. The workflow has a dependency on the build job, so that will take place first.

The Heroku workflow will build the application from source code using the heroku/deploy-via-git. Only changes pushed to the live branch of the GitHub repository will be used in the Heroku deploy workflow.

Feature branches can be deployed on Heroku by creating an additional Heroku application and push the branch to it. Or use Heroku pipelines

version: 2.1\n\norbs:\n  heroku: circleci/heroku@1.2.6 # Invoke the Heroku orb\n\nworkflows:\n  heroku_deploy:\n    jobs:\n      - build\n      - heroku/deploy-via-git: # Use the pre-configured job, deploy-via-git\n          requires:\n            - build\n          filters:\n            branches:\n              only: live\n\njobs:\n  build:\n    working_directory: ~/build\n    docker:\n      - image: cimg/clojure:1.10\n    environment:\n      JVM_OPTS: -Xmx3200m\n    steps:\n      - checkout\n      - restore_cache:\n          key: status-monitor-service-{{ checksum \"deps.edn\" }}\n      - run: clojure -P\n      - save_cache:\n          paths:\n            - ~/.m2\n            - ~/.gitlibs\n          key: status-monitor-service-{{ checksum \"deps.edn\" }}\n      - run: clojure -X:test/run\n
"},{"location":"projects/status-monitor-deps/deployment-via-ci/#circleci-environment-variables","title":"CircleCI Environment Variables","text":"

Open the CircleCI and select project settings > Environment Variables

Add environment variables to define where the Heroku application can be found and a token to provide access.

Environment Variable Value HEROKU_API_KEY name of the application created on Heroku HEROKU_APP_NAME API key found in Account Settings > API Key"},{"location":"projects/status-monitor-deps/deployment-via-ci/#push-changes-to-trigger-build","title":"Push changes to trigger build","text":"

Commit the changed and push them to the GitHub repository. This triggers a build by CircleCI. The build downloads the dependencies and runs the unit tests. If the tests pass, then the Heroku deploy workflow starts.

The two stages can be seen in the dashboard as the pipeline runs.

Now visit the deployed Heroku application to see it in action.

"},{"location":"projects/status-monitor-deps/deployment-via-ci/#troubleshooting","title":"Troubleshooting","text":"

If there are issues, then use the Heroku toolbelt to look at the logs. In a command line terminal, issue the login command which opens a web browser to login to Heroku. Once logged in, run the heroku logs command to view the latest logs

heroku login\n\nheroku logs --app status-monitor-service\n

The logs can also be viewed live, as the application is being deployed by including the --tail option when running the heroku logs command in a terminal

heroku logs --app status-monitor-service --tail\n

The example Heroku logs show that the status-monitor-service is using the default port number if non is supplied as an argument, rather than Heroku assigned port. Heroku therefore considers the application as unresponsive and sets it status to crashed, tearing down the container the application is running in.

These logs were generated before adding the $PORT to the command in the Procfile.

"},{"location":"projects/status-monitor-deps/deployment-via-ci/#no-forced-pushes","title":"No forced pushes","text":"

Heroku doesn't like force Git pushes coming via CircleCI.

To get around this, either don't do force pushes to GitHub, or add the Heroku repository for the project as a remote to local git repository.

Heroku repository details in heroku dashboard Settings under App Information

Changes can now be pushed, ideally using force-with-lease to Heroku repository.

git push heroku live:master\n

Heroku only builds from a branch called master or main, so the above command pushes the local live branch to the remote master branch on Heroku.

"},{"location":"projects/status-monitor-deps/deployment-via-ci/#stopping-the-application","title":"Stopping the application","text":"

An application can be run for free on Heroku with the monthly free credits provided. However, to make the most out of these free credits then applications not in use should be shut down

Run the following command in the root of the Clojure project.

heroku ps:stop status-monitor-service\n
"},{"location":"projects/status-monitor-deps/refactor-handlers-and-tests/","title":"Refactor handlers and unit tests","text":"

Refactor the unit tests to use the ring-mock library to test handler functions. Create separate handler functions for the routes in defroute (there is only one custom handler at present).

(deftest dashboard-test\n  (testing \"Testing elements on the dashboard\"\n    (is (= (SUT/dashboard (mock/request :get \"/\"))\n           {:status  200\n            :body    \"Status Monitor Dashboard\"\n            :headers {}}))))\n

Create a handler for the GET \"/\" route

(defn dashboard\n  [request]\n  {:status (:OK http-status-code) :body \"Status Monitor Dashboard\" :headers {}})\n

Use the ring.util.response/response function to create a well formed response map which has a status code of 200. This removes the need to type in the response map structure explicitly which potentially can introduce bugs.

In the current namespace ns form, this function is required as an explicit refer, [ring.util.response :refer [response]], so its available to use by its unqualified name, response.

(defn dashboard\n  [request]\n  (response \"Status Monitor Dashboard\"))\n

Update the defroutes definition to call this handler rather than hard coding the response map.

(defroutes status-monitor\n  (GET \"/\" [] dashboard)\n  (GET \"/request-dump\" [] handle-dump)\n  )\n

Run the Cognitect test runner to check the unit tests are still passing after the code refactor.

clojure -M:env/dev:test:runner\n
"},{"location":"projects/status-monitor-deps/refactor-handlers-and-tests/#helper-functions","title":"Helper functions","text":"

The ring util library contains several other helper functions, bad-request, not-found and redirect:

(ring.util.response/bad-request \"Hello\")\n;;=> {:status 400, :headers {}, :body \"Hello\"}\n\n(ring.util.response/created \"/post/clojure-is-awesome\")\n;;=> {:status 201, :headers {\"Location\" \"/post/clojure-is-awesome\"}, :body nil}\n\n(ring.util.response/redirect \"https://clojure.org/getting-started/\")\n;;=> {:status 302, :headers {\"Location\" \"https://clojure.org/getting-started/\"}, :body \"\"}\n

The status function converts an existing response to use a given status code (which can be anything). Use this with care and document what the status code means otherwise confusion will abound.

(ring.util.response/status (ring.util.response/response \"Time for Cake!\") 555)\n;;=> {:status 555, :headers {}, :body \"Time for Cake!\"}\n

Ring utilities has functions for setting the header data for responses, content-type, header or set-cookie.

(ring.util.response/content-type (ring.util.response/response \"Hello\") \"text/plain\")\n;;=>  {:status 200, :headers {\"Content-Type\" \"text/plain\"}, :body \"Hello\"}\n\n(ring.util.response/header (ring.util.response/response \"Hello\") \"X-Tutorial-For\" \"Practicalli\")\n;;=>  {:status 200, :headers {\"X-Tutorial-For\" \"Practicalli\"}, :body \"Hello\"}\n\n(ring.util.response/set-cookie (ring.util.response/response \"Hello\") \"User\" \"123\")\n;;=>  {:status 200, :headers {}, :body \"Hello\", :cookies {\"User\" {:value \"123\"}}}\n

wrap-cookies middleware required

The set-cookie function adds a new entry to the response map and requires the wrap-cookies middleware to process correctly.

"},{"location":"projects/status-monitor-deps/refactor-handlers-and-tests/#handler-functions","title":"Handler functions","text":"

A handler to return the incoming IP Address

(defn check-ip-handler [request]\n    (ring.util.response/content-type\n        (ring.util.response/response (:remote-addr request))\n        \"text/plain\"))\n
"},{"location":"projects/status-monitor-deps/unit-test-mocking-handlers/","title":"Mocking in Unit Tests","text":"

The main focus of unit tests in a web application are the handler functions, passing requests to those functions and checking the responses.

All handler functions are passed a request object by default when using compojure defroutes function for routing.

If handler functions do not use arguments then you can test those handlers by simply passing an empty hash-map, {}.

For all other handler functions you can pass a request object or just specific parts of a request in a hash-map.

"},{"location":"projects/status-monitor-deps/unit-test-mocking-handlers/#ring-mock-library","title":"Ring mock library","text":"

ring-mock is a small library creating Ring request maps (Clojure hash-maps) to support unit testing. Generated hash-maps are examples of a ring request and used as arguments when calling the handler functions in tests.

"},{"location":"projects/status-monitor-deps/unit-test-mocking-handlers/#add-dev-dependency","title":"Add dev dependency","text":"

As ring-mock is a development only library, it should be added to an alias not included in the packaging of the project for production.

Edit the project deps.edn file in the project and add ring-mock to an alias called :env/dev, creating the alias if required.

  :env/dev\n  {:extra-deps {ring/ring-mock {:mvn/version \"0.4.0\"}}}\n
"},{"location":"projects/status-monitor-deps/unit-test-mocking-handlers/#add-namespace","title":"Add namespace","text":"

Add the ring-mock.request namespace to any of the test namespaces mocking of requests will be useful.

Edit the test/practicalli/status-monitor-service.clj and add ring-mock as a required namespace in files ns form.

(ns practicalli.status-monitor-service-test\n  (:require [clojure.test :refer [deftest is testing]]\n            [ring.mock.request :as  mock]\n            [practicalli.status-monitor-service :as status-monitor]))\n

Add unit tests to check the handlers (which are going to be added next - TDD style)

(deftest test-app\n  (testing \"main route\"\n    (let [response ((status-monitor/app) (request :get \"/\"))]\n      (is (= 200 (:status response)))))\n\n  (testing \"not-found route\"\n    (let [response ((status-monitor/app) (request :get \"/invalid\"))]\n      (is (= 404 (:status response))))))\n
"},{"location":"projects/status-monitor-deps/unit-test-mocking-handlers/#examples","title":"Examples","text":"
  • API: ring-mock
(deftest your-handler-test\n  (is (= (your-handler (mock/request :get \"/doc/10\"))\n         {:status  200\n          :headers {\"content-type\" \"text/plain\"}\n          :body    \"Your expected result\"})))\n\n(deftest your-json-handler-test\n  (is (= (your-handler (-> (mock/request :post \"/api/endpoint\")\n                           (mock/json-body {:foo \"bar\"})))\n         {:status  201\n          :headers {\"content-type\" \"application/json\"}\n          :body    {:key \"your expected result\"}})))\n
"},{"location":"reference/","title":"reference","text":""},{"location":"reference/continuous-integration/heroku/","title":"CI: Heroku","text":"

Heroku is a now only a commercial service without a developer environment.

Practicalli is looking into other services as that are more developer friendly.

"},{"location":"reference/continuous-integration/heroku/#heroku-pipelines","title":"Heroku pipelines","text":"

Using Heroku Pipelines the staging environment is promoted to production rather than being rebuilt

The Heroku dashboard can be used to promote the application into production, once the staging application is signed off.

"},{"location":"reference/continuous-integration/heroku/#heroku-build-process","title":"Heroku Build process","text":"

The build process starts when commits are pushed to Heroku, either directly or via a continuous integration service (eg. CircleCI).

"},{"location":"reference/ring/","title":"Ring specification","text":"

Information to complement the ring projects

Ring provides a defacto web standard that the majority of server-side web appliications use

  • request (Clojure hash-map)
  • response (Clojure hash-map)
  • handler (Clojure function)
  • middleware (Clojure function)
  • adaptor (Clojure function / wrapper)

Routing requests to handlers is typially managed by functions or libraries used in conjunction with ring, e.g reitit or compojure

"},{"location":"reference/ring/#ring-request-map","title":"Ring request map","text":"

Ring represents HTTP requests as simple Clojure maps, whose keys are drawn from the Java Servlet API and the official documentation RFC2616 \u2013 Hypertext Transfer Protocol - HTTP/ 1.1 ( http:// www.w3. org/ Protocols/ rfc2616/ rfc2616. html ).

A request map contains the following keys:

  • :server-port the port the HTTP server was listening for the request
  • :server-name the resolved name or IP address of the server handling the request
  • :remote-addr IP address of the client that made the request
  • :uri the path to the requested resource (the part of the URL address after the domain name)
  • :query-string the HTTP query string if included in the request. e.g. http://practical.li/blog?topic=clojure has a request map that includes :query-string \"topic=clojure\".
  • :scheme protocol used to make the request as a keyword, i.e. :http for HTTP request and :https for Secure HTTP
  • :request-method HTTP method used to make the request as keyword, one of :get, :post, :put, :delete, :head or :options
  • :headers hash-map of header names and values, e.g: {:headers {\"content-type\" \"text/html\" \"content-length\" \"500\" \"pragma\" \"no-cache\"}}
  • :body a string of the request body (e.g. contents of an HTTP POST request)

Request maps are not restricted to these top level keys. Middleware is commonly used to mutate the request map by adding keys.

See the reference page for a Clojure request map

"},{"location":"reference/ring/#response-maps","title":"Response maps","text":"

Ring represents an HTTP response as a simple Clojure map.

The response map contains only three keys:

  • :status HTTP status code of the response as an integer, such as 200 or 403. A full list of HTTP status codes is made available as part of the RFC2616, and can be viewed at http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html.
  • :headers contains a map of header names (string) to header values, similar to the request map.
  • :body the body of the response as one of the following four types, and the behavior will change for each:
  • String - the body is sent directly to client
  • ISeq - each element of the sequence is sent to client as a String
  • File - contents of the file sent to client
  • InputStream - contents of the stream sent to the client, after which the stream is closed

An example of a simple Hello World! response map can look like this:

{:status 200 \n :headers {\" Content-Type\" \"text/ html\"} \n :body \"<html><body><h1>Hello World!</h1></body></html>\"}\n
"},{"location":"reference/ring/#handlers","title":"Handlers","text":"

A handler is a Clojure function that accepts a request map and returns a response map.

an example handler function:

(defn hello-world \n  \"Returns an Hello World Response Map\" \n  [request] \n  {:status 200 \n   :headers {\"Content-Type\" \"text/html\"} \n   :body \"<html><body><h1>Hello World</h1></body></html>\"}) \n

Handlers are the core of the application.

Typically our URLs will map one-to-one with a handler.

create a handler and configure a route to use that handler too generate a response.

Open the src/practicalli/routes/home.clj file.

Above the call to defroutes, add the following handler:

(defn hello-world \n  [] \n  {:status 200 \n   :headers {\"Content-Type\" \"text/html\"} \n   :body \"<html><body><h1>Hello World</h1></body></html>\"})\n

Define a get request for an /about URI route that calls the hello-world handler:

(GET \"/about\" [] (foo-response)) \n

Navigate to http://localhost:3000/about in a browser and see a simple Hello World! page.

"},{"location":"reference/ring/#middleware","title":"Middleware","text":"

Middleware are functions that sit between the adapter and the handler and can be assigned to one or more routes.

A middleware function accepts a handler function and returns a new handler function.

Middleware functions can update the request or response map (adding keys, changingg values, coercing types or logging request and response maps) before passing it on to the handler function.

An example: a middleware function which adds a :friday? key to the request map which is used in the hello-world handler :

Edit the src/practicalli/middleware.clj file.

Add the following middleware function, which takes a handler and returns a new handler function (which in turn adds a new key to the request map and calls the next handler in the chain):

(defn friday? \n  [handler] \n  (fn [request] \n  (let [request (assoc request :friday? true)] \n    (handler request)))) \n

In the middleware definition add the friday? middleware function call:

(def middleware [go-bowling? wrap-error-page wrap-exceptions])\n

In the src/practicalli/routes/home.clj adjust the :body value in our hello-world handler to include a message based on the :friday? key value on the request map.

Adjust the handler function parameters to accept the request map:

(defn hello-world \n  [request] \n  {:status 200 \n   :headers {\" Content-Type\" \"text/html\"} \n   :body (str \"<html><body> <dt>Is it Friday?</dt>\"\"<dd >\"(:friday? request)\"</dd></body></html>\")}) \n

Change the /about route to make use of the request map:

(GET \"/ about\" request (hello-world request))\n

Refresh the browser page at http://localhost:3000/about and see the middleware in action!

Ring Wiki: Middleware concepts has further details on middleware and it's use in Ring

"},{"location":"reference/ring/#adapters","title":"Adapters","text":"

Adapters translate between the HTTP protocol and Clojure data, greatly simplifying all Clojure web applications.

An adapter converts an incoming HTTP request into a Clojure request hash-map and passes the request to the Clojure web application. The adaptor converts the Clojure response hash-map into the appropriate servlet HTTP response, sending that HTTP respose back to the client.

The Ring library comes with a Jetty adapter ([ring/ring-jetty-adapter \"1.3.0\"]) which sits between a Jetty servlet container and the rest of the application stack.

Http-kit also provides a ring compatible adaptor for its HTTP server.

"},{"location":"reference/ring/request-map/","title":"Ring - request map","text":"
  • What is a request
Key Always Present? Type Description :async-supported? Y boolean True if this request supports asynchronous operation :body Y ServletInputStream The body of the request. May be a zero-length stream. :content-type N String Present if sent by client. Content type of the request body. :content-length N Long Present if sent by client. Content length of the request body. :character-encoding N String Present if sent by client. Character encoding applicable to request body. :edn-params N Any :form-params N Map of keyword -> String :headers Y Map of String -> String Request headers sent by the client. Header names are all converted to lower case. :json-params N Map of String -> String :params N Map of keyword or String -> String Query params. See :query-params. :path-info Y String Request path, below the context path. Always at least \"/\", never an empty string. :path-params N Map of keyword -> String Present if the router found any path parameters. :protocol Y String Name and version of the protocol with which the request was sent :query-params N Map of keyword -> String :query-string Y String The part of the request's URL after the '?' character. :remote-addr Y String IP Address of the client (or the last proxy to forward the request) :request-method Y Keyword The HTTP verb used to make this request, lowercase and in keyword form. For example, :get or :post. :put and :delete request methods via a query parameter :_method :server-name Y String Host name of the server to which the request was sent :server-port Y int Port number to which the request was sent :scheme Y String The name of the scheme used to make this request, for example, http, https, or ftp. :ssl-client-cert N java.security.cert.X509Certificate[] Present if sent by client. Array of certificates that identify the client. :transit-params N Any data structure :uri Y String The part of this request's URL from the protocol name up to the query string in the first line of the HTTP request"},{"location":"relational-databases-and-sql/","title":"SQL and Relational Databases","text":"

seancorfield/next.jdbc is the defacto Clojure wrapper for SQL queries and managing connections to relational databases.

next.jdbc supports a wide range of databases and automatically pulls in the relevant database drivers. Abstractions are provided for insert, query, update and delete actions, which define data in a hash map allow the use of Clojure specifications (clojure.spec or Malli) for validation and generative testing.

"},{"location":"relational-databases-and-sql/#relational-databases","title":"Relational Databases","text":"

This guide will use the following relational databases

  • H2 database - lightweight in-process database that writes to disk, easily added for a fast and simple dev environment.
  • Postgresql - open source, feature rich and production grade database (defacto production choice)

Other persistent storage approach include

  • Amazon RDS - a postgres-like storage as an AWS service (should work just like postgres)
  • CockroachDB an elastic, indestructible SQL database for developers building modern applications
  • yugabyteDB open source, cloud native relational DB for powering global, internet-scale apps.
"},{"location":"relational-databases-and-sql/#key-value-stores","title":"Key Value stores","text":"
  • Redis
  • RocksDB is a high performance embedded persistent key-value store with fast storage writes (fork of Google's LevelDB)
  • AWS Dynamo - 400k limit per stored value
"},{"location":"relational-databases-and-sql/#clojure-databases","title":"Clojure databases","text":"
  • Crux - open database with temporal graph query
  • Datomic - transactional database with a flexible data model, elastic scaling, and rich queries Interesting databases in the Clojure spaces include Datomic and Crux.
"},{"location":"relational-databases-and-sql/#database-drivers","title":"Database drivers","text":"

Database drivers for commonly used database

  • Apache Derby
  • H2
  • HSQLDB
  • Microsoft SQL Server jTDS
  • Microsoft SQL Server -- Official MS Version
  • MySQL
  • PostgreSQL
  • SQLite

Database drivers may require a minimum version of Java, so consider Java 8 as the minimum version and Java 11 as the recommended version (until a new long term support version of Java is release).

see database support at http://clojure-doc.org/articles/ecosystem/java_jdbc/home.html (is this up to date?)

Feedback welcome

"},{"location":"relational-databases-and-sql/h2-database/","title":"H2 Lightweight Relational Database","text":""},{"location":"relational-databases-and-sql/managing-connections/","title":"Managing database connections","text":"

A Clojure application / REPL can connect to a database source and request a new connection. This connection can be used to send SQL statements to the database and receive the results.

In a single threaded application, then only one connection to each database would be created. In a multi-threaded application then multiple connections to the database may be created. If multiple instances of an application are deployed, then multiple database connections will be created.

As the number of simultaneous connections to the database grows, the more need for a connection pool service to maximize the performance of the database.

"},{"location":"relational-databases-and-sql/managing-connections/#postgresql","title":"PostgreSQL","text":"

When a client connects to PostgreSQL database the parent process spawns a worker process which listens to the newly created connection. Spawning a work process each time can support a small number of database connections. As the number of simultaneous connections increases, CPU and memory resources also increase.

Without a connection pool a new database connection is created for each client.

"},{"location":"relational-databases-and-sql/managing-connections/#hintlimits-via-postgresql-configuration","title":"Hint::Limits via PostgreSQL configuration","text":"

The max_connections configuration for PostgreSQL limits the number of client connections allowed, so additional connections are refused or dropped.

"},{"location":"relational-databases-and-sql/managing-connections/#connection-pool-with-postgresql","title":"Connection pool with PostgreSQL","text":"

A connection pool shares a fixed set of recyclable connections which can manage a large numbers of simultaneous connections due to the reduced CPU and memory usage.

The fixed set of connections is called the pool size and it is recommended to test the size of the pool used during integration tests.

A connection pool can efficiently deal with idle or stagnant client connections, as well as queue up client requests during traffic spikes instead of rejecting them.

"},{"location":"relational-databases-and-sql/managing-connections/#connection-pool-implementations","title":"Connection pool implementations","text":"

A connection pool can be implemented either on the application side or as middleware between the database and your application.

  • pgBouncer - a lightweight, open-source middleware connection pool for PostgreSQL.
  • HikariCP - Fast, simple, reliable. HikariCP is a \"zero-overhead\" production ready JDBC connection pool. At roughly 130Kb, the library is very light.
  • c3p0 - an easy-to-use library for making traditional JDBC drivers \"enterprise-ready\" by augmenting them with functionality defined by the jdbc3 spec and the optional extensions to jdbc
  • Heroku pgBouncer build-pack, Heroku Postgres connection limit guidance and [Heroku Postgres plans] with connection limits.
  • Tuning your PostgreSQL Server - wiki.postgresql.org
  • PostgreSQL 12.4 Documentation
"},{"location":"relational-databases-and-sql/postgresql-database/","title":"Postgresql database","text":"

PostgreSQL is a powerful, open source object-relational database system with over 30 years of active development that has earned it a strong reputation for reliability, feature robustness, and performance.

PostgreSQL is a common choice for Clojure WebApps that require a persistent store for data, especially where that data is relational in nature. PostgreSQL is also a great choice for JSON and other formats.

"},{"location":"relational-databases-and-sql/postgresql-database/#heroku-postgres","title":"Heroku Postgres","text":"

Heroku provides a Posgresql service (free 10,000 rows limit per database) which provisions PostgreSQL databases on demand, so is a simple way to start development.

"},{"location":"relational-databases-and-sql/h2-database/","title":"H2 Relational Database","text":"

H2 is a database distributed as library, making it a ideal for a self-contained development environment for a Clojure application with a relational database. Data is persisted to mv.db files as SQL queries are executed in Clojure.

H2 database main features include * Very fast, open source, JDBC API * Embedded and server modes; in-memory databases * Browser based Console application * Small footprint: around 2 MB jar file size

Whilst H2 could be used for very small production web applications, it is recommended only as a development time database.

"},{"location":"relational-databases-and-sql/h2-database/#using-h2-in-the-repl","title":"Using h2 in the REPL","text":"

H2 works with next.jdbc, the defacto relational database library for Clojure.

"},{"location":"relational-databases-and-sql/h2-database/#including-h2-in-clojure-projects","title":"Including H2 in Clojure projects","text":"

next.jdbc is highly recommended library for SQL queries in Clojure

{% tabs deps=\"deps.edn projects\", lein=\"Leiningen projects\" %}

{% content \"deps\" %} To use H2 database as only a development database, add an :extra-deps entry to include the H2 library in a :dev alias in the project deps.edn file.

{:deps\n {org.clojure/clojure    {:mvn/version \"1.10.1\"}\n org.seancorfield/next.jdbc {:mvn/version \"1.1.569\"}}}\n\n{:aliases\n  {:dev\n   {:extra-deps {com.h2database/h2 {:mvn/version \"1.4.200\"}}}}}\n

Alternative, if using practicalli/clojure-deps-edn configuration, use the :database-h2 alias when starting the REPL to include the H2 library on the class path.

{% content \"lein\" %}

Edit the project.clj configuration file and add the H2 library to the :dev-dependencies section to run H2 as the development only database.

(defproject project-name \"1.0-SNAPSHOT\"\n  :description \"Database application using next.jdbc with H2 as development database\"\n  :url \"http://practicalli.github.io/clojure/\"\n  :dependencies [[org.clojure/clojure \"1.10.1\"]\n                 [seancorfield/next.jdbc \"1.1.582\"]]\n\n  :dev-dependencies [[com.h2database/h2 \"1.4.200\"]])\n

{% endtabs %}

"},{"location":"relational-databases-and-sql/h2-database/#auto-increment-values-in-h2-database","title":"Auto-increment values in H2 database","text":"

The IDENTITY type is used for automatically generating an incrementing 64-bit long integer in H2 database.

CREATE TABLE public.account (\n  id IDENTITY NOT NULL PRIMARY KEY ,\n  name VARCHAR NOT NULL,\n  number VARCHAR NOT NULL,\n  sortcode VARCHAR NOT NULL,\n  created TIMESTAMP WITH TIME ZONE NOT NULL);\n

No need to pass a value for our primary key column value as it is being automatically generated by H2.

INSERT INTO public.account ( id, name, number, sortcode, created)\nVALUES ( ? , ? , ? , ? );\n
"},{"location":"relational-databases-and-sql/h2-database/#resources","title":"Resources","text":"
  • next.jdbc documentation and next.jdbc db-types list
  • H2 Database website
  • SQL Constraints - W3Schools.com
  • Purpose of constraint naming - Stack Overflow
  • seancorfield/honeysql - SQL as data structures
  • stack overflow - auto increment id in h2 database
"},{"location":"relational-databases-and-sql/h2-database/database-tools/","title":"Database tools","text":"

DBeaver is a free database tool that supports the H2 database and many other databases.

"},{"location":"relational-databases-and-sql/h2-database/database-tools/#create-a-new-connection","title":"Create a new connection","text":"

Create a New Connection and select Embedded > H2 database

Select a *.mv.db file as the path

If the H2 driver is not installed in DBeaver, a will prompt will display to download it.

Expand the connection to see the schema details

"},{"location":"relational-databases-and-sql/h2-database/database-tools/#h2-database-single-connection","title":"H2 database single connection","text":"

When connecting to the H2 database using a database management tool such ad DBeaver, the database is locked and prevents code from running in the REPL.

Close the connection in the database management tool to continue using the REPL.

"},{"location":"relational-databases-and-sql/h2-database/schema-design/","title":"H2 Schema design","text":"

Key concepts and syntax for designing database schema for the H2 database

"},{"location":"relational-databases-and-sql/h2-database/schema-design/#auto-increment-values-in-h2-database","title":"Auto-increment values in H2 database","text":"

The IDENTITY type is used for automatically generating an incrementing 64-bit long integer in H2 database.

CREATE TABLE public.account (\n  id IDENTITY NOT NULL PRIMARY KEY ,\n  name VARCHAR NOT NULL,\n  number VARCHAR NOT NULL,\n  sortcode VARCHAR NOT NULL,\n  created TIMESTAMP WITH TIME ZONE NOT NULL);\n

The value for id is automatically generated by H2, so no need to provide a value for id in the SQL statement

INSERT INTO public.account ( id, name, number, sortcode, created)\nVALUES ( ? , ? , ? , ? );\n
"},{"location":"relational-databases-and-sql/h2-database/schema-design/#resources","title":"Resources","text":"
  • next.jdbc documentation and next.jdbc db-types list
  • H2 Database website
  • SQL Constraints - W3Schools.com
  • Purpose of constraint naming - Stack Overflow
  • seancorfield/honeysql - SQL as data structures
  • stack overflow - auto increment id in h2 database
"},{"location":"relational-databases-and-sql/next-jdbc-library/","title":"SQL queries in Clojure with next.jdbc library","text":"

Using next.jdbc to connect to a database and run queries only a few steps

  • add seancorfield/next.jdbc as a project dependency
  • require the seancorfield/next.jdbc in the relevant project namespace definitions
  • define a database specification (hash-map of database details or JDBC string)
  • create a connection (optionally using a connection pool)
  • execute SQL statements (individual, batch, transaction)

"},{"location":"relational-databases-and-sql/next-jdbc-library/#hintnextjdbc-supersedes-clojurejavajdbc","title":"Hint::next.jdbc supersedes clojure.java.jdbc","text":"

seancorfield/next.jdbc supersedes clojure.java.jdbc which used to be the defacto library for database backed projects. next.jdbc is faster and exposes a more modern API design (according to the author of clojure.java.jdbc). Migration from clojure.java.jdbc is documented on the next.jdbc repository

"},{"location":"relational-databases-and-sql/next-jdbc-library/#live-coding-example","title":"Live Coding example","text":""},{"location":"relational-databases-and-sql/next-jdbc-library/#summary-of-using-nextjdbc","title":"Summary of using next.jdbc","text":"

Include next.jdbc as a dependency in the project

{:deps\n {org.clojure/clojure        {:mvn/version \"1.10.1\"}\n  org.seancorfield/next.jdbc {:mvn/version \"1.1.569\"}}}\n

Require next.jdbc into the project namespace

(ns practicalli.database-access\n  (:require [next.jdbc :as jdbc]))\n
"},{"location":"relational-databases-and-sql/next-jdbc-library/#specify-the-database-connection","title":"Specify the database connection","text":"

Define a data source connection using a next.jdbc hash map or a JDBC URL

An example next.jdbc specification for H2 database

{:dbtype \"h2\" :dbname \"banking-on-clojure\"}\n

An example JDBC connection string for postgres database

\"jdbc:postgresql://<hostname>:port/<database-name>?user=<username>&password=<password>&sslmode=require\"\n
"},{"location":"relational-databases-and-sql/next-jdbc-library/#running-sql-queries","title":"Running SQL queries","text":"

execute! runs an SQL statement and returns the results as a vector of hash maps. The hash maps use table and column name to create qualified keywords in the results.

A Clojure string contains the SQL statement.

(jdbc/execute!\n      connection\n      [(str \"insert into account_holders(\n               account_holder_id,first_name,last_name,email_address,residential_address,social_security_number)\n             values(\n               '\" account-holder-id \"', 'Jenny', 'Jetpack', 'jen@jetpack.org', '42 Meaning Lane, Altar IV', 'AB101112C' )\")])\n
"},{"location":"relational-databases-and-sql/next-jdbc-library/#hintdatafy-results","title":"Hint::Datafy results","text":"

Hash maps returned by execute! use Datafy and are therefore navigable using Clojure data browsers

"},{"location":"relational-databases-and-sql/next-jdbc-library/#using-connections-and-queries-effectively","title":"Using connections and queries effectively","text":"

Define a name for the database connection using the form (jdbc/get-datasource {:dbtype \"...\" :dbname \"...\" ...})

(def db-spec (jdbc/get-datasource {:dbtype \"h2\" :dbname \"banking-on-clojure\"}))\n

Use the with-open Clojure core function to automatically close connections after running SQL expressions

    (with-open [connection (jdbc/get-connection db-spec)]\n      (jdbc/execute! connection [...]))\n

Defining a generic function provides a simple way to run any SQL query for a specified data base connection.

(defn query-database\n  [db-spec sql-statement]\n  (with-open [connection (jdbc/get-connection db-spec)]\n    (jdbc/execute! connection sql-statement)))\n
"},{"location":"relational-databases-and-sql/next-jdbc-library/#using-nextjdbc-friendly-functions","title":"Using next.jdbc friendly functions","text":"

next.jdbc provides higher level abstractions over execute! function. These friendly functions take a database connection, a table name as a Clojure keyword and a hash map that contains the values in the query. As the query is a hash map it can also be represented by a Clojure specification (clojure.spec or Malli).

Function names ending with a bang, !, change the contents of the database

  • insert! - insert new rows
  • query - read data
  • update! - update existing rows
  • delete! - remove rows
"},{"location":"relational-databases-and-sql/next-jdbc-library/#generic-insert-function-with-nextjdbcsql","title":"Generic insert function with next.jdbc.sql","text":"

To save repetition, define a generic function that uses insert and takes a database table name, data to insert and the database connection.

(defn insert-data\n  [db-spec table record-data]\n  (with-open [connection (jdbc/get-connection db-spec)]\n    (jdbc-sql/insert! connection table record-data)))\n

Call the generic insert function with the database connection, table name and query specification

(insert-data\n  db-spec\n  :public.account_holders\n  {:account_holder_id      (java.util.UUID/randomUUID)\n   :first_name             \"Rachel\"\n   :last_name              \"Rocketpack\"\n   :email_address          \"rach@rocketpack.org\"\n   :residential_address    \"1 Ultimate Question Lane, Altar IV\"\n   :social_security_number \"BB104312D\"} )\n
"},{"location":"relational-databases-and-sql/next-jdbc-library/#hintnextjdbc-getting-started-guide","title":"HINT::next.jdbc getting started guide","text":"

next.jdbc getting started guide is very detailed.

"},{"location":"relational-databases-and-sql/next-jdbc-library/add-to-project/","title":"Add next.jdbc to a project","text":"

Create a new Clojure project using clj-new tool (see Clojure install for details)

clojure -T:project/new :template app :name practicalli/simple-database\n
"},{"location":"relational-databases-and-sql/next-jdbc-library/add-to-project/#main-library-dependency","title":"Main library dependency","text":"

Edit the deps.edn file in the root of the project directory.

In the :deps hash-map, add next.jdbc libraries as a dependency

{:deps {org.clojure/clojure    {:mvn/version \"1.10.1\"}\n        org.seancorfield/next.jdbc {:mvn/version \"1.1.569\"}}}\n

An alias should be used to include the H2 database library as a development dependency, to avoid including it in the packaged project.

{% tabs practicalli=\"practicalli/clojure-deps-edn\", manual=\"Manually add Alias\" %}

{% content \"practicalli\" %}

practicalli/clojure-deps-edn provides user level aliases that can be used with any project

:database/h2 adds the library dependency for H2 database

{% content \"manual\" %}

Edit the project deps.edn file in the root of the project (or add an alias to the user level deps.edn to use with any project).

Include an :extra-deps section for the H2 library

{:deps {org.clojure/clojure    {:mvn/version \"1.10.1\"}\n        org.seancorfield/next.jdbc {:mvn/version \"1.1.569\"}}}\n\n :aliases\n {:database/h2\n  {:extra-deps {com.h2database/h2 {:mvn/version \"2.1.210\"}}}}\n

{% endtabs %}

"},{"location":"relational-databases-and-sql/next-jdbc-library/add-to-project/#starting-a-repl-for-development","title":"Starting a REPL for development","text":"

Include the :database/h2 alias when starting a REPL

clojure -M:database/h2:repl/rebel\n
"},{"location":"relational-databases-and-sql/next-jdbc-library/add-to-project/#staging-and-production-dependencies","title":"Staging and Production dependencies","text":"

Assuming PostgreSQL is used as the staging and production database, the postgres library should be added to the main dependencies of the project.

In the :deps hash-map, add the PostgreSQL JDBC driver library as dependencies along-side next.jdbc.

{:deps\n  {org.clojure/clojure {:mvn/version \"1.10.1\"}\n\n  ;; Database\n  org.seancorfield/next.jdbc    {:mvn/version \"1.1.582\"}\n  org.postgresql/postgresql {:mvn/version \"42.2.16\"}}}\n
"},{"location":"relational-databases-and-sql/next-jdbc-library/add-to-project/#hintcheck-for-latest-library-versions","title":"Hint::Check for latest library versions","text":"

Check clojars.org for latest version org.seancorfield/next.jdbc and Maven Central for latest version of H2 database

jdbc.postgres.org shows the latest release, or look at the Postgresql page on Maven Central

"},{"location":"relational-databases-and-sql/next-jdbc-library/connection-pool-lifecycle/","title":"Using next.jdbc with a Connection pool","text":"

As the scale of database use increases it becomes more efficient to continually re-use existing connections to the database, rather than create a new connection to execute each SQL statement.

A connection pool is a set of open connections that are used over and over again, enhancing the performance of the database and allowing the database to scale more efficiently.

Databases may provide their own connection pool (postgres has ..., h2 has ...). Hikari and c3p0 are commonly used database connection pool libraries

"},{"location":"relational-databases-and-sql/next-jdbc-library/connection-pool-lifecycle/#configure-nextjdbc-with-a-connection-pool","title":"Configure next.jdbc with a connection pool","text":"
  • Add connection pool library (question: if using a db connection pool, is this just the driver?)
  • Require next.jdbc and next.jdbc.connection in the Clojure namespace where the connection pool will be used

{% tabs hikari=\"hikari\", c3p0=\"C3P0\", h2=\"H2 database\", postgresql=\"PostgreSQL database\" %}

{% content \"hikari\" %}

(ns my.main\n  (:require\n    [next.jdbc :as jdbc]\n    [next.jdbc.connection :as connection])\n\n  (:import\n    (com.zaxxer.hikari HikariDataSource)))\n

Create a database specification

HikariCP requires :username instead of :user in the db-spec

(def ^:private db-spec {:dbtype \"...\" :dbname \"...\" :username \"...\" :password \"...\"})\n

When using a JDBC URL with a connection pool, use :jdbcUrl in the database spec instead of :dbtype, :dbname, etc)

{% content \"c3p0\" %}

(ns my.main\n  (:require\n    [next.jdbc :as jdbc]\n    [next.jdbc.connection :as connection])\n\n  (:import\n    (com.mchange.v2.c3p0 ComboPooledDataSource PooledDataSource)))\n

{% content \"h2\" %}

{% endtabs %}

"},{"location":"relational-databases-and-sql/next-jdbc-library/connection-pool-lifecycle/#execute-with-a-connection-pools","title":"Execute with a connection pools","text":"

next.jdbc.connection/->pool takes a connection pool (Java Class) and a database specification.

(with-open [^HikariDataSource ds (connection/->pool HikariDataSource db-spec)]\n    (jdbc/execute! ds ...)\n    (jdbc/execute! ds ...)\n    (do-other-stuff ds args)\n    (into [] (map :column) (jdbc/plan ds ...)))\n
"},{"location":"relational-databases-and-sql/next-jdbc-library/connection-pool-lifecycle/#configure-nextjdbc-with-lifecycle-management-libraries","title":"Configure next.jdbc with lifecycle management libraries","text":"

A connection pool has a start/stop lifecycle, so fits easily into lifecycle management libraries such as mount, component and integrant.

Start the database server connection pool

Assumes database is a separate service already running. Add a check for the status of the database before starting the components?

{% tabs mount=\"Mount\", component=\"Component\", integrant=\"Integrant\" %}

{% content \"mount\" %}

{% content \"component\" %} next.jdbc.connection/component supports Component directly by creating a Component-compatible entity.

Example code from next.jdbc.connection/component

(component/start (connection/component HikariDataSource db-spec))\n
(ns practicalli.application\n  (:require\n    [com.stuartsierra.component :as component]\n    [next.jdbc :as jdbc]\n    [next.jdbc.connection :as connection])\n\n  (:import\n    (com.zaxxer.hikari HikariDataSource)))\n\n(def ^:private db-spec {:dbtype \"...\" :dbname \"...\" :username \"...\" :password \"...\"})\n\n(defn -main [& args]\n  ;; connection/component takes the same arguments as connection/->pool:\n  (let [ds (component/start (connection/component HikariDataSource db-spec))]\n    (try\n      ;; \"invoke\" the data source component to get the javax.sql.DataSource:\n      (jdbc/execute! (ds) ...)\n      (jdbc/execute! (ds) ...)\n      ;; can pass the data source component around other code:\n      (do-other-stuff ds args)\n      (into [] (map :column) (jdbc/plan (ds) ...))\n      (finally\n        ;; stopping the component will close the connection pool:\n        (component/stop ds)))))\n

{% content \"integrant\" %}

{% endtabs %}

"},{"location":"relational-databases-and-sql/next-jdbc-library/database-specifications/","title":"Database Specifications","text":"

Call the next.jdbc/get-datasource function with a database specification or a JDBC URL string

A database specification is a hash map describing the database you wish to connect to. TODO: examples of database specifications

A \"database spec\" is a Clojure map that specifies how to access the data source. Specify the database type, the database name, and the username and password.

(def db-spec\n  {:dbtype \"mysql\"\n   :dbname \"db-name\"\n   :user \"user-account\"\n   :password \"secret\"})\n

use aero to use a different database specification based on the environment being run (dev, test, prod, etc.)

next.jdbc also works with connection pooling libraries which can be used to construct a datasource from. Examples include HikariCP or c3p0

"},{"location":"relational-databases-and-sql/next-jdbc-library/next-jdbc-and-resultsets/","title":"next.jdbc and result sets","text":"

We are using the db-query-with-resultset to apply a result-set-fn on the result-set lazily (in the db sense) but the fetch-size doesn't seem to be respected. If I do a (count result-set) it returns the size of the all the rows expected from the query instead of the fetch size, this is how our function looks like. clojure.java.jdbc version is \"0.3.5\"

(defn do-lazy-read [db-spec sql-params size result-set-fn]\n  (jdbc/db-query-with-resultset\n    db-spec\n    (into [] (cons {:fetch-size size} sql-params))\n    (fn [result-set]\n      (prn (count result-set))\n      (-> result-set\n          (jdbc/result-set-seq :identifiers qstr/underscores->hyphens)\n          result-set-fn))))\n

That is expected :fetch-size is not a limit, it's just a hint for each \"chunk\" of the overall result set during database access.

But we are facing memory issues and we think this not being lazy is the cause, number of rows are in the order of a few 100,000 rows to a million

You need reducible-query

Haven\u2019t used it before, but it seems it will close the connection after reducing the result-set, how would I go about maintaining the cursor? (edited)

I am going through the documentation, will explore reducible query. But the question is if lets say the fetch size 1000 is just a hint, why is the hint not considered? Why would it always return all the rows, that too rows close to a million?

Reading this answer of yours https://stackoverflow.com/questions/39765943/clojure-java-jdbc-lazy-query/39775018#39775018 and the linked docs and the other SO question on why jdbc ignores setFetchSize (edited)

  1. fetch size tells the JDBC driver to try to only fetch that many rows at a time but it is not a limit on how many rows come back in the result set 17:43
  2. the result set is built lazily -- so result-set is a lazy sequence and if you call count you will realize the entire sequence, which will be you 1M rows 17:44
  3. even trying to process result set lazily and using fetch, you are at the usual mercy of Clojure's treatment of very large lazy sequences -- and you must completely process the result set before c.j.j. closes the connection (otherwise you'll get errors when you try to realize the next piece of the lazy result set -- because it relies on the connection staying open). 17:45
  4. since all of that is very tricky (as you're discovering), reducible-query was added so you can process the result set in a single pass reduction without needing to worry about laziness 17:46 FWIW, next.jdbc is built on that concept as a primary API: next.jdbc/plan is explicitly a reducible that is also \"foldable\" (in the clojure.core.reducers/fold sense so you can achieve some level of concurrency as well). 17:47 The reducible-query function in c.j.j. is the predecessor to next.jdbc/plan -- but the latter is better designed for performance (as is the whole of next.jdbc). 17:49 As another part of #3 above: holding onto the head is definitely a possibility -- as with processing any very large lazy sequence, but you're dealing with a Clojure problem there, not a JDBC problem. 17:49
"},{"location":"relational-databases-and-sql/next-jdbc-library/simple-example/","title":"Simple database example","text":"

Create a project called simple database

clojure -T:project/new :template app :name practicalli/simple-database\n

Edit the deps.edn file in the root of the project directory.

In the :deps hash-map, add next.jdbc library as dependency and add a :dev alias could include an :extra-deps section for the H2 driver

{:deps {org.clojure/clojure        {:mvn/version \"1.10.1\"}\n        org.seancorfield/next.jdbc {:mvn/version \"1.1.569\"}}}\n\n{:dev\n  {:extra-deps {com.h2database/h2 {:mvn/version \"1.4.200\"}}}}\n

{% tabs repl=\"In the REPL\", project=\"In a Clojure Project\" %}

{% content \"repl\" %}

"},{"location":"relational-databases-and-sql/next-jdbc-library/simple-example/#using-nextjdbc-in-a-repl-session","title":"Using next.jdbc in a REPL session","text":"

In a terminal window, change to the root directory of the simple-database project.

Start a Rebel REPL from the root of the new project

cd simple-project\n\nclojure -M:repl/rebel\n
The first time libraries are used they are downloaded and cached locally (~/.m2/repository)

Require the next.jdbc namespace using an alias called jdbc

(require '[next.jdbc :as jdbc])\n

Define a database specification containing the details of the H2 database to be used

(def db-specification {:dbtype \"h2\" :dbname \"address-book\"})\n

Define a data source that is a connection to the database

(def data-source (jdbc/get-datasource db-specification))\n

Create a table in the database using a standard SQL statement

(jdbc/execute!\n  data-source\n  [\"create table contacts (\n     id int auto_increment primary key,\n     name varchar(32),\n     email varchar(255))\"])\n

An address-book.mv.db file is created in the root of the project

Insert an entry into the database by executing an SQL insert query

(jdbc/execute!\n  data-source\n  [\"insert into contacts(name,email)\n    values('Jenny Jetpack','jenny@jetpack.org')\"])\n

View all the records added to the database (there should be only one)

(jdbc/execute!\n  data-source\n  [\"select * from address\"])\n

To delete all the records in the database, drop the contacts table in the database

(jdbc/execute!\n  data-source\n  [\"drop table contacts\"])\n

{% content \"project\" %}

Edit the file src/practicalli/simple-database.clj from the simple-database project.

Update the practicalli.simple-database namespace definition with a require statement for next.jdbc

(ns practicalli.simple-database\n  (:gen-class)\n  (:require [next.jdbc :as jdbc]))\n

Define a data source for the H2 database

(def db-specification {:dbtype \"h2\" :dbname \"address-book\"})\n

Define a data source that is a connection to the database

(def data-source (jdbc/get-datasource db-specification))\n

Create a table in the database using a standard SQL statement

(jdbc/execute!\n  data-source\n  [\"create table contacts (\n     id int auto_increment primary key,\n     name varchar(32),\n     email varchar(255))\"])\n

An address-book.mv.db file is created in the root of the project

Insert an entry into the database by executing an SQL insert query

(jdbc/execute!\n  data-source\n  [\"insert into contacts(name,email)\n    values('Jenny Jetpack','jenny@jetpack.org')\"])\n

View all the records added to the database (there should be only one)

(jdbc/execute!\n  data-source\n  [\"select * from address\"])\n

To delete all the records in the database, drop the contacts table in the database

(jdbc/execute!\n  data-source\n  [\"drop table contacts\"])\n

{% endtabs %}

"},{"location":"relational-databases-and-sql/next-jdbc-library/simple-example/#hintdatabase-driver-lookup","title":"Hint::Database driver lookup","text":"

The :dbtype (:classname) is used to find the correct database driver

"},{"location":"service-repl-workflow/","title":"Clojure Service REPL workflow","text":"

Practicalli Service REPL workflow extends the reloading concept and tools used in Practicalli REPL Reloaded workflow to Clojure Services.

Services are composed of components such as HTTP server, database connection, log publisher, message queue, request router, etc. These components can be updated by evaluating code changes as they are made, although some changes require the component or whole system to be restarted.

Lifecycle tools (Donut, Integrant, Mount, etc.) manage the components in a system, e.g. (start), (restart), (stop). The state of a running system can be inspected, (system), as it is represented by a Clojure hash-map.

System Configuration as Data

Practicalli recommends expressing a system configuration as data to provide a readily understandable system.

A component may have a dependancy on one or more other components, so starting and stoping the sytem should manage components in the correct order, e.g.:

  • a request router component is dependent on an http server component
  • a request router is dependant on a database connection to an external data source

Using a data configuration, each component is a top-level key associated with hash-map containing the component configuration, optionally using an aero profile to support multiple environments in which the components run (e.g. development, testing, staging, production)

"},{"location":"service-repl-workflow/#aero-configuration","title":"Aero Configuration","text":"

Aero provides tag litterals to use with an EDN configuration file to inject values based on a #profile or #env environment variables.

Multiple configurations can be defined within the same file, i.e. resources/config.edn, with each profile providing values for a specific environment, e.g. dev, test, stage and prod environments.

"},{"location":"service-repl-workflow/#system-component-libraries","title":"System component libraries","text":"
  • Mount - shared data approach using custom defstate atom
  • donut.system - configuration as data, functions to manage components
  • Integrant & Integrant REPL - configuration as data, runtime polymorphism (defmethod) to manage components, repl reloading tools
  • Component - provides lifecycle protocol approach using defrecord
  • Component.repl - development tools for Component
  • JUXT/clip
"},{"location":"service-repl-workflow/#references","title":"References","text":"

Enter Integrant - James Reeves

Practicalli REPL Reloaded Workflow

Clojure Reloaded workflow

"},{"location":"service-repl-workflow/aero/","title":"Aero System configuration","text":"

Aero library is an EDN reader that provides reader tags (tag literals) to support the declarative definition of system components, especially across different deployment environments (e.g. dev, stage, production).

A specific profile value (e.g. :dev :stage :prod) is given to aero/read-config which parses the Integrant configuration, returning an updated Integrant configuration containing values specific to the given profile.

Quote

Configuration should be explicit obvious, but not clever. It should be easy to understand what the config is, and where it is declared. - JUXT.

"},{"location":"service-repl-workflow/aero/#aero-reader","title":"Aero reader","text":"

Require the aero.core library and use the read-config function to process an EDN configuration file saved in resources/config.end.

ProjectREPL

Project with aero and supporting requires

(ns practicalli.gameboard.environment\n  (:require\n   [aero.core       :as aero]\n   [clojure.java.io :as io]))\n\n(defn aero-config\n  \"Profile specific configuration for all services\"\n  [profile]\n  ;; (mulog/log ::aero-parse-config :profile profile :local-time (java.time.LocalDateTime/now)\n  (aero/read-config (io/resource \"config.edn\") {:profile profile}))\n

Project with aero and supporting requires

(require '[aero.core :as aero])\n(require '[clojure.java.io :as io])\n(aero/read-config (io/resource \"config.edn\"))\n

resources/config.edn commonly used with aero

System EDN configuration loaded at runtime are created within the resources directory which is typically defined as part of the class path and therefore available even when the service is packaged in a jar file.

clojure.java.io/resource reads a file from the resources directory, the resulting file can be passed to the aero.core/reader fuction with an optional profile value.

"},{"location":"service-repl-workflow/aero/#aero-tag-literals","title":"Aero Tag Literals","text":"

Aero uses tag literals as placeholders for specific values

  • #profile - replace with the value from the given profile name
  • #env - replace with the value of the matching operating system environment variable
  • #hostname - replace with the value from the given computer hostname
  • #or - a vector of possible values, returning the first \"truthy\" value
  • #long - cast a String value to a Clojure Long type (e.g. for PORT values)
  • #ref - refer to another part of the system configuration rather than duplicate it
  • #ig/ref - integrant version of #ref to reference another part of the system
Aero tag literal definitions

aero/core.cljc contains the definitions for the Aero tag literals

Define Custom Tags

Custom tag literals can be added to extend Aero, e.g. adding Integrant Reference tag literal

"},{"location":"service-repl-workflow/aero/#profiles","title":"Profiles","text":"

Aero #profile tag literal supports multiple environment within one EDN configuration

Pass a profile and configuration to the aero reader and only the matching profile values in the configuration are returned each key. So a profile is a type of filter on the system configuration.

Aero reader with profile

Aero reader with profile
(defn aero-config\n  \"Apply profile to service configuration: :dev :stage :prod\"\n  [profile]\n  (aero/read-config (io/resource \"config.edn\") {:profile profile}))\n

As each part of the system can be defined using profiles, the same resources/config.edn configuration can be used for both Integrant and Integrant REPL

Mulog Event Publish Configuration with aero

The areo profiles (:dev :docker :prod) determine which type of publisher is use for mulog events

 ;; Event logging service - mulog\n :practicalli.gameboard.service/log-publish\n {\n  :mulog #profile {:dev  {:type :console-json :pretty? true}\n\n                   ;; Multiple publishers using Open Zipkin service (started via docker-compose)\n                   :docker  {:type :multi\n                             :publishers\n                             [{:type :console-json :pretty? false}\n                              {:type :zipkin :url \"http://localhost:9411/\"}]}\n\n                   :prod {:type :console-json :pretty? false}}}\n

As an alternative, a separate dev/resources/config.edn file could be defined if the development environment significantly deviates from stage and production. Using separate files does require additional maintenance to ensure the deployed environments are consistent.

"},{"location":"service-repl-workflow/aero/#set-default-values","title":"Set default values","text":"

#or defines a vector of possible values, returning the first \"truthy\" value.

Use #or to define a default value to avoid nil appearing in the configuration, especially where #env is used to get operating system environment variables.

{:practicalli.gameboard.service/http-server\n {:port #long #or [#env APP_SERVER_PORT 8888]}}\n
"},{"location":"service-repl-workflow/aero/#environment-variables","title":"Environment variables","text":"

#env tag will use Environment Variables from the Operating system as values in the configuration

Environment Variables must be defined in the Operating System before the Clojure REPL process is started. Adding Environment Variables after the REPL starup requires a restart of the REPL to pick up the values.

HTTP Server component with Profile and Environment Variables

{:practicalli.gameboard.service/http-server\n {:handler #ig/ref :practicalli.scoreboard.service/router\n  :port #profile {:develop #long #or [#env APP_SERVER_PORT 8888]\n                  :test    #long #or [#env APP_SERVER_PORT 8080]\n                  :stage   #long #or [#env APP_SERVER_PORT 8080]\n                  :live    #long #or [#env APP_SERVER_PORT 8000]}\n  :join? false}\n\n :practicalli.scoreboard.service/router\n {:persistence #ig/ref :practicalli.scoreboard.service/relational-store}\n\n :practicalli.scoreboard.service/relational-store\n {:connection #profile {:develop  {:url \"http://localhost/\" :port 57207 :database \"scoreboard-develop\"}\n                        :test     {:url \"http://localhost/\" :port 57207 :database \"scoreboard-test\"}\n                        :stage    {:url \"http://localhost/\" :port 57207 :database \"scoreboard-stage\"}\n                        :live     {:url \"http://localhost/\" :port 57207 :database \"scoreboard\"}}}}\n
"},{"location":"service-repl-workflow/aero/#gameboard-configuration","title":"Gameboard Configuration","text":"

An example configuration for the Practicalli Gameboard Web Service

Example configuration for Integrant and Aero

;; --------------------------------------------------\n;; Application Component configuration - Integrant & Integrant REPL\n;;\n;; - Event logging (mulog)\n;; - HTTP Server  (embedded jetty or http-kit)\n;; - Request routing (reitit)\n;; - Persistence (relational) connection\n;;\n;; #profile used by aero to select the configuration to use for a given profile (dev, test, prod)\n;; #long defines Long Integer type (required for Java HTTP server port)\n;; #env reads the environment variable of the given name\n;; #or uses first non nil value in sequence\n;;\n;; Environment variables should be defined locally and in deployment provisioner\n;; --------------------------------------------------\n\n\n{;; --------------------------------------------------\n ;; Event logging service - mulog\n\n ;; https://github.com/BrunoBonacci/mulog#publishers\n ;; https://github.com/openzipkin/zipkin\n :practicalli.gameboard.service/log-publish\n {;; Type of publisher to use for mulog events\n  ;; Publish json format logs, captured by fluentd and exposed via OpenDirectory\n  :mulog #profile {:dev  {:type :console-json :pretty? true}\n\n                   ;; Multiple publishers using Open Zipkin service (started via docker-compose)\n                   :docker  {:type :multi\n                             :publishers\n                             [{:type :console-json :pretty? false}\n                              {:type :zipkin :url \"http://localhost:9411/\"}]}\n\n                   :prod {:type :console-json :pretty? false}}}\n\n ;; --------------------------------------------------\n ;; HTTP Server - embedded service\n\n :practicalli.gameboard.service/http-server\n {;; Router function passed into the HTTP server form managing requests/responses\n  :handler #ig/ref :practicalli.gameboard.service/router\n\n  ;; Port number (Java Long type) - environment variable or default number\n  :port  #long #or [#env HTTP_SERVER_PORT 8080]\n\n  ;; Join REPL to HTTP server thread\n  :join? false}\n\n ;; --------------------------------------------------\n ;; persistence - connection to Practicall relational storage\n\n ;; TODO: add database connection pool ?\n\n :practicalli.gameboard.service/relational-store\n {:host #or [#env DATABASE_HOST \"localhost\"]\n  :port #or [#env DATABASE_PORT 3306]\n  :username #or [#env DATABASE_USERNAME \"gameboard\"]\n  :password #or [#env DATABASE_PASSWORD \"trustnoone\"]}\n\n ;; --------------------------------------------------\n ;; Data provider services\n ;; - connection to services that provide eSports data\n\n :practicalli.gameboard.service/data-provider\n {;; external data providers via Risky\n  :llamasoft-api-url  #or [#env LAMASOFT_API_URL \"http://localhost\"]\n  :polybus-report-uri \"/report/polybus\"\n  :moose-life-report-uri \"/api/v1/report/moose-life\"\n  :minotaur-arcade-report-uri \"/api/v2/minotar-arcade\"\n  :gridrunner-revolution-report-uri \"/api/v1.1/gridrunner\"\n  :space-giraffe-report-uri \"/api/v1/games/space-giraffe\"}\n\n;; --------------------------------------------------\n ;; routing\n\n ;; Configure web routing application with application environment\n ;; define top-level keys to access via the environment hash-map\n ;; - :persistence - database connection information\n ;; - :services - url, endpoint, tokens for services used by the Fraud API (e.g. risky)\n :practicalli.gameboard.service/router\n {:persistence #ig/ref :practicalli.gameboard.service/relational-store\n  :data-provider #ig/ref :practicalli.gameboard.service/data-provider}}\n
"},{"location":"service-repl-workflow/donut-system/","title":"Donut System","text":"

Donut system takes a system as data approach, using a hash-map to define the overall system with keys to define each component (or component group) in that system.

Component definitions are also a hash-map with :start, :stop, :config keys to express how to manage that component

Donut system configuration is a similar data-centric approach to that used by reitit for http request routing.

Practicalli uses ::donut alias instead of ::ds

The donut.system library is required using the :as donut alias.

::donut is used as the keyword qualifier

Practicalli recommends meaningful names to make code easier to read and searching considerably simpler (fewer false matches)

"},{"location":"service-repl-workflow/donut-system/#create-project-with-donut","title":"Create project with Donut","text":"

practicalli/service template from Practicalli Project Templates can be given a :component option to include the Donut System library and example code.

:project/create alias from Practicalli Clojure CLI Config

Create Clojure Web Service project with Donut

clojure -T:project/create :template practicalli/service :component :donut :name practicalli/web-service-name\n
"},{"location":"service-repl-workflow/donut-system/#including-donut","title":"Including Donut","text":"

Donut library includes a REPL workflow namespace, so there is only one library dependency to add to the project. This project must be included at runtime so should be added to the project deps.edn configuration

Donut dependency in Gameboard project

deps.edn
{\n:paths\n [\"src\" \"resources\"]\n\n :deps\n {;; Service\n  http-kit/http-kit {:mvn/version \"2.6.0\"}  ; latest \"2.7.0-alpha1\"\n  metosin/reitit    {:mvn/version \"0.5.13\"}\n\n  ;; Logging\n  com.brunobonacci/mulog             {:mvn/version \"0.9.0\"}\n  com.brunobonacci/mulog-adv-console {:mvn/version \"0.9.0\"}\n\n  ;; System\n  aero/aero           {:mvn/version \"1.1.6\"}\n  party.donut/system {:mvn/version \"0.0.202\"}\n  org.clojure/clojure {:mvn/version \"1.12.0\"}}}\n
"},{"location":"service-repl-workflow/donut-system/#define-a-system","title":"Define a System","text":"

Donut defines a system using a Clojure hash-map with the following top level keys

  • ::donut/defs to define components of a system or component group
  • ::donut/signals customise the startup/shutdown approach (optional)

Create a system namespace to define the donut system

Require libraries in the namespace form

(ns practicalli.gameboard.system\n  (:require\n   ;; Application dependencies\n   [practicalli.donoughty.router :as router]\n\n   ;; System dependencies\n   [org.httpkit.server     :as http-server]\n   [com.brunobonacci.mulog :as mulog]\n   [donut.system           :as donut]\n   [aero.core              :as aero]\n   [clojure.java.io        :as io]))\n

Define a system that runs a web server with event log publisher

The http server use the :env environment to determine the port, although this could be defined directly in the :http :server :config section.

There is a relationship inside the http component between server and handler. The handler depends on configuration within the :env environment configuration.

The :instance key is associated with the component reference that is returned when a component is started. The :instance reference is used to shut down the service.

The event log publisher and http service have no intrinsic relationship, so order of startup is not an issue as any mulog events created are cached until the publisher has started.

Simple Web Service

src/practicalli/gameboard/system.clj
(def system\n  \"System Component management with Donut\"\n  {::donut/defs\n   {:env  {:http-port 8080\n           :persistence {:database-host (System/getenv \"POSTGRES_HOST\")\n                         :database-port (System/getenv \"POSTGRES_PORT\")\n                         :database-username (System/getenv \"POSTGRES_USERNAME\")\n                         :database-password (System/getenv \"POSTGRES_PASSWORD\")\n                         :database-schema (System/getenv \"POSTGRES_SCHEMA\")}}\n    :event-log {:publisher\n                #::donut{:start (fn mulog-publisher-start\n                                  [{{:keys [dev]} ::donut/config}]\n                                  (mulog/start-publisher! dev))\n                         :stop (fn mulog-publisher-stop\n                                 [{::donut/keys [instance]}]\n                                 (instance))\n                         :config {:dev {:type :console :pretty? true}}}}\n    :http {:server\n           #::donut{:start (fn http-kit-run-server\n                             [{{:keys [handler options]} ::donut/config}]\n                             (http-server/run-server handler options))\n                    :stop  (fn http-kit-stop-server\n                             [{::donut/keys [instance]}]\n                             (instance))\n                    :config {:handler (donut/local-ref [:handler])\n                             :options {:port  (donut/ref [:env :http-port])\n                                       :join? false}}}\n           :handler (router/app (donut/ref [:env :persistence]))}}})\n
"},{"location":"service-repl-workflow/donut-system/#start-the-system","title":"Start the system","text":"

Use donut/signal with the ::donut/start key to start all the components in the system.

::donut/signals key is associated with a signal configuration to modify the start and stop process, although the default process should work in most cases.

Define a -main function in the main namespace of the service, e.g practicalli.gameboard.service

The -main function starts the Donut system and keeps the system reference as a local name

The system reference is used to shutdown the system, typically wrapped in code to handle SIGTERM signals from the infrastructure running the service (Operating system, Kubernettes, EC2, etc.)

Start a Donut system

(defn -main\n  \"practicalli service managed by donut system,\n  Aero is used to configure Integrant configuration based on profile (dev, test, prod),\n  allowing environment specific configuration, e.g. mulog publisher\n  The shutdown hook gracefully stops the service on receipt of a SIGTERM from the infrastructure,\n  giving the application 30 seconds before forced termination.\"\n  []\n\n  (mulog/set-global-context!\n   {:app-name \"practicalli donoughty service\" :version  \"0.1.0\"})\n\n  (mulog/log ::gameboard-system :system-config system/config)\n\n  (let [running-system (donut/signal system/system ::donut/start)]\n       (.addShutdownHook\n         (Runtime/getRuntime)\n         (Thread. ^Runnable #(donut/signal running-system ::donut/stop)))))\n
Mulog event logging is included and the system should start a mulog publisher via the system configuration

"},{"location":"service-repl-workflow/donut-system/#service-repl-workflow","title":"Service REPL Workflow","text":"

donut.system.repl namespace provides functions to start, stop and restart system components.

The main system configuration used when starting the service can also be used for the REPL, or other named systems can be defined allowing for a customised system during development.

Service REPL workflow

(ns system-repl\n  \"Tools for REPL Driven Development\"\n  (:require\n   [donut.system :as donut]\n   [donut.system.repl :as donut-repl]\n   [practicalli.donoughty.system :as donoughty]\n   [com.brunobonacci.mulog :as mulog]))\n\n(defmethod donut/named-system :donut.system/repl\n  [_] donoughty/main)\n\n(defn start\n  \"Start system with donut, optionally passing a named system\"\n  ([] (donut-repl/start))\n  ([system-config] (donut-repl/start system-config)))\n\n(defn stop\n  \"Stop the currently running system\"\n  []  (donut-repl/stop))\n\n(defn restart\n  \"Restart the system with donut repl,\n  Uses clojure.tools.namespace.repl to reload namespaces\n  `(clojure.tools.namespace.repl/refresh :after 'donut.system.repl/start)`\"\n  [] (donut-repl/restart))\n
"},{"location":"service-repl-workflow/mulog-events/","title":"Mulog","text":"

Mulog is library that defines log events as data, with a wide range of publisher for popular log aggregation services, e.g. Elastic Search, Cloudwatch, Kinesis, Prometheus, etc.)

Creating a custom publisher, all mulog events can be sent to portal data inspector.

"},{"location":"service-repl-workflow/mulog-events/#mulog-event","title":"Mulog event","text":"

mulog/log function is used to define an event message. The first argument is a unique event name, followed by key/value pairs that define the contents of the message.

Simple Mulog Event

mulog/log ::dev-user-ns :message \"Example event message\" :ns (ns-publics *ns*))\n
"},{"location":"service-repl-workflow/mulog-events/#mulog-configuration","title":"Mulog configuration","text":"

mulog/set-global-context! defines key/value pairs included in every mulog event allowing a separate context to be used for logs, e.g. :env :dev indicating development time events.

TapPublisher defines a custom Mulog publisher which wraps tap> around every mulog event created, sending each mulog event to Portal.

tap-publisher is a var that starts the custom mulog publisher, providing a reference to the publisher so it can be shut down.

stop function is provided as a convienient way to stop the publisher via the REPL.

Mulog events publisher

dev/mulog_events.clj
;; ---------------------------------------------------------\n;; Mulog Global Context and Custom Publisher\n;;\n;; - set event log global context\n;; - tap publisher for use with Portal and other tap sources\n;; - publish all mulog events to Portal tap source\n;; ---------------------------------------------------------\n\n(ns mulog-events\n  (:require\n   [com.brunobonacci.mulog        :as mulog]\n   [com.brunobonacci.mulog.buffer :as mulog-buffer]))\n\n;; ---------------------------------------------------------\n;; Set event global context\n;; - information added to every event for REPL workflow\n(mulog/set-global-context! {:service-name \"todo-tracker Service\",\n                            :version \"0.1.0\", :env \"dev\"})\n;; ---------------------------------------------------------\n\n;; ---------------------------------------------------------\n;; Mulog event publishing\n\n(deftype TapPublisher\n         [buffer transform]\n  com.brunobonacci.mulog.publisher.PPublisher\n  (agent-buffer [_] buffer)\n  (publish-delay [_] 200)\n  (publish [_ buffer]\n    (doseq [item (transform (map second (mulog-buffer/items buffer)))]\n      (tap> item))\n    (mulog-buffer/clear buffer)))\n\n#_{:clj-kondo/ignore [:unused-private-var]}\n(defn ^:private tap-events\n  [{:keys [transform] :as _config}]\n  (TapPublisher. (mulog-buffer/agent-buffer 10000) (or transform identity)))\n\n(def tap-publisher\n  \"Start mulog custom tap publisher to send all events to Portal\n  and other tap sources\n  `mulog-tap-publisher` to stop publisher\"\n  (mulog/start-publisher!\n   {:type :custom, :fqn-function \"mulog-events/tap-events\"}))\n\n#_{:clj-kondo/ignore [:unused-public-var]}\n(defn stop\n  \"Stop mulog tap publisher to ensure multiple publishers are not started\n Recommended before using `(restart)` or evaluating the `user` namespace\"\n  []\n  tap-publisher)\n\n;; ---------------------------------------------------------\n
"},{"location":"service-repl-workflow/portal/","title":"Portal","text":"

Start Portal and capture all evaluation results over nrepl when portal middleware included in the REPL startup.

All evaluation carried out over nREPL, i.e. between the connected editor and the Clojure REPL, will be sent to Portal.

Use Practicalli Clojure CLI Config aliases or define your own alias in the project or user deps.edn file.

Practicalli Clojure CLI ConfigAlias definition

:repl/reloaded aliases from Practicalli Clojure CLI Config starts a REPL process with Portal and nrepl middleware

Connect an editor to the REPL via the nREPL server port created during the REPL startup

clojure -M:repl/reloaded\n

make repl also launches Portal listening over nREPL in projects created with Practicalli Project Templates

Define an alias that includes the portal library in the :extra-deps section and portal.nrepl/wrap-portal nrepl middleware in the :main-opts section with the --middleware flag.

Clojure CLI alias including Portal & nREPL middleware

:repl/reloaded\n{:extra-paths [\"dev\" \"test\"]\n :extra-deps {nrepl/nrepl                  {:mvn/version \"1.0.0\"}\n              cider/cider-nrepl            {:mvn/version \"0.37.0\"}\n              com.bhauman/rebel-readline   {:mvn/version \"0.1.4\"}\n              djblue/portal                {:mvn/version \"0.46.0\"}\n              clj-commons/clj-yaml         {:mvn/version \"1.0.27\"}\n              org.clojure/tools.namespace  {:mvn/version \"1.4.4\"}\n              org.clojure/tools.trace      {:mvn/version \"0.7.11\"}\n              org.slf4j/slf4j-nop          {:mvn/version \"2.0.9\"}\n              com.brunobonacci/mulog       {:mvn/version \"0.9.0\"}\n              lambdaisland/kaocha          {:mvn/version \"1.86.1355\"}\n              org.clojure/test.check       {:mvn/version \"1.1.1\"}\n              ring/ring-mock               {:mvn/version \"0.4.0\"}\n              criterium/criterium          {:mvn/version \"0.4.6\"}}\n :main-opts  [\"-e\" \"(apply require clojure.main/repl-requires)\"\n              \"--main\" \"nrepl.cmdline\"\n              \"--middleware\" \"[cider.nrepl/cider-middleware,portal.nrepl/wrap-portal]\"\n              \"--interactive\"\n              \"-f\" \"rebel-readline.main/-main\"]}\n
"},{"location":"service-repl-workflow/portal/#launching-portal","title":"Launching Portal","text":"

Start Portal listening to all evaluations

dev/portal.clj
(ns portal\n  (:require\n   [portal.api :as inspect]))\n\n(def instance\n  \"Open portal window if no portal sessions have been created.\n   A portal session is created when opening a portal window\"\n  (or (seq (inspect/sessions))\n      (inspect/open {:portal.colors/theme :portal.colors/gruvbox})))\n\n;; Add portal as tapsource (add to clojure.core/tapset)\n(add-tap #'portal.api/submit)\n
"},{"location":"service-repl-workflow/portal/#themes","title":"Themes","text":"

A portal theme can be specified when starting portal, e.g. :portal.colors/gruvbox.

"},{"location":"service-repl-workflow/portal/#reference-docs","title":"Reference Docs","text":"

Portal nREPL connection documentation

"},{"location":"service-repl-workflow/system-repl/","title":"System REPL workflow","text":"

dev/system_repl.clj file provides functions to start, stop and restart the components in the Clojure system. This functions are required into the custom user namespace (dev/user.clj).

Practicalli Project templates creates a dev/system_repl.clj with one of the following approaches

  • (default) Atom reference to restart http server and refresh namespaces
  • :component :donut provides a donut-party/system definition and component management functions, including refresh namepaces
  • :component :integrant provides Integrant REPL: Integrant REPL using Integrant system definition and component management functions, including refresh namepaces
AtomDonut SystemIntegrant REPL

A reference to the http server started by http-kit is held in a Clojure atom named http-server-reference. The reference can be used to stop the http server without requiring a restart of the Clojure REPL.

System REPL - Atom based Restart

;; ---------------------------------------------------------\n;; System REPL - Atom Restart \n;;\n;; Tools for REPl workflow with Aton reference to HTTP server \n;; https://practical.li/clojure-web-services/app-servers/simple-restart/\n;; ---------------------------------------------------------\n\n(ns system-repl\n  (:require \n    [clojure.tools.namespace.repl :refer [refresh]]\n    [practicalli.todo-tracker.service :as service]))\n\n;; ---------------------------------------------------------\n;; HTTP Server State\n\n(defonce http-server-instance (atom nil))\n;; ---------------------------------------------------------\n\n\n;; ---------------------------------------------------------\n;; REPL workflow commands\n\n(defn stop\n  \"Gracefully shutdown the server, waiting 100ms\"\n  []\n  (when-not (nil? @http-server-instance)\n    (@http-server-instance :timeout 100)\n    (reset! http-server-instance nil)\n    (println \"INFO: HTTP server shutting down...\")))\n\n(defn start\n  \"Start the application server and run the application\"\n  [& port]\n  (let [port (Integer/parseInt\n              (or (first port)\n                  (System/getenv \"PORT\")\n                  \"8080\"))]\n    (println \"INFO: Starting server on port:\" port)\n\n    (reset! http-server-instance\n            (service/http-server-start port))))\n\n\n(defn restart\n  \"Stop the http server, refresh changed namespace and start the http server again\"\n  []\n  (stop)\n  (refresh)  ;; Refresh changed namespaces\n  (start))\n;; ---------------------------------------------------------\n

donut-party/system is a data-centric configuration that includes functions to :start and :stop each component.

dev/service_repl.clj defines functions to manage the components defined in the Clojure system

  • start the components, optionally passing a named system
  • stop components in the currently running system
  • restart stops components, reloads namespaces and starts components
  • system returns a hash-map of currently running system state

Donut System REPL functions

;; ---------------------------------------------------------\n;; Donut System REPL\n;;\n;; Tools for REPl workflow with Donut system components\n;; ---------------------------------------------------------\n\n(ns system-repl\n  \"Tools for REPl workflow with Donut system components\"\n  (:require\n   [donut.system :as donut]\n   [donut.system.repl :as donut-repl]\n   [donut.system.repl.state :as donut-repl-state]\n   [practicalli.todo-donut.system :as system]))\n\n\n(defmethod donut/named-system :donut.system/repl\n  [_] system/main)\n\n(defn start\n  \"Start system with donut, optionally passing a named system\"\n  ([] (donut-repl/start))\n  ([system-config] (donut-repl/start system-config)))\n\n(defn stop\n  \"Stop the currently running system\"\n  []  (donut-repl/stop))\n\n(defn restart\n  \"Restart the system with donut repl,\n  Uses clojure.tools.namespace.repl to reload namespaces\n  `(clojure.tools.namespace.repl/refresh :after 'donut.system.repl/start)`\"\n  [] (donut-repl/restart))\n\n(defn system\n  \"Return: fully qualified hash-map of system state\"\n  [] donut-repl-state/system)\n

Donut System configuration

;; ---------------------------------------------------------\n;; practicalli.todo-donut\n;;\n;; TODO: Provide a meaningful description of the project\n;;\n;; Start the service using donut configuration and an environment profile.\n;; ---------------------------------------------------------\n\n(ns practicalli.todo-donut.system\n  \"Service component lifecycle management\"\n  (:gen-class)\n  (:require\n   ;; Application dependencies\n   [practicalli.todo-donut.router :as router]\n\n   ;; Component system\n   [donut.system :as donut]\n   ;; [practicalli.todo-donut.parse-system :as parse-system]\n\n   ;; System dependencies\n   [org.httpkit.server     :as http-server]\n   [com.brunobonacci.mulog :as mulog]))\n\n;; ---------------------------------------------------------\n;; Donut Party System configuration\n\n(def main\n  \"System Component management with Donut\"\n  {::donut/defs\n   ;; Option: move :env data to resources/config.edn and parse with aero reader\n   {:env\n    {:http-port 8080\n     :persistence\n     {:database-host (or (System/getenv \"POSTGRES_HOST\") \"http://localhost\")\n      :database-port (or (System/getenv \"POSTGRES_PORT\") \"5432\")\n      :database-username (or (System/getenv \"POSTGRES_USERNAME\") \"clojure\")\n      :database-password (or (System/getenv \"POSTGRES_PASSWORD\") \"clojure\")\n      :database-schema (or (System/getenv \"POSTGRES_SCHEMA\") \"clojure\")}}\n\n    ;; mulog publisher for a given publisher type, i.e. console, cloud-watch\n    :event-log\n    {:publisher\n     #::donut{:start (fn mulog-publisher-start\n                       [{{:keys [publisher]} ::donut/config}]\n                       (mulog/log ::log-publish-component\n                                  :publisher-config publisher\n                                  :local-time (java.time.LocalDateTime/now))\n                       (mulog/start-publisher! publisher))\n\n              :stop (fn mulog-publisher-stop\n                      [{::donut/keys [instance]}]\n                      (mulog/log ::log-publish-component-shutdown :publisher instance :local-time (java.time.LocalDateTime/now))\n                      ;; Pause so final messages have chance to be published\n                      (Thread/sleep 250)\n                      (instance))\n\n              :config {:publisher {:type :console :pretty? true}}}}\n\n    ;; HTTP server start - returns function to stop the server\n    :http\n    {:server\n     #::donut{:start (fn http-kit-run-server\n                       [{{:keys [handler options]} ::donut/config}]\n                       (mulog/log ::http-server-component\n                                  :handler handler\n                                  :port (options :port)\n                                  :local-time (java.time.LocalDateTime/now))\n                       (http-server/run-server handler options))\n\n              :stop  (fn http-kit-stop-server\n                       [{::donut/keys [instance]}]\n                       (mulog/log ::http-server-component-shutdown\n                                  :http-server-instance instance\n                                  :local-time (java.time.LocalDateTime/now))\n                       (instance))\n\n              :config {:handler (donut/local-ref [:handler])\n                       :options {:port  (donut/ref [:env :http-port])\n                                 :join? false}}}\n\n     ;; Function handling all requests, passing system environment\n     ;; Configure environment for router application, e.g. database connection details, etc.\n     :handler (router/app (donut/ref [:env :persistence]))}}})\n\n;; End of Donut Party System configuration\n;; ---------------------------------------------------------\n

Integrant REPL manages Integrant components from the REPL.

Practicalli uses Integrant configuration, resources/config.edn also used by the service when deployed or otherwise calling the -main function of the service.

Integrant REPL functions

;; ---------------------------------------------------\n;; System component management for REPL workflow\n;;\n;; System config components defined in `resources/config.edn`\n;;\n;; `system` namespace automatically loaded via the `dev/user.clj` namespace\n;;\n;; Commands:\n;; `(start)` starts all components in system config\n;; `(restart)` reads system config, reloads changed namespaces & restarts system\n;; `(restart-all)` as above with all namespaces reloaded\n;; `(stop)` shutdown all components in the system (gracefully where appropriate)\n;; `(system)` show configuration of the running system\n;; `(config)` system configuration\n;;\n;; NOTE: standard IntegrantREPL code, maintenance should not be required\n;; ---------------------------------------------------\n\n(ns system-repl\n  \"Configure the system components and provide Integrant REPL convenience functions\n  to start/stop/restart components and show system configuration\"\n  (:require\n   ;; REPL workflow\n   [integrant.repl       :as ig-repl]\n   [integrant.repl.state :as ig-state]\n   [clojure.pprint :as pprint]\n\n   [practicalli.todo-integrant.parse-system :as parse-system]))\n\n(println \"Loading system namespace for Integrant REPL\")\n\n\n;; ---------------------------------------------------------\n;; System Configuration\n;; - `resources/config.edn` Integrant & Aero system configuration\n\n(defn environment-prep!\n  \"Parse system configuration with aero-reader and apply the given profile values\n  Return: Integrant configuration to be used to start the system\n  integrant.repl/set-prep! takes an anonymous function that returns an integrant configuration\n  Arguments: profile - a keyword determining the environment - :dev :test :stage :live\"\n\n  [profile]\n  (ig-repl/set-prep! #(parse-system/aero-prep profile)))\n\n;; ---------------------------------------------------------\n\n\n;; ---------------------------------------------------------\n;; Integrant REPL convenience functions\n;; - enable use of aero profiles (`dev`, `stage`, `prod`)\n;; - simplify Integrant REPL commands for managing the system\n\n(defn start\n  \"Prepare configuration and start the system services with Integrant-repl\"\n  ([] (start :dev))\n  ([profile] (environment-prep! profile) (ig-repl/go)))\n\n\n(defn restart\n  \"Read updates from the system configuration, reloads changed namespaces\n  and restart the system services with Integrant-repl\"\n  ([] (restart :dev))\n  ([profile] (environment-prep! profile) (ig-repl/reset)))\n\n\n(defn restart-all\n  \"Read updates from the configuration, reloads all namespaces\n  and restart the system services with Integrant-repl\"\n  ([] (restart-all :dev))\n  ([profile] (environment-prep! profile) (ig-repl/reset-all)))\n\n\n(defn stop\n  \"Shutdown all services\"\n  []\n  (ig-repl/halt))\n\n\n(defn system\n  \"The running system configuration,\n  including component references and specific profile values\"\n  []\n  ig-state/system)\n\n\n(defn config\n  \"The current system configuration used by Integrant\"\n  []\n  (pprint/pprint ig-state/config))\n\n;; End of Integrant REPL convenience functions\n;; ---------------------------------------------------------\n

Integrant System configuration

;; --------------------------------------------------\n;; System Component configuration - Integrant & Integrant REPL\n;;\n;; - Event logging (mulog)\n;; - HTTP Server  (embedded jetty or http-kit)\n;; - Request routing (reitit)\n;; - Persistence (relational) connection\n;;\n;; Components managed in practicalli.todo-integrant.system namespace\n;;\n;; #profile used by aero to select the configuration to use for a given profile (dev, test, prod)\n;; #long defines Long Integer type (required for Java HTTP server port)\n;; #env reads the environment variable of the given name\n;; #or uses first non nil value in sequence\n;;\n;; Environment variables should be defined locally and in deployment provisioner tooling\n;; --------------------------------------------------\n\n\n{;; --------------------------------------------------\n ;; Event logging service - mulog\n\n ;; https://github.com/BrunoBonacci/mulog#publishers\n ;; https://github.com/openzipkin/zipkin\n :practicalli.todo-integrant.system/log-publish\n {;; Type of publisher to use for mulog events\n  ;; Publish json format logs, captured by fluentd and exposed via OpenDirectory\n  :mulog #profile {:dev  {:type :console-json :pretty? true}\n\n                   ;; Multiple publishers using Open Zipkin service (started via docker-compose)\n                   :docker  {:type :multi\n                             :publishers\n                             [{:type :console-json :pretty? false}\n                              {:type :zipkin :url \"http://localhost:9411/\"}]}\n\n                   :prod {:type :console-json :pretty? false}}}\n\n ;; --------------------------------------------------\n ;; HTTP Server - embedded service\n\n :practicalli.todo-integrant.system/http-server\n {;; Router function passed into the HTTP server form managing requests/responses\n  :handler #ig/ref :practicalli.todo-integrant.system/router\n\n  ;; Port number (Java Long type) - environment variable or default number\n  :port  #long #or [#env HTTP_SERVER_PORT 8080]\n\n  ;; Join REPL to HTTP server thread\n  :join? false}\n\n ;; --------------------------------------------------\n ;; persistence - connection to relational storage\n\n ;; TODO: add database connection pool ?\n\n :practicalli.todo-integrant.system/relational-store\n {:host #or [#env DATABASE_HOST \"localhost\"]\n  :port #or [#env DATABASE_PORT 3306]\n  :username #or [#env DATABASE_USERNAME \"gameboard\"]\n  :password #or [#env DATABASE_PASSWORD \"trustnoone\"]}\n\n ;; --------------------------------------------------\n ;; Data provider services\n ;; - connection to services that provide eSports data\n\n :practicalli.todo-integrant.system/data-provider\n {;; external data providers\n  :game-service-base-url  #or [#env GAME_SERVICE_BASE_URL \"http://localhost\"]\n  :llamasoft-api-uri  #or [#env LAMASOFT_API_URI \"http://localhost\"]\n  :polybus-report-uri \"/report/polybus\"\n  :moose-life-report-uri \"/api/v1/report/moose-life\"\n  :minotaur-arcade-report-uri \"/api/v2/minotar-arcade\"\n  :gridrunner-revolution-report-uri \"/api/v1.1/gridrunner\"\n  :space-giraffe-report-uri \"/api/v1/games/space-giraffe\"}\n\n ;; --------------------------------------------------\n ;; routing\n\n ;; Configure web routing application with application environment\n ;; define top-level keys to access via the environment hash-map\n ;; - :persistence - database connection information\n ;; - :data-provider - url, endpoint, tokens for external services \n :practicalli.todo-integrant.system/router\n {:persistence #ig/ref :practicalli.todo-integrant.system/relational-store\n  :data-provider #ig/ref :practicalli.todo-integrant.system/data-provider}}\n

Integrant System components

;; ---------------------------------------------------------\n;; practicalli.todo-integrant\n;;\n;; TODO: Provide a meaningful description of the project\n;;\n;; Start the service using Integrant configuration and an environment profile.\n;; A profile is injected into the configuration in the `practicalli.gameboard.environment` namespace\n;; and the resulting configuration is used by Integrant to start the system components\n;;\n;; The service consist of\n;; - httpkit web application server\n;; - metosin/reitit for routing and ring for request / response management\n;; - mulog event logging service\n;;\n;; Related namespaces\n;; `resources/config.edn` system configuration with environment #profile placeholders\n;; `practicalli.environment` injects profile & other aero tag values into a resulting configuration\n;; ---------------------------------------------------------\n\n(ns practicalli.todo-integrant.system\n  \"Service component lifecycle management\"\n  (:gen-class)\n  (:require\n   ;; Application dependencies\n   [practicalli.todo-integrant.router :as router]\n\n   ;; Component system\n   [practicalli.todo-integrant.parse-system :as parse-system]\n\n   ;; System dependencies\n   [org.httpkit.server     :as http-server]\n   [integrant.core         :as ig]\n   [com.brunobonacci.mulog :as mulog]))\n\n;; --------------------------------------------------\n;; Configure and start application components\n\n(defn initialise\n  \"initialise the system using Integrant\"\n  [profile]\n  (ig/init (parse-system/aero-prep profile)))\n\n;; Start mulog publisher for the given publisher type, i.e. console, cloud-watch\n#_{:clj-kondo/ignore [:unused-binding]}\n(defmethod ig/init-key ::log-publish\n  [_ {:keys [mulog] :as config}]\n  (mulog/log ::log-publish-component :publisher-config mulog :local-time (java.time.LocalDateTime/now))\n  (let [publisher (mulog/start-publisher! mulog)]\n    publisher))\n\n;; Connection for Relational Database Persistence\n;; return hash-map of connection values: endpoint, access-key, secret-key\n;; TODO: add example of connection pool\n(defmethod ig/init-key ::relational-store\n  [_ {:keys [connection] :as config}]\n  (mulog/log ::persistence-component :connection connection :local-time (java.time.LocalDateTime/now))\n  config)\n\n;; Connections for data services\n(defmethod ig/init-key ::data-provider\n  [_ config]\n  (mulog/log ::data-provider-component :configuration config :local-time (java.time.LocalDateTime/now))\n  config)\n\n;; Configure environment for router application, e.g. database connection details, etc.\n(defmethod ig/init-key ::router\n  [_ config]\n  (mulog/log ::app-routing-component :app-config config)\n  (router/app config))\n\n;; HTTP server start - returns function to stop the server\n(defmethod ig/init-key ::http-server\n  [_ {:keys [handler port join?]}]\n  (mulog/log ::http-server-component :handler handler :port port :local-time (java.time.LocalDateTime/now))\n  (http-server/run-server handler {:port port :join? join?}))\n\n;; Shutdown HTTP service\n(defmethod ig/halt-key! ::http-server\n  [_ http-server-instance]\n  (mulog/log ::http-server-component-shutdown  :http-server-object http-server-instance :local-time (java.time.LocalDateTime/now))\n  ;; Calling http instance shuts down that instance\n  (http-server-instance))\n\n;; Shutdown Log publishing\n(defmethod ig/halt-key! ::log-publish\n  [_ publisher]\n  (mulog/log ::log-publish-component-shutdown :publisher-object publisher :local-time (java.time.LocalDateTime/now))\n  ;; Pause so final messages have chance to be published\n  (Thread/sleep 250)\n  ;; Call publisher again to stop publishing\n  (publisher))\n\n(defn stop\n  \"Stop service using Integrant halt!\"\n  [system]\n  (mulog/log ::http-server-sigterm :system system :local-time (java.time.LocalDateTime/now))\n  ;; (println \"Shutdown of service via Integrant\")\n  (ig/halt! system))\n\n;; --------------------------------------------------\n
"},{"location":"service-repl-workflow/integrant/","title":"Integrant Overview","text":"

Integrant manages the life-cycle of components that are composed to create the Clojure service, i.e. start, stop, restart.

Integrant uses a declarative configuration (resources/config.edn) to define a system configuration.

Components are managed using runtime polymorphism, i.e. defmethod, to define how each component is managed.

  • init-key start a component
  • halt-key! stop a component

Integrant REPL manages components during development to restart the services, loading all code changes into the REPL (especially useful after ranaming functions and namespaces)

integrant.repl.state/config shows the configuration used to start the service. integrant.repl.state/system to inspect the configuration state of the running system.

Integrant and Integrant REPL can share the same system configuration file, although are otherwise separate ways of working with a system.

"},{"location":"service-repl-workflow/integrant/#integrant-configuration","title":"Integrant configuration","text":"

Define the configuration for each part of the system, such as http server (jetty, httpkit), router application (reitit, compojure, ring) and persistence storage (postgres, crux)

Use a shared resources/config.edn file with Integrant for consistency. Or if there is significant experimentation to be done, create a dev/resources/config.edn file

Define composite components

resources/config.edn
{:practicalli.gameboard.service/http-server\n {:handler #ig/ref :practicalli.gameboard.service/router\n  :port  8888\n  :join? false}\n\n :practicalli.gameboard.service/router\n {:persistence #ig/ref :practicalli.gameboard.service/relational-store}\n\n :practicalli.gameboard.service/relational-store\n {:connection  {:url \"http://localhost/\" :port 57207 :database \"gameboard\"}}}\n

Fully qualified keywords, e.g. domain.service.name/component, are used so that keys are unique throughout the system.

The fully qualified name is the namespace that contains the defmethod init-key for the key. The Integrant load-namespaces function will automatically load all namespaces that match key names

"},{"location":"service-repl-workflow/integrant/#composite-components","title":"Composite components","text":"

Components can be composed of configuration and references to other components, creating a composite component.

For example an HTTP component may reference a request handler component and in turn the request handler may include a database connection component.

Integrant uses the #ig/ref tag literal to define a references to anther component.

Component relationships in a Clojure web service
  • relational-store defines a database connection
  • an request router includes a reference to the database connection, so handlers can be passed connection details
  • http-server includes a reference to the router that will assign all requests to the relevant handler functions
    {:practicalli.gameboard.service/relational-store\n {:connection {:url \"http://localhost/\" :port 57207 :database \"scoreboard\"}}\n\n :practicalli.gameboard.service/router\n {:persistence #ig/ref :practicalli.gameboard.service/relational-store}}\n\n :practicalli.gameboard.service/http-server\n {:handler #ig/ref :practicalli.gameboard.service/router\n  :join? false}\n
"},{"location":"service-repl-workflow/integrant/#aero-and-integrant","title":"Aero and Integrant","text":"

Aero defines a range of tag literals that can be used in a system configuration.

Aero does not include the #ig/ref reference so needs to be taught how to handle this tag using a defmethod.

Define integrant ref tag for Aero reader

Define ig/ref tag for Aero reader
(defmethod aero/reader 'ig/ref\n  [_ tag value]\n  (ig/ref value))\n

Now aero can parse a system configuration EDN file that contains Integrant references

"},{"location":"service-repl-workflow/integrant/#integrant-system-configuration","title":"Integrant System Configuration","text":"

The system configuration is a hash-map with each component defined as a top-level key and its associated values (a hash-map of key-value pairs). Component dependencies are defined by including a component key within the definition of another component.

Defining relationships between services, such as an HTTP server and a Persistent store, are achieved by passing the relevant parts of the configuration to each service. As this is information is managed at the top level of a Clojure system, it avoids unnecessary coupling between system services.

The request router is dependant on a database connection and an external data provider connection to provide information to the handlers that satisfy the requests.

Define the request router as a composite component including these dependant components via an Integrant reference, #ig/ref

Integrant dependant components

resources/clojure.edn
 {:practicalli.gameboard.service/router\n  {:persistence #ig/ref :practicalli.gameboard.service/relational-store\n   :data-provider #ig/ref :practicalli.gameboard.service/data-provider}}\n
"},{"location":"service-repl-workflow/integrant/integrant-system/","title":"Integrant implementation","text":"

Define

  • an integrant system configuration in resources/config.edn
  • Integrant init-key used to start each component
  • Integrant halt-key! to stop each component
  • Define -main function to load the system configuration, optionally parse with aero, start all components and hold a reference to the running system that listens to SIGTERM events
"},{"location":"service-repl-workflow/integrant/integrant-system/#prepare-system","title":"Prepare system","text":"

Use prep function to load namespaces into the REPL.

Aero Parse system config and load component namespaces

Prepare system confige and load namespaces
(defn aero-prep\n  \"Parse the system config and update values for the given profile (:dev, :test :prod)\n  Top-level keys in `config.edn` use fully qualified namespace name for `ig/init-key` defmethod\n  `ig/load-namespaces` automatically loads each namespace referenced by a top-level key\n  Return: configuration hash-map for specified profile (:dev :test :prod) with aero tags resolved\"\n  [profile]\n  (let [config (aero-config profile)]\n    ;; (mulog/log ::integrant-load-namespaces :config config :local-time (java.time.LocalDateTime/now)\n    (ig/load-namespaces config)\n    config))\n
"},{"location":"service-repl-workflow/integrant/integrant-system/#initialise-components","title":"Initialise Components","text":"

Define how components are initialised (started and/or configured)

Start mulog publisher for the given publisher type, i.e. console, cloud-watch

Start mulog publisher

(defmethod ig/init-key ::log-publish\n  [_ {:keys [mulog] :as config}]\n  (mulog/log ::log-publish-component :publisher-config mulog :local-time (java.time.LocalDateTime/now))\n  (let [publisher (mulog/start-publisher! mulog)]\n    publisher))\n

Connection for a Relational Database Persistence which returns a hash-map of connection values: endpoint, access-key, secret-key

Database Connection

(defmethod ig/init-key ::relational-store\n  [_ {:keys [connection] :as config}]\n  (mulog/log ::persistence-component :connection connection :local-time (java.time.LocalDateTime/now))\n  config)\n

TODO: add example of connection pool

Configure environment for router application, e.g. database connection details, etc.

Request Router

(defmethod ig/init-key ::router\n  [_ config]\n  (mulog/log ::app-routing-component :app-config config)\n  (router/app config))\n

HTTP server start - returns function to stop the server

HTTP Server start

(defmethod ig/init-key ::http-server\n  [_ {:keys [handler port join?]}]\n  (mulog/log ::http-server-component :handler handler :port port :local-time (java.time.LocalDateTime/now))\n  (http-server/run-server handler {:port port :join? join?}))\n
"},{"location":"service-repl-workflow/integrant/integrant-system/#shutdown-components","title":"Shutdown Components","text":"

Define how each component should be halted (if required)

Server processes should be halted gracefully

Shut down components via Integrant

Shut down all components using the Integrant system configuration
(defn stop\n  \"Stop service using Integrant halt!\"\n  [system]\n  (mulog/log ::http-server-sigterm :system system :local-time (java.time.LocalDateTime/now))\n  ;; (println \"Shutdown of Practicall Gameboard service via Integrant\")\n  (ig/halt! system))\n

Shutdown HTTP service

Halt HTTP server

(defmethod ig/halt-key! ::http-server\n  [_ http-server-instance]\n  (mulog/log ::http-server-component-shutdown  :http-server-object http-server-instance :local-time (java.time.LocalDateTime/now))\n  ;; Calling http instance shuts down that instance\n  (http-server-instance))\n
"},{"location":"service-repl-workflow/integrant/integrant-system/#halt-process-gracefully","title":"Halt process gracefully","text":"

Use the Java addShutdownHook method to obtains detail of the current JVM runtime environment and call a Clojure stop function with the Integrant system configuration to stop the components.

(.addShutdownHook (Runtime/getRuntime) (Thread. ^Runnable #(stop system)))\n
Typical 30 second shutdown

Most Cloud Infrastructure should provide 30 seconds to shut down all services before the operating system begins to shut down.

The -main function from the Gameboard Web Service which starts all components with a given Aero profile (:dev profile default).

The -main function creates a system local name from preparing the Integrant system configuration, which is passed to the Clojure service stop function via an .addShutdownHook to gracefully shut down the services

-main function to start and shut down an HTTP server gracefully

 (defn -main\n   \"Gameboard service is started with `ig/init` and the Integrant configuration,\n   with the return value bound to the namespace level `system` name.\n   Aero is used to configure Integrant configuration based on profile (dev, test, prod),\n   allowing environment specific configuration, e.g. mulog publisher\n   The shutdown hook calling a zero arity function, gracefully stopping the service\n   on receipt of a SIGTERM from the infrastructure, giving the application 30 seconds before forced termination.\"\n   []\n\n   (let [profile (or (keyword (System/getenv \"SERVICE_PROFILE\"))\n                     :dev)\n\n         ;; Add keys to every event / publish profile use to start the service\n         _ (mulog/set-global-context!\n            {:app-name \"Practicalli Gameboard Service\" :version  \"0.1.0\" :env profile})\n\n         system (ig/init (environment/aero-prep profile))\n\n         _ (mulog/log ::gameboard-system :system-config system)]\n\n     ;; Gracefully shutdown the HTTP server on recieving a SIGTERM\n     (.addShutdownHook (Runtime/getRuntime) (Thread. ^Runnable #(stop system)))))\n
"},{"location":"service-repl-workflow/integrant/integrant-system/#halt-event-log-publisher","title":"Halt Event Log Publisher","text":"

The event log publisher should be the last component to be shut down to ensure all events have been captured and had time to be published.

Shutdown the mulog event publisher process, including a (Thread/sleep 250) to sleep the thread for 250ms to give time for all events to be published.

Halt event log publisher

(defmethod ig/halt-key! ::log-publish\n  [_ publisher]\n  (mulog/log ::log-publish-component-shutdown :publisher-object publisher :local-time (java.time.LocalDateTime/now))\n  ;; Pause so final messages have chance to be published\n  (Thread/sleep 250)\n  ;; Call publisher again to stop publishing\n  (publisher))\n

Important to shut down the mulog publisher

If the mulog publisher is not shut down then multiple publishers could be run when restarting system components. Each publisher running will publish each event, leading to events being published multiple times.

The mulog configuration can be defined to run multiple types of publishers, which if not shut down on a system restart will publish mutltipe events to each type of publisher.

Ensuring all types of mulog publishers are shut down will avoid this issue.

Stoping the REPL process will also terminate any mulog publishers that were initialised.

Practicalli Gameboard Service Integrant System Configuration
(ns practicalli.gameboard.system\n  \"Service component lifecycle management\"\n  (:gen-class)\n  (:require\n   ;; Component system\n   [{{top/ns}}.{{main/ns}}.parse-system :as parse-system]\n\n   ;; System dependencies\n   [integrant.core         :as ig]\n   [com.brunobonacci.mulog :as mulog]))\n\n;; --------------------------------------------------\n;; Configure and start application components\n\n;; Start mulog publisher for the given publisher type, i.e. console, cloud-watch\n#_{:clj-kondo/ignore [:unused-binding]}\n(defmethod ig/init-key ::log-publish\n  [_ {:keys [mulog] :as config}]\n  (mulog/log ::log-publish-component :publisher-config mulog :local-time (java.time.LocalDateTime/now))\n  (let [publisher (mulog/start-publisher! mulog)]\n    publisher))\n\n;; Connection for Relational Database Persistence\n;; return hash-map of connection values: endpoint, access-key, secret-key\n;; TODO: add example of connection pool\n(defmethod ig/init-key ::relational-store\n  [_ {:keys [connection] :as config}]\n  (mulog/log ::persistence-component :connection connection :local-time (java.time.LocalDateTime/now))\n  config)\n\n;; Connections for data services\n(defmethod ig/init-key ::data-provider\n  [_ config]\n  (mulog/log ::data-provider-component :configuration config :local-time (java.time.LocalDateTime/now))\n  config)\n\n;; Configure environment for router application, e.g. database connection details, etc.\n(defmethod ig/init-key ::router\n  [_ config]\n  (mulog/log ::app-routing-component :app-config config)\n  (router/app config))\n\n;; HTTP server start - returns function to stop the server\n(defmethod ig/init-key ::http-server\n  [_ {:keys [handler port join?]}]\n  (mulog/log ::http-server-component :handler handler :port port :local-time (java.time.LocalDateTime/now))\n  (http-server/run-server handler {:port port :join? join?}))\n\n;; Shutdown HTTP service\n(defmethod ig/halt-key! ::http-server\n  [_ http-server-instance]\n  (mulog/log ::http-server-component-shutdown  :http-server-object http-server-instance :local-time (java.time.LocalDateTime/now))\n  ;; Calling http instance shuts down that instance\n  (http-server-instance))\n\n;; Shutdown Log publishing\n(defmethod ig/halt-key! ::log-publish\n  [_ publisher]\n  (mulog/log ::log-publish-component-shutdown :publisher-object publisher :local-time (java.time.LocalDateTime/now))\n  ;; Pause so final messages have chance to be published\n  (Thread/sleep 250)\n  ;; Call publisher again to stop publishing\n  (publisher))\n\n(defn stop\n  \"Stop service using Integrant halt!\"\n  [system]\n  (mulog/log ::http-server-sigterm :system system :local-time (java.time.LocalDateTime/now))\n  ;; (println \"Shutdown of Billie Fraud API service via Integrant\")\n  (ig/halt! system))\n;; --------------------------------------------------\n
"},{"location":"service-repl-workflow/integrant/repl/","title":"Integrant REPL","text":"

Integrant REPL is a library to manage components as part of a REPL workflow, to extend features provided by Integrant.

Integrant REPL includes functions to start, stop and restart services during development, enabling changes to the system without restarting the REPL process.

(start), (reset) and (stop) functions are evaluated in the REPL to control the system.

To assist debugging, (config) displays the parsed system configuration and (system) shows how the configuration has being resolved (service instances, profile values, etc.). Viewing the live system configuration is especially useful when using aero and environment variables to confirm the expected values are used.

"},{"location":"service-repl-workflow/integrant/repl/#user-namespace","title":"User namespace","text":"

Common practice is to place the Integrant REPl code in a user namespace, which is automatically loaded when the REPL process starts.

The user namespace is defined separately from the source code, as it is code to develop the service rather than part of the service itself. The user namespace is added to the dev/user.clj file and added to the classpath via an alias, e.g. :env/dev or :repl/reloaded aliases from Practicalli Clojure CLI Config

Custom user namespace

dev/user.clj
(ns user\n  (:require\n   ;; REPL workflow\n   [integrant.repl       :as ig-repl]\n   [integrant.repl.state :as ig-state]\n\n   ;; Environment parsing\n   [aero.core :as aero]\n\n   ;; Utilities\n   [clojure.pprint :as pprint]))\n

Practicalli REPL Startup - detailed examples

Practicalli Clojure CLI Config aliases defines aliases that include the dev directory that contains the user namespace on the class path

REPL ReloadedDev ToolsPath

:repl/reloaded alias starts a rich terminal REPL prompt, with the dev path and several tools to enhance the REPL workflow

clojure -M:repl/reloaded\n

:dev/reloaded alias adds the dev path and several tools to enhance the REPL workflow

clojure -M:dev/reloaded:repl/rebl\n

:env/dev alias adds the dev path on REPL start up, include the dev/user.clj file

clojure -M:env/dev:repl/rebl\n
"},{"location":"service-repl-workflow/integrant/repl/#environment-configuration","title":"Environment Configuration","text":"

Using aero with the Integrant configuration file includes tag literals that need to be resolved.

Parsing Aero tags in Integrant system configuration

;;;; Aero environment management\n\n;; extra reader tag for Integrant references\n(defmethod aero/reader 'ig/ref\n  [_ tag value]\n  (ig/ref value))\n\n(defn aero-config\n  \"Profile specific configuration for all services.\n  Profiles supported: :develop :stage :live\"\n  [profile]\n  (aero/read-config (io/resource \"config.edn\") {:profile profile}))\n\n(defn aero-prep\n  \"Parse the system config and update values for the given profile (:develop, :stag :live)\n  Top-level keys in the config.edn use a qualified name of the Clojure namespace the ig/init-key defmethod is defined in\n  ig/load-namespaces will automatically load each namespace referenced by a top-level key in the Integrant configuration\n  Return: configuration hash-map for the specified profile (:develop :stage :live)\"\n  [profile]\n  (let [config (aero-config profile)]\n    (ig/load-namespaces config)\n    config))\n
"},{"location":"service-repl-workflow/integrant/repl/#parse-configuration","title":"Parse Configuration","text":"

Parsing Integrant system configuration

(defn integrant-prep!\n  \"Parse system configuration with aero-reader and apply the given profile values\n  Return: Integrant configuration to be used to start the system\n\n  integrant.repl/set-prep! takes an anonymous function that returns an integrant configuration\n\n  Arguments: profile - a keyword determining the environment - :develop :test :stage :live\"\n\n  [profile]\n  (ig-repl/set-prep!\n   #(aero-prep profile)))\n
"},{"location":"service-repl-workflow/integrant/repl/#repl-convenience-functions","title":"REPL convenience functions","text":"

REPL convenience functions

(defn go\n  \"Prepare configuration and start the system services with Integrant-repl\"\n  ([] (go :develop))\n  ([profile] (integrant-prep! profile) (ig-repl/go)))\n\n(defn reset\n  \"Read updates from the configuration and restart the system services with Integrant-repl\"\n  ([] (reset :develop))\n  ([profile] (integrant-prep! profile) (ig-repl/reset)))\n\n(defn reset-all\n  \"Read updates from the configuration and restart the system services with Integrant-repl\"\n  ([] (reset-all :develop))\n  ([profile] (integrant-prep! profile) (ig-repl/reset-all)))\n\n(defn stop\n  \"Shutdown all services\"\n  [] (ig-repl/halt))\n\n(defn system\n  \"The running system configuration\"\n  [] ig-state/system)\n\n(defn config\n  \"The current system configuration used by Integrant\"\n  [] ig-state/config)\n
"},{"location":"service-repl-workflow/integrant/repl/#repl-commands","title":"REPL Commands","text":"

REPL commands

(comment\n  ;; Prepare and start the system using the :develop profile or specify the environment\n  (go)\n  (go :test)\n\n  ;; Reload changed and new source code files and restart the system\n  (reset)\n  (reset :develop)\n\n  ;; Reload all source code files on the Classpath and restart the system\n  (reset-all)\n  (reset-all :develop)\n\n  ;; Return the current Integrant configuration (already parsed by environment)\n  (config)\n\n  ;; Show the running system configuration, returns nil when system not running\n  (system)\n\n  ;; Shutdown the system using the app-server object reference in the Integrant state\n  (stop)\n\n  ;; Pretty print the system state in the REPL\n  (pprint/pprint ig-state/system)\n\n  #_()) ;; End of rich comment block\n
requiring-resolve for Just In Time requires

Integrant in practice provides an example of using requiring-resolve to avoid including all requires in the ns form, potentially reducing REPL startup time by not adding library

When calling an Integrant function, requiring-resolve returns the name of the symbol if already available in the REPL, or requires the functions namespace if the function is not available.

The library containing the namespace must be part of the class path when the REPL starts (or library has been hotloaded into the REPL)

(ns user\n  \"Reduce REPL startup time by not including requires\")\n\n(defmacro jit\n  \"Resolve a symbol name and require its namespace if not currently available in the REPL\"\n  [qualified-symbol]\n  `(requiring-resolve '~qualified-symbol))\n\n(defn set-prep! []\n  ((jit integrant.repl/set-prep!) #((jit feralberry.system/prep) :dev)))\n\n(defn go []\n  (set-prep!)\n  ((jit integrant.repl/go)))\n\n(defn reset []\n  (set-prep!)\n  ((jit integrant.repl/reset)))\n\n(defn system []\n  @(jit integrant.repl.state/system))\n\n(defn config []\n  @(jit integrant.repl.state/config))\n

"}]} \ No newline at end of file +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Practicalli Clojure Web Services","text":"

Develop server-side web services and API's from the ground up using Clojure following a simple and data-centric design and applying functional programming concepts.

Use a REPL Workflow approach to provide instant feedback on the code behaviour as it is written, validating design decisions as they are made.

"},{"location":"#tools","title":"Tools","text":"

Clojure CLI is used to manage library dependencies and run Clojure code, enhanced with aliases from Practicalli Clojure CLI Config.

Larger projects use Integrant & Integrant REPL to manage components and state, using a reloaded REPL workflow to manage changes in addtion to evaluating functions in the REPL.

Persistence is provides via Postgresql (and eventually JUXT Crux)

tools.build will be used to create Clojure artefacts for deployment, with GitHub actions and Docker used for Continuous Integration and orchestrating systems.

make is a general build tool used to support project development and support automation of wokflow tasks.

Heroku deployment to be archived

Heroku Cloud service deployment approach is being archived as the service no longer provides a developer environment (November 2022)

Older content using Leiningen

Older content uses Leiningen for project configuration. This content can be converted to a Clojure CLI project by creating a deps.edn file containing the relevant dependencies. Add a build.clj configuration to create assets to deploy, e.g. jar & uberjar.

"},{"location":"#library-composition-approach","title":"Library Composition approach","text":"

The Clojure community provides a diverse set of libraries, each focused on a specific need. Libraries are assembled to rapidly develop a tailored solution, avoiding bloat and the unnecessary complexity that comes with large frameworks. Libraries are relatively simple to replace with alternatives or used as inspiration for your own custom functions.

Templates can be used to create example projects with common libraries, with code to show showing how libraries can be wired together. provide examples of libraries working together.

Avoiding large frameworks

Frameworks are design decisions others have made and generalised to solve a range of problem, so there is no guarantee on how many of those decisions are relevant for the current project.

Frameworks tend to include many features not relevant to the current problem, which can be challenging to remove or replace. Frameworks can be over relied upon, taking away an opportunity to think about the most relevant solution.

Clojure does not focus on the classic framework approach like Rails or Spring, for this reason.

"},{"location":"#navigate-the-book","title":"Navigate the book","text":"

Use the mouse or built-in key bindings to navigate the pages of the book

  • P , , : go to previous page
  • N , . : go to next page

Use the search box to quickly find a specific topic

  • F , S , / : open search dialog
  • Down , Up : select next / previous result
  • Esc , Tab : close search dialog
  • Enter : follow selected result
"},{"location":"#sponsor-my-work","title":"Sponsor my work","text":"

All sponsorship recieved is used to maintain and further develop the Practicalli series of books and videos, although most of the work is still done with my own time and cost.

Thank you to Cognitect, Nubank and a wide range of other sponsors from the Clojure community for your continued support

"},{"location":"#creative-commons-license","title":"Creative commons license","text":"This work is licensed under a Creative Commons Attribution 4.0 ShareAlike License (including images & stylesheets)."},{"location":"adding-more-route/using-cond-function/","title":"Using cond function","text":"

Change the if function to cond and define additional routes you want to match on. We will show you a /goodbye route, feel free to add your own.

Edit the src/webdev/core.clj file and update the greet function as follows

(defn greet\n  \"A function to process all requests for the web server.  The default route / returns one message, /goodbye route another. for all other routes an error message is returned\"\n  [request]\n  (cond\n   (= \"/\" (:uri request))\n   {:status 200\n    :body \"Hello, Clojure World.  I now update automatically\"\n    :headers {}}\n   (= \"/goodbye\" (:uri request))\n   {:status 200\n    :body \"This is the end, my old friend\"\n    :headers {}}\n   :else\n   {:status 404\n    :body \"Sorry, page not found\"\n    :headers {}}))\n

Writing a big cond statement for all our routes would be really tedious and difficult to manage. So lets look at Compojure.

"},{"location":"app-servers/","title":"Application servers","text":"

Application servers provide a common platform services to support server-side running of JVM applications (hence the term application server).

These servers are often referred to more generically as web servers as they mostly work over http / https.

Clojure uses embedded servers to support REPL Driven Development, so both new function definitions and server restarts can be managed within the context of a running REPL (avoiding the need to restart the REPL).

"},{"location":"app-servers/#application-components","title":"Application components","text":"
  • Routing
  • Requests
  • Responses
  • Middleware
"},{"location":"app-servers/#practicalli-defacto-library-choices","title":"Practicalli defacto library choices","text":"

Practicalli defacto choices for building web services:

Library Purpose ring/ring Provides Jetty and Ring - managing requests and responses in Clojure using hash-maps metosin/reitit Routing of request and responses, support for ring handlers and middleware (and interceptors)"},{"location":"app-servers/#example-projects","title":"Example Projects","text":"Project Description Status Monitor Clojure CLI project using Httpkit, Compojure for routing, Hiccup and SVG graphics. Deployed via CircleCI on Heroku Banking On Clojure Clojure CLI project using httpkit, Ring utilities, Compojure for routing. relational data store using next.jdbc, HoneySQL, clojure.spec & postgresql. Generative testing using clojure.spec ToDo app Leiningen project using Ring (Jetty), Compojure for routing and Hiccup for HTML generation"},{"location":"app-servers/app-server-logging/","title":"Application Server Logging","text":"
  • What to log in which environments
  • Logging levels
  • Logging as object rather than text
  • mulog
"},{"location":"app-servers/app-server-logging/#simplistic-logging","title":"Simplistic logging","text":"

println function sends information to the standard out and so is a very simple mechanism to create logs from specific parts of the application. This should be used sparingly and is no substitute for a specific logging framework.

println can be useful in the REPL the standard out message as well as the evaluation result (nil) are shown. println can provide additional feedback for non-terminating processes that run in the REPL, such as an application server.

"},{"location":"app-servers/app-server-logging/#logging-to-elastic-search-kibana","title":"Logging to Elastic Search / Kibana","text":"

Log messages as objects, rather than text strings, provides greater sophistication by search tools as the messages have a structure.

  • Elastisch - Clojure client for Elasticsearch and GitHub repository
  • Elasticsearch and Clojure: Getting Started - the practical academic
  • Spandex - Elasticsearch new low level rest-client wrapper
"},{"location":"app-servers/app-server-logging/#problematic-practices","title":"Problematic Practices","text":"

Logging to the REPL - sending lots of logs to the REPL makes the REPL much harder to use directly

Logging strings - logs entries are typically objects and far more searchable and discoverable that strings, so send objects to the logging service

"},{"location":"app-servers/atom-based-restart/","title":"Atom based restart","text":"

A Clojure atom is used to hold a reference to a running server instance.

An atom is a mutable container that holds any type of value. The value in the atom is immutable. The atom is mutable, but only with specific functions, avoiding locking issues often arising with mutable values.

  • swap! the current value in the atom using a function to create a new value
  • reset! the current value in the atom with a specific value
  • deref or @ returns the value contained within the atom
"},{"location":"app-servers/atom-based-restart/#reference-to-server-process","title":"Reference to server process","text":"

A reference to the server process will be held in a Clojure atom, a mutable container. An atom is used as a value for the server reference will be swapped into the atom on server start. The atom is set to nil when the server stops. Using an atom allows for this value to change.

Define a Clojure atom with the initial value of nil.

defonce is used instead of def to prevent the reference to the app-server being lost if the expression is re-evaluated. A restart of the REPL process is required before evaluation the expression has an effect.

(defonce app-server-instance (atom nil))\n
"},{"location":"app-servers/atom-based-restart/#-main-function","title":"-main function","text":"

The -main function determines an HTTP port value, from either an argument, an operating system $PORT environment variable or using the default 8888 value.

-main calls app-server-start which starts the app server and resets the value of the atom with a reference to that instance.

(.stop @app-server-instance) uses the instance reference to stop the server. app-server-stop function check to see if a running instance exists and if so, stops the server.

HTTP Kit timeout

HTTP Kit can be sent a timeout value to gracefully shut down the server. The app-server-stop function sends a :timeout 100 value to the running app server instance.

The REPL is still running, so the server can be started by calling (-main) or (app-server-start 8888).

app-server-restart is a convenience function that stops and starts the application server, meaning the developer only needs to evaluate (app-server-restart)

jettyhttpkit
(ns practicalli.example-webapp\n  (:gen-class)\n  (:require [ring.adapter.jetty :as jetty]\n            [compojure.core :refer [defroutes GET]]))\n\n;; Routing\n(defroutes app\n  (GET \"/\" [] {:status 200 :body \"App Server Running\"}))\n\n\n;; System\n;; Reference to server instance\n(defonce app-server-instance (atom nil))\n\n\n(defn app-server-start\n\"Start Jetty Application server, adding instance to global state\"\n  [port]\n  (reset! app-server-instance\n          (jetty/run-jetty #'app {:port port :join? false})))\n\n\n(defn app-server-stop\n  \"Check for a running app-server instance, shutdown if present\"\n  []\n  (when @app-server-instance\n    (.stop @app-server-instance))\n  (reset! app-server-instance nil))\n\n\n(defn app-server-restart\n  \"Stop and then start the application server, loading in the new code\"\n  []\n  (app-server-stop)\n  (app-server-start (Integer. (or (System/getenv \"PORT\") 8888))))\n\n\n(defn -main\n  \"Determine an HTTP port number and start application server on that port.\n  A value for port can be passed as the first argument to the command to start the application via the CLI\"\n  [& [port]]\n  (let [port (Integer. (or port\n                           (System/getenv \"PORT\")\n                           8888))]\n    (app-server-start port)))\n\n\n;; REPL driven development\n(comment\n (app-server-restart)\n (-main)\n)\n
(ns practicalli.example-webapp\n  (:gen-class)\n  (:require [org.httpkit.server :as app-server]\n            [compojure.core :refer [defroutes GET]]))\n\n\n;; Routing\n\n(defroutes app\n  (GET \"/\" [] {:status 200 :body \"App Server Running\"}))\n\n\n;; System\n\n(defonce app-server-instance (atom nil))\n\n\n(defn app-server-stop\n  \"Gracefully shutdown the server, waiting 100ms\"\n  []\n  (when-not (nil? @app-server-instance)\n    (@app-server-instance :timeout 100)\n    (reset! app-server-instance nil)\n    (println \"INFO: Application server shutting down...\")))\n\n\n(defn app-server-start\n  \"Start the application server and run the application\"\n  [port]\n  (println \"INFO: Starting server on port: \" port)\n\n  (reset! app-server-instance\n          (app-server/run-server #'app {:port (Integer/parseInt port)})))\n\n\n(defn app-server-restart\n  \"Convenience function to stop and start the application server\"\n  []\n  (app-server-stop)\n  (-main))\n\n(defn -main\n  \"Start the application server on a specific port\"\n  [& [port]]\n  (let [port (Integer. (or port (System/getenv \"PORT\") 8888))]\n    (app-server-start port)))\n\n\n;; REPL driven development\n\n(comment\n\n  ;; Re/Start application server\n  (app-server-restart)\n\n  ;; Shutdown server\n  (app-server-stop)\n\n)\n

Http-kit server documentation contains details of asynchronous websockets and HTTP streaming configurations.

"},{"location":"app-servers/clojure-project/","title":"New Clojure Project","text":"

Create a new Clojure project using Clojure CLI

deps-newclj-new

Create a new project using the :project/create alias from Practicalli Clojure CLI Config and the app template using deps-new

clojure -T:project/create :template app :name practicalli/web-service\n

Create a new project using the :project/new alias from Practicalli Clojure CLI Config and the app template using clj-new

clojure -T:project/new :template app :name practicalli/web-service\n

"},{"location":"app-servers/clojure-project/#add-web-server-library","title":"Add web server library","text":"

Add either Ring (Jetty) or Httpkit library as a project dependency to run an embedded server that listens to HTTP requests and passes those requests to the Clojure service.

JettyHTTP Kit

Add a ring library that includes an embedded Jetty server.

The ring/ring library includes all ring libraries including the embedded Jetty server

Edit the project deps.edn file and add the ring/ring {:mvn/version \"1.9.5\"} dependency to the top-level :deps key, which defines the libraries used to make the project.

{:paths [\"src\" \"resources\"]\n :deps {org.clojure/clojure {:mvn/version \"1.11.3\"}\n        ring/ring           {:mvn/version \"1.9.6\"}}}\n

Or add the ring/ring-core and ring/ring-jetty-adapter libraries, saving a few millisecond when starting the project.

{:paths [\"src\" \"resources\"]\n :deps {org.clojure/clojure     {:mvn/version \"1.11.3\"}\n        ring/ring-core          {:mvn/version \"1.9.6\"}\n        ring/ring-jetty-adapter {:mvn/version \"1.9.6\"}}}\n

Add the HTTP Kit Server library which includes the client and server namespaces, although only the Server namespace will be used.

Edit the project deps.edn file and add the http-kit/http-kit {:mvn/version \"2.3.0\"} dependency to the top-level :deps key, which defines the libraries used to make the project.

{:paths [\"src\" \"resources\"]\n :deps {org.clojure/clojure {:mvn/version \"1.11.3\"}\n        http-kit/http-kit   {:mvn/version \"2.3.0\"}}}\n
"},{"location":"app-servers/clojure-project/#ring-library","title":"Ring library","text":"

Ring is a Clojure web applications library inspired by Python's WSGI and Ruby's Rack. By abstracting the details of HTTP into a simple, unified API, Ring allows web applications to be constructed of modular components that can be shared among a variety of applications, web servers, and web frameworks.

Ring interface specification

Ring is composed of several libraries which can be included specifically, rather than requiring all of them with ring/ring

  • ring/ring-core - essential functions for handling parameters, cookies and more
  • ring/ring-devel - functions for developing and debugging Ring applications
  • ring/ring-servlet - construct Java servlets from Ring handlers
  • ring/ring-jetty-adapter - a Ring adapter that uses the Jetty webserver

Ring documentation Ring API docs

"},{"location":"app-servers/create-server/","title":"Create a server","text":"

To create a web (http) server using a common library, e.g. Jetty or Http-kit

  1. Require a library that provides the web server
  2. Create a function to start a server, taking a port number as an option
"},{"location":"app-servers/create-server/#require-web-server-library","title":"Require Web Server Library","text":"jettyhttpkit

Add the web server library to the namespace using a :reqiure directive.

(ns practicalli.web-server\n  (:gen-class)\n  (:require [ring.adapter.jetty :as http-server]))\n

Add the web server library to the namespace using a :reqiure directive.

(ns practicalli.web-server\n  (:gen-class)\n  (:require [org.httpkit.server :as http-server]))\n
"},{"location":"app-servers/create-server/#code-a-basic-web-server","title":"Code a basic web server","text":"jettyhttpkit

Define a function called server-start that takes a value for the port number which the server will listen too.

Call the run-jetty function from ring.adapter.jetty to start the Jetty Server. run-jetty takes several arguments

  • the main request handler, initially just a function (usually a router function to handle many different types of requests)
  • a hash-map of options, e.g. {:port 8080 :join? false}. Options are listed in the run-jetty function definition.

The :port key is associated with an integer value that represents the port number.

The :join key is associated with a boolean value, true if the the REPL process should attached to the Jetty thread (blocking input until server stops), false to continue in a separate thread.

  (defn server-start\n    [port]\n    (http-server/run-jetty #'app {:port port :join? false}))\n

Define a function called server-start that takes a value for the port number which the server will listen too.

Call the run-server function from org.httpkit.server to start the Http-kit server. run-jetty takes several arguments

  • the main request handler, initially just a function (usually a router function to handle many different types of requests)
  • a hash-map of options, e.g. {:port 8080 :join? false}. Options are listed in the run-server function definition.

The :port key is associated with an integer value that represents the port number.

(defn server-start\n  \"Start the application server and run the application\"\n  [port]\n  (app-server/run-server #'app {:port port}))\n

Http-kit server documentation contains details of asynchronous websockets and HTTP streaming configurations.

"},{"location":"app-servers/create-server/#request-handler-function","title":"Request handler function","text":"

The server passes all HTTP requests, converted to a request hash-map by ring, to the app function. The app function returns a ring response hash-map which is sent back to the client (browser) as an Http response.

Define a function that takes a request hash-map as an argument and returns a basic response hash-map.

(defn app [request]\n  {:status  200\n   :headers {:content-type \"text/html\"}\n   :body    \"<h1>Clojure Web Server Alive</h1>\"})\n

:status http status code as an integer - 200 means okay

:header response headers such as content type, a hash-map with optional values (the value can be an empty hash-map {})

:body a string containing the body of the response, such as text, HTML or JSON.

"},{"location":"app-servers/debugging/","title":"Debugging application servers","text":""},{"location":"app-servers/debugging/#debugging-handlers","title":"Debugging handlers","text":"

As handler functions are simply Clojure functions that take a request hash-map, those functions can be called from unit tests or the REPL to test they are working correctly.

"},{"location":"app-servers/debugging/#ring-mock","title":"Ring mock","text":"

Generate mock requests and responses (?) for testing handler functions

"},{"location":"app-servers/debugging/#problematic-practices-to-avoid","title":"Problematic Practices to avoid","text":"

Using (def name ,,,) expressions for debugging is very bad, especially if those expressions are left in production code.

(println ,,,) statements seem convenient however have very limited value. Using the REPL and REPL based debugging tools provide very useful output

"},{"location":"app-servers/http-kit-server-options/","title":"Http-kit Server Options","text":"

Available options are defined in the doc-string of org.httpkit.server/run-server

Options Description :ip Which ip (if has many ips) to bind :port Which port listen incomming request :thread Http worker thread count :queue-size Max job queued before reject to project self :max-body Max http body: 8m :max-ws Max websocket message size :max-line Max http inital line length :proxy-protocol Proxy protocol e/o #{:disable :enable :optional} :worker-name-prefix Worker thread name prefix :worker-pool ExecutorService to use for request-handling (:thread, :worker-name-prefix, :queue-size are ignored if set) :error-logger Arity-2 fn (args: string text, exception) to log errors :warn-logger Arity-2 fn (args: string text, exception) to log warnings :event-logger Arity-1 fn (arg: string event name) :event-names map of HTTP-Kit event names to respective loggable event names :server-header The \"Server\" header. If missing, defaults to \"http-kit\", disabled if nil. :legacy-return-value? true (default) returns a (fn stop-server [& {:keys [timeout] :or {timeout 100}}]) ; false (recommended) Returns the HttpServer which can be used with server-port, ; server-status, server-stop!, etc.

See Httpkit migration documentation to see the minor difference between Http-kit server and other ring compliant servers like Jetty.

"},{"location":"app-servers/java-system-properties/","title":"Java System properties","text":"

System properties can be set on the Java command line using the -Dpropertyname=value syntax. They can also be added at Clojure runtime using (System/getProperties) will return a Properties object with the system properties for the current REPL.

Properties are often defined in a *.properties file to configure the environment in containerized deployment processes. For example, the version of Java used in Heroku containers is set by adding java.runtime.version=11 property to a system.properties file.

"},{"location":"app-servers/java-system-properties/#commonly-used-properties","title":"Commonly used properties","text":"Java Runtime Description java.home JRE home directory java.library.path JRE library search path for search native libraries (usually taken from PATH environment variable) java.class.path JRE classpath e.g., '.' (dot \u2013 used for current working directory). java.ext.dirs JRE extension library path(s) java.version JDK version java.runtime.version JRE version File system Description file.separator symbol for file directory separator ('/' for Unix or '\\' for windows) path.separator symbol for separating path entries in PATH or CLASSPATH. (':' for Unix or ';' for windows) line.separator symbol for end-of-line / new line (\"\\n\" for Unix or \"\\r\\n\" for windows) or /Mac OS X. User system Description user.name the user\u2019s name. user.home the user\u2019s home directory. user.dir the user\u2019s current working directory Operating System Description os.name operating System name os.version operating System version os.arch operating System architecture"},{"location":"app-servers/java-system-properties/#examining-the-system-properties","title":"Examining the system properties","text":"

Evaluating (System/getProperties) on an Ubuntu Linux operating system running Java 11 and Spacemacs with CIDER returned the following properties.

  \"sun.desktop\" = \"gnome\"\n  \"awt.toolkit\" = \"sun.awt.X11.XToolkit\"\n  \"java.specification.version\" = \"11\"\n  \"sun.cpu.isalist\" = \"\"\n  \"sun.jnu.encoding\" = \"UTF-8\"\n  \"java.class.path\" = \"src:resources:/home/practicalli/.m2/repository/org/clojure/clojure/1.10.1/clojure-1.10.1.jar:/home/practicalli/.m2/repository/joda-time/joda-time/2...\n  \"java.vm.vendor\" = \"Ubuntu\"\n  \"sun.arch.data.model\" = \"64\"\n  \"sun.font.fontmanager\" = \"sun.awt.X11FontManager\"\n  \"java.vendor.url\" = \"https://ubuntu.com/\"\n  \"user.timezone\" = \"Europe/London\"\n  \"os.name\" = \"Linux\"\n  \"java.vm.specification.version\" = \"11\"\n  \"sun.java.launcher\" = \"SUN_STANDARD\"\n  \"user.country\" = \"US\"\n  \"sun.boot.library.path\" = \"/usr/lib/jvm/java-11-openjdk-amd64/lib\"\n  \"sun.java.command\" = \"clojure.main -m nrepl.cmdline --middleware [\\\"refactor-nrepl.middleware/wrap-refactor\\\", \\\"cider.nrepl/cider-middleware\\\"]\"\n  \"jdk.debug\" = \"release\"\n  \"sun.cpu.endian\" = \"little\"\n  \"user.home\" = \"/home/practicalli\"\n  \"user.language\" = \"en\"\n  \"java.specification.vendor\" = \"Oracle Corporation\"\n  \"clojure.libfile\" = \".cpcache/4064833315.libs\"\n  \"java.version.date\" = \"2020-04-14\"\n  \"java.home\" = \"/usr/lib/jvm/java-11-openjdk-amd64\"\n  \"file.separator\" = \"/\"\n  \"java.vm.compressedOopsMode\" = \"Zero based\"\n  \"line.separator\" = \"\\n\"\n  \"java.specification.name\" = \"Java Platform API Specification\"\n  \"java.vm.specification.vendor\" = \"Oracle Corporation\"\n  \"java.awt.graphicsenv\" = \"sun.awt.X11GraphicsEnvironment\"\n  \"sun.management.compiler\" = \"HotSpot 64-Bit Tiered Compilers\"\n  \"java.runtime.version\" = \"11.0.7+10-post-Ubuntu-3ubuntu1\"\n  \"user.name\" = \"practicalli\"\n  \"path.separator\" = \":\"\n  \"os.version\" = \"5.4.0-40-generic\"\n  \"java.runtime.name\" = \"OpenJDK Runtime Environment\"\n  \"file.encoding\" = \"UTF-8\"\n  \"java.vm.name\" = \"OpenJDK 64-Bit Server VM\"\n  \"java.vendor.url.bug\" = \"https://bugs.launchpad.net/ubuntu/+source/openjdk-lts\"\n  \"java.io.tmpdir\" = \"/tmp\"\n  \"java.version\" = \"11.0.7\"\n  \"user.dir\" = \"/home/practicalli/projects/clojure/database-access/banking-on-clojure-webapp\"\n  \"os.arch\" = \"amd64\"\n  \"java.vm.specification.name\" = \"Java Virtual Machine Specification\"\n  \"java.awt.printerjob\" = \"sun.print.PSPrinterJob\"\n  \"sun.os.patch.level\" = \"unknown\"\n  \"java.library.path\" = \"/usr/java/packages/lib:/usr/lib/x86_64-linux-gnu/jni:/lib/x86_64-linux-gnu:/usr/lib/x86_64-linux-gnu:/usr/lib/jni:/lib:/usr/lib\"\n  \"java.vendor\" = \"Ubuntu\"\n  \"java.vm.info\" = \"mixed mode, sharing\"\n  \"java.vm.version\" = \"11.0.7+10-post-Ubuntu-3ubuntu1\"\n  \"sun.io.unicode.encoding\" = \"UnicodeLittle\"\n  \"apple.awt.UIElement\" = \"true\"\n  \"java.class.version\" = \"55.0\"\n
"},{"location":"app-servers/jetty-server-options/","title":"Jetty Server Options","text":"

Option keys available to pass to the run-jetty function from ring.adaptor.jetty

Option Description :configurator a function called with the Jetty Server instance :async? if true, treat the handler as asynchronous :async-timeout async context timeout in ms (defaults to 0, no timeout) :async-timeout-handler an async handler to handle an async context timeout :port the port to listen on (defaults to 80) :host the hostname to listen on :join? blocks the thread until server ends (defaults to true) :daemon? use daemon threads (defaults to false) :http? listen on :port for HTTP traffic (defaults to true) :ssl? allow connections over HTTPS :ssl-port the SSL port to listen on (defaults to 443, implies :ssl? is true) :ssl-context an optional SSLContext to use for SSL connections :exclude-ciphers when :ssl? is true, additionally exclude these cipher suites :exclude-protocols when :ssl? is true, additionally exclude these protocols :replace-exclude-ciphers? when true, :exclude-ciphers will replace rather than add to the cipher exclusion list (defaults to false) :replace-exclude-protocols? when true, :exclude-protocols will replace rather than add to the protocols exclusion list (defaults to false) :keystore the keystore to use for SSL connections :keystore-type the keystore type (default jks) :key-password the password to the keystore :keystore-scan-interval if not nil, the interval in seconds to scan for an updated keystore :thread-pool custom thread pool instance for Jetty to use :truststore a truststore to use for SSL connections :trust-password the password to the truststore :max-threads the maximum number of threads to use (default 50) :min-threads the minimum number of threads to use (default 8) :max-queued-requests the maximum number of requests to be queued :thread-idle-timeout Set the maximum thread idle time. Threads that are idle for longer than this period may be stopped (default 60000) :max-idle-time the maximum idle time in milliseconds for a connection (default 200000) :client-auth SSL client certificate authenticate, may be set to :need,:want or :none (defaults to :none) :send-date-header? add a date header to the response (default true) :output-buffer-size the response body buffer size (default 32768) :request-header-size the maximum size of a request header (default 8192) :response-header-size the maximum size of a response header (default 8192) :send-server-version? add Server header to HTTP response (default true)"},{"location":"app-servers/middleware/","title":"Middleware","text":"

Apply common transformations to request and or response hash-maps, such as security tokens, cookie management, session and access management, presentation templates, etc.

Middleware is implemented by Clojure functions that receive a handler as an argument and return a handler as a result.

"},{"location":"app-servers/middleware/#middleware-in-ring","title":"Middleware in Ring","text":"

Middleware can wrap handlers or other middleware, affecting their behavior.

For example the wrap-reload middleware enables live reloading by detecting file changes and reloading affected functions into their namespace, before the request is passed to the relevant handler function

Middleware provided by Ring includes:

In ring/ring-core:

  • wrap-cookies (ring.middleware.cookies)
  • wrap-file (ring.middleware.file)
  • wrap-file-info (ring.middleware.file-info)
  • wrap-flash (ring.middleware.flash)
  • wrap-keyword-params (ring.middleware.keyword-params)
  • wrap-multipart-params (ring.middleware.multipart-params
  • wrap-nested-params (ring.middleware.nested-params
  • wrap-params (ring.middleware.params)
  • wrap-session (ring.middleware.session)

In ring/ring-devel:

  • wrap-lint (ring.middleware.lint)
  • wrap-reload (ring.middleware.reload)
  • wrap-stacktrace (ring.middleware.stacktrace)
"},{"location":"app-servers/overview/","title":"Overview of Application servers","text":"

Application servers for Clojure run an embedded JVM application, such as Jetty or http-kit server.

App servers are started within the Clojure REPL process, as an embedded server. This approach means that during development a server can be restarted to load in new code and immediately update the running application, without having to restart the REPL.

Embedded servers in Clojure

Clojure takes a more distributed approach to deployment, starting an embedded application server within the application itself. This approach is more conducive to the container and cloud compute infrastructure. Scaling is achieved by running multiple instances of the application, each on its own embedded application server.

In Java was common to have a single application server with all applications deployed as Jar or War archives. This fitted with the classic architecture of deploying on a single resource rich physical hardware server. Clojure applications can also be deployed in this classic approach if required.

"},{"location":"app-servers/overview/#which-application-server-to-use","title":"Which Application Server to use","text":"

The most commonly used application servers are in the table below, with Jetty being the most common as it is wrapped by the ring library.

Jetty is the the defacto server in Practicalli guides, with occasional examples using other libraries.

Application Server Description Eclipse Jetty The original embedded Java application server most commonly used for Clojure web apps Http-kit High performance Clojure/Java application server Apache Tomcat Classic Java application server, very common in JVM environments Netty Java NIO asynchronous event-driven network application framework Aleph HTTP Clojure (Netty) Ring-compliant server with support for returning Manifold for asynchronous programming"},{"location":"app-servers/overview/#eclipse-jetty","title":"Eclipse Jetty","text":"

Jetty is the most commonly used application server on the Java Virtual machine.

Eclipse Jetty provides a Web server and javax.servlet container, plus support for HTTP/2, WebSocket, OSGi, JMX, JNDI, JAAS and many other integrations. These components are open source and available for commercial use and distribution.

Eclipse Jetty is used in a wide variety of projects and products, both in development and production. Jetty can be easily embedded in devices, tools, frameworks, application servers, and clusters. See the Jetty Powered page for more uses of Jetty.

The current recommended version for use is Jetty 9

"},{"location":"app-servers/overview/#http-kit","title":"Http-kit","text":"

HTTP Kit is a minimalist, efficient, Ring-compatible HTTP client/server for Clojure.

HTTP Kit uses a event-driven architecture to support highly concurrent a/synchronous web applications. Feature a unified API for WebSocket and HTTP long polling/streaming

The underlying server is implemented in Java with a Clojure wrapper.

"},{"location":"app-servers/overview/#apache-tomcat","title":"Apache Tomcat","text":"

Apache Tomcat is an open source implementation of the Java Servlet, JavaServer Pages, Java Expression Language and Java WebSocket technologies. The Java Servlet, JavaServer Pages, Java Expression Language and Java WebSocket specifications are developed under the Java Community Process.

Apache Tomcat software powers numerous large-scale, mission-critical web applications across a diverse range of industries and organizations, example are listed on the PoweredBy page.

Apache Tomcat, Tomcat, Apache, the Apache feather, and the Apache Tomcat project logo are trademarks of the Apache Software Foundation.

Apache Tomcat 9.0 is the current stable version with active development now on version 10.

Tomcat can be run in embedded mode, so it is not necessary to build a WAR file and deploy it in a standalone Tomcat server.

  • Create a Java Web Application Using Embedded Tomcat - Heroku

Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.

Netty is a NIO client server framework which enables quick and easy development of network applications such as protocol servers and clients. It greatly simplifies and streamlines network programming such as TCP and UDP socket server.

"},{"location":"app-servers/overview/#aleph-and-manifold","title":"Aleph and Manifold","text":"

Manifold provides streams focused libraries, such as Aleph HTTP for web applications and TCP/UDP for more general networking.

"},{"location":"app-servers/route-requests/","title":"Route Requests","text":"

Https servers accept many different types of requests, defined as a combination of

  • Https protocol - GET, POST, etc.
  • Path of resource - page, api endpoint, etc.
"},{"location":"app-servers/route-requests/#defining-routes","title":"Defining routes","text":"

Reitit is a data driven approach to defining routes and support ring Ring request, response and middleware.

Compojure provides the defroutes macro and a simple DSL for defining routes.

reititcompojure

Add reitit as a dependency

deps.edn
{:paths\n [\"src\" \"resources\"]\n\n :deps\n {org.clojure/clojure {:mvn/version \"1.10.1\"}\n  http-kit/http-kit   {:mvn/version \"2.3.0\"}\n  ring/ring-core      {:mvn/version \"1.8.1\"}\n  metosin/reitit      {:mvn/version \"0.5.18\"}}}\n

Restart the REPL (unless Reitit was added using hotload)

Add reitit namespace to web-server namespace

(ns practicalli.web-server\n  (:gen-class)\n  (:require [ring.adapter.jetty :as http-server]\n            [reitit.core :as routing]))\n

Define example routes

(def routes\n [[\"/\"\n    {:handler (constantly {:status 200\n                           :headers {:content-type \"text/html\"}\n                           :body \"<h1>Welcome to Practicalli Clojure Web Server\"})}\n [\"status\"\n   {:handler (constantly {:status 200\n                          :headers {:content-type \"application/json\"}\n                          :body {:alive true}})}]]])\n

Define example routes using the reitit ring-handler

(defroutes app\n  \"Initial routes for web server\"\n  []\n\n  (reitit-ring/ring-handler\n   (reitit-ring/router       ;; routes defined as a vector of vectors\n    [\n     [\"/\"\n\n     ]\n]\n    ;; Middleware, coersion & content negotiation\n\n   ;; Default hanlder passed to ring-handler\n   (ring/routes\n    ;; Respond to any other route - returns blank page\n    ;; TODO: create custom handler for routes not recognised\n    (ring/create-default-handler))\n)))\n

Add Compojure as a dependency

{:paths\n [\"src\" \"resources\"]\n\n :deps\n {org.clojure/clojure {:mvn/version \"1.10.1\"}\n  http-kit/http-kit   {:mvn/version \"2.3.0\"}\n  ring/ring-core      {:mvn/version \"1.8.1\"}\n  ring/ring-devel     {:mvn/version \"1.8.1\"}\n  compojure/compojure {:mvn/version \"1.6.1\"}}}\n

Add Compojure namespace to web-server namespace

(ns practicalli.web-server\n  (:gen-class)\n  (:require [ring.adapter.jetty :as http-server]\n\n            [compojure.core  :refer [defroutes GET]]\n            [compojure.route :refer [not-found]\n\n            [ring.middleware.reload :refer [wrap-reload]]]))\n

Restart the REPL (unless Compojure was added using hotload)

Define routes using compojure

(defroutes app\n  (GET \"/\"         [] handler/welcome-page)\n  (GET \"/accounts\" [] handler/accounts-overview-page)\n  (GET \"/account\"  [] handler/account-history)\n  (GET \"/transfer\" [] handler/money-transfer)\n  (GET \"/payment\"  [] handler/money-payment)\n  (GET \"/register\" [] handler/register-customer) )\n
"},{"location":"app-servers/routing-libraries/","title":"Routing libraries","text":""},{"location":"app-servers/routing-libraries/#application-logic","title":"Application Logic","text":"
  • Routing
  • Requests
  • Responses
  • handlers
  • middleware
  • Serving static content
"},{"location":"app-servers/routing-libraries/#ring","title":"Ring","text":"

Ring is the defacto library for server-side web applications. Even if not using the Ring library, the contents that Ring established are used by other libraries.

"},{"location":"app-servers/routing-libraries/#compojure","title":"Compojure","text":"

Compojure is a library that works with Ring to manage Compojure also has convenience functions that make ring responses easier to generate.

In this section we will update our project to use Compojure.

"},{"location":"app-servers/routing-libraries/#bidi-bi-directional-uri-dispatch","title":"Bidi - Bi-directional URI dispatch","text":"

https://github.com/juxt/bidi Clojure and ClojureScript

bidi is written to do 'one thing well' (URI dispatch and formation) and is intended for use with Ring middleware, HTTP servers (including Jetty, http-kit and aleph) and is fully compatible with Liberator.

"},{"location":"app-servers/routing-libraries/#yada-resources-as-data","title":"yada - resources as data","text":"

yada is a web library for Clojure, designed to support the creation of production services via HTTP.

It has the following features:

  • Standards-based, comprehensive HTTP coverage (content negotiation, conditional requests, etc.)
  • Parameter validation and coercion, automatic Swagger support
  • Rich extensibility (methods, mime-types, security and more)
  • Asynchronous, efficient interceptor-chain design built on manifold
  • Excellent performance, suitable for heavy production workloads

yada is a sibling library to bidi - whereas bidi is based on routes as data, yada is based on resources as data.

"},{"location":"app-servers/routing-libraries/#reitit","title":"Reitit","text":"

A data approach to routing

"},{"location":"app-servers/routing/","title":"Routing","text":"

Compojure Bidi Reitit

"},{"location":"app-servers/routing/#injecting-resources-using-routing","title":"Injecting resources using routing","text":"
(defroutes\n  GET /accounts/account [] (partial db/connection request))\n

And then have a handler that takes both a request and resource arguments. This makes the handler pure in respect that it does not require any external data to do its job (yes the connection is external, but the reference to the connection is provided as an argument to the side effect is abstracted away).

Middleware could also be used to wrap all the routes, however, if some routes do not use the database then this approach adds redundancy and makes the abstraction feel too high a level in the application design. This approach also makes it harder to test the handlers as normal Clojure functions, as its not possible to simply call that function with an argument.

"},{"location":"app-servers/routing/#resources","title":"Resources","text":"
  • How to manage database connections in Clojure - ClojureVerse

  • https://devcenter.heroku.com/articles/database-connection-pooling-with-clojure

  • https://stackoverflow.com/questions/19776462/passing-state-as-parameter-to-a-ring-handler
"},{"location":"app-servers/simple-restart/","title":"Simple restart approach","text":"

Use a def expression to create a named reference to the running server, providing a simple way to stop the application server.

jettyhttpkit

The app-server starts when the application starts, as the app-server-start is called from -main once the port value has been taken from either an argument to -main, an operating system $PORT environment variable or the default 8080.

(def app-server-instance (-main 8080)) is placed within a (comment ) expression. This provides a manual way for the developer to start the application server.

The app-server-instance is a symbol pointing to the server instance. This instance can be used to shut down the server.

When the developer evaluates (.stop app-server-instance), the instance is used to shut down the running application server.

The REPL itself is still running, so the application can be started again quickly by evaluating (def app-server-instance (-main 8888)).

(ns practicalli.example-webapp\n  (:gen-class)\n  (:require [ring.adapter.jetty :as jetty]\n            [compojure.core :refer [defroutes GET]]))\n\n\n;; Routing\n\n(defroutes app\n  (GET \"/\" [] {:status 200 :body \"App Server Running\"}))\n\n\n;; System\n\n  (defn app-server-start\n    [port]\n    (jetty/run-jetty #'app {:port port :join? false}))\n\n\n  (defn -main [& [port]]\n    (let [port (Integer. (or port\n                             (System/getenv \"PORT\")\n                             8888))]\n      (app-server-start port)))\n\n\n;; REPL driven development\n\n(comment\n\n  (def app-server-instance (-main 8888))\n  (.stop app-server-instance)\n)\n

The -main function identifies a value for a port and calls app-server-start function which starts the http-kit server.

(def app-server-instance (-main 8888)) is placed within a (comment ) expression. This provides a manual way for the developer to start the application server.

The app-server-instance reference can be used to stop the app-server by calling it with the arguments :timeout 100 and gracefully shutting down the server, (app-server-instance :timeout 100).

(ns practicalli.example-webapp\n  (:gen-class)\n  (:require [org.httpkit.server :as app-server]\n            [compojure.core :refer [defroutes GET]]))\n\n\n;; Routing\n\n(defroutes app\n  (GET \"/\" [] {:status 200 :body \"App Server Running\"}))\n\n\n;; System\n\n(defn app-server-start\n  \"Start the application server and run the application\"\n  [port]\n  (println \"INFO: Starting server on port: \" port)\n  (app-server/run-server #'app {:port port}))\n\n(defn -main\n  \"Start the application server on a specific port\"\n  [& [port]]\n  (let [port (Integer. (or port (System/getenv \"PORT\") 8888))]\n    (app-server-start port)))\n\n\n;; REPL driven development\n\n(comment\n\n  (def app-server-instance (-main 8888))\n  (app-server-instance :timeout 100)\n)\n

Http-kit server documentation contains details of asynchronous websockets and HTTP streaming configurations.

"},{"location":"app-servers/start-server/","title":"Start a server","text":"

The very basics of starting an Http server with Jetty or Httpkit.

Projects containing application servers are typically started from the command line, especially when deployed.

During development an application server is typically managed via the REPL, either calling a or via a component lifecycle service (mount, integrant, component)

"},{"location":"app-servers/start-server/#the-main-function","title":"The -main function","text":"

The -main function is used to capture optional arguments to set configuration such as port and ip address. These values can either be passed as function arguments, operating system environment variables or default values.

Define a -main function that optionally takes an argument that will be used as a port number to listen for requests.

A value is bound to the local name port using either the argument to the -main function, an operating system environment variable or the default port number.

(defn -main\n  \"Start the application server on a specific port\"\n  [& [port]]\n  (let [port (Integer. (or port\n                           (System/getenv \"PORT\")\n                           8080))]\n    (server-start port)))\n

The app-server-start function starts a specific application server, eg. Jetty, http-kit-server.

"},{"location":"app-servers/start-server/#listen-on-port","title":"Listen on Port","text":"

Application servers listen on a specific port for messages over HTTP/S which is set when starting the application server. Public facing application will receive requests over port 80, although for security reasons a firewall or proxy is placed in front of the application server which redirects traffic to an internal port number for the application server.

Cloud application platforms (Heroku, Google, AWS) provide a port number each time a new cloud environment (eg. container) is provisioned, so an application server should read in this dynamically assigned port number from the provided operating system environment variable.

"},{"location":"app-servers/start-server/#integer-type-port-number","title":"Integer type port number","text":"

(Integer. port-number) will cast either a string or number to a JVM integer type. (Integer/parseInt port-number) is also commonly used and has the same result.

These functions are used to ensure an Integer type value is passed to the application server. As Clojure uses Java application servers (Java app servers have decades of development) then the correct type must be passed to avoid error.

"},{"location":"app-servers/start-server/#get-environment-variables","title":"Get environment variables","text":"

A common way to get environment variables from the operating system is to use the Java method, System/getenv.

(System/getenv \"PORT\") returns the value of the `PORT` environment variable.  If the variable is not set, then nil is returned (TODO: check what is actually returned)\n

System/getProperty method will get specific values from Java .properties files, usually from a system.properties file in the root of the project. System/getProperties will get all properties found in .properties files in the project. Settings typically found in the system.properties files include version of Java

java.runtime.version=11\n

The Java Tutorials: Properties

(System/getenv) returns a hash-map of all current environment variables. Wrap in a def name to make a useful REPL tool to inspect the current environment variables available.

(comment\n  ;; Get all environment variables\n  ;; use a data inspector to view environment-variables name\n  (def environment-variables\n    (System/getenv))\n  )\n

Use tools such as the clojure inspector or data inspector tools in Clojure aware editors (e.g. CIDER inspector)

"},{"location":"app-servers/start-server/#environ-library-for-environment-variables","title":"Environ library for environment variables","text":"

Use environment variables as Clojure keywords.

weavejester/environ library will source environment settings from Leiningen and Boot configurations, the Operating System and Java system property files. The library works for Clojure and ClojureScript projects.

Include environ/environ.core library as a dependency in the Clojure project configuration

Require the library in the web-service namespace ns form

(require '[environ.core :refer [env]])\n

Then call the env function with a property name to return the value associated with that property.

(env :port)\n
"},{"location":"app-servers/static-content/","title":"Static Content","text":"

Avoid serving large or complex static content

The most efficient and secure way of serving static content from a Clojure (or any other app) is to not server content directly. Using a static web server such as nginx or apache httpd provides a separation of concerns.

Nginx and Apache Httpd provide many features for serving static content and managing mime types, etc which would have little value to implement inside a Clojure web application.

Nginx and Apache Httpd can be configured as a reverse proxy, only redirecting specific request to the Clojure application

"},{"location":"assets/images/social/","title":"Social Cards","text":"

Social Cards are visual previews of the website that are included when sending links via social media platforms.

Material for MkDocs is configured to generate beautiful social cards automatically, using the colors, fonts and logos defined in mkdocs.yml

Generated images are stored in this directory.

"},{"location":"building-api/","title":"Server-side API's","text":"

APIs are an excellent way to make a service accessible to the wider world.

Server-side apis in Clojure can be self-documenting (OpenAPI), highly efficient and a joy to develop.

The ring specification abstracts HTTP requests & responses, automatically converting to and from Clojure hash-maps which are far simpler and effecitive to work with.

Clojure routing libraries typically take either use a data configuration or provide a Domain Specific Language (DSL) for defining routes.

Practicalli recommends Reitit

There are many excellent routing libraries available for Clojure, however reitit is very well documented and takes a data centric approach.

Practicalli has found reitit very easy to work with on new project and to migrate existing project.

"},{"location":"building-api/#reitit","title":"Reitit","text":"

Reitit is a data defined routing library which can be used with the Ring specification and middleware.

Route configuration is pre-compiled, so if highly efficient. Routing is also bi-directional. Defining route configuration as data simplifies validation, with errors returned containing clojure.spec information.

  • define routes as data (validated with clojure.spec)
  • ring support
  • middleware support
  • data coercion
  • validation (clojure.spec or Malli)
  • swagger (openapi) documentation with custom templates

Reitit

"},{"location":"building-api/#compojure","title":"Compojure","text":"

compojure library provides the defroutes macro and HTTP methods (GET, POST, etc) as a DSL for defining routes.

"},{"location":"building-api/#compojure-api","title":"compojure-api","text":"

Compojure API library and template provide a quick way to create an API, by extending Compojure features.

compojure-api library

Compojure API documentation

"},{"location":"building-api/#prismatic-schema","title":"Prismatic schema","text":"

Schema validation defines the shape of any data that the API will respond with as well as any data that is sent along with a request.

"},{"location":"building-api/#self-documenting-with-swagger","title":"Self-documenting with Swagger","text":"

This template contains Swagger that documents the API's you are creating and ring-swagger constructs the documentation as you create your code.

"},{"location":"building-api/#references","title":"References","text":"
  • Getting started with Compojure API
  • ring-swagger
  • RESTful CRUD APIs Using Compojure-API and Toucan - part1
  • RESTful CRUD APIs Using Compojure-API and Toucan - part2
"},{"location":"building-api/#alternatives","title":"Alternatives","text":"
  • Yada introduction - JUXT.pro
  • Yada manual
"},{"location":"building-api/cheshire/","title":"Cheshire - fast JSON encoding","text":"

Cheshire is fast JSON encoding library with support for Date/UUID/Set/Symbol encoding and SMILE support.

  • Github repository and usage
  • API documentation
"},{"location":"building-api/compojure-api-template/","title":"compojure-api template","text":"

Quickly create the basics of a server-side webapp with the compojure-api template for Leiningen.

lein new compojure-api project-name\n

This command creates a new Clojure project in a directory called scoreboard-service.

"},{"location":"building-api/compojure-api-template/#adding-tests","title":"Adding tests","text":"

Using either of the +clojure-test or +midge will add the specific test library to the project created.

lein new compojure-api project-name +clojure-test\n
"},{"location":"building-api/create-compojure-api-project/","title":"Create a compojure-api project","text":""},{"location":"building-api/create-compojure-api-project/#notecreate-a-project-with-tests","title":"NOTE::Create a project with tests","text":"
lein new compojure-api my-api +clojure-test\n
"},{"location":"building-api/create-compojure-api-project/#deconstruct-the-project","title":"Deconstruct the project","text":"

The project this template creates is relatively simple in terms of dependencies in the project.clj file

  (defproject my-api \"0.1.0-SNAPSHOT\"\n    :description \"Experimenting with the compojure-api\"\n    :dependencies [[org.clojure/clojure \"1.8.0\"]\n                   [metosin/compojure-api \"1.1.11\"]]\n    :ring {:handler my-api.handler/app}\n    :uberjar-name \"server.jar\"\n    :profiles {:dev {:dependencies [[javax.servlet/javax.servlet-api \"3.1.0\"]\n                                   [cheshire \"5.5.0\"]\n                                   [ring/ring-mock \"0.3.0\"]]\n                    :plugins [[lein-ring \"0.12.0\"]]}})\n

Interesting things to note are its using the lein-ring plugin, so we should run the application with lein ring server.

When we want to deploy the application then we should use the lein ring uberjar command to create an uberjar (a java archive file that includes our Clojure application and the clojure.core library, so we can just run it as a java library).

"},{"location":"building-api/create-compojure-api-project/#dev-profile","title":":dev profile","text":"

In the :dev profile, dependencies include ring/ring-mock library to help us test our server-side web application.

There is also the cheshire library to help us work with JSON data in an efficient way.

"},{"location":"building-api/json-files/","title":"Working with JSON files","text":"

slurp will read files into our Clojure code.

(slurp \"spicy-vegan-pepperoni.json\")\n

We can use cheshire library to convert the JSON to a Clojure data structure.

(cheshire/parse-string\n  (slurp \"spicy-vegan-pepperoni.json\"))\n;; => {\"name\" \"Spicy Vegan Pepperoni\", \"size\" \"XL\", \"origin\" {\"country\" \"PO\", \"city\" \"Tampere\"}, \"description\" \"Healthy and delicious Vegan version of a double pepperoni pizza with some jalapenos to spice it up\"}\n

Lets pretty print the Clojure data structure to make it easier to read

(clojure.pprint/pprint\n  (cheshire/parse-string\n    (slurp \"spicy-vegan-pepperoni.json\")))\n;; => nil\n\n;; From the REPL output\n{\"name\"   \"Spicy Vegan Pepperoni\",\n \"size\"   \"XL\",\n \"origin\" {\"country\" \"PO\", \"city\" \"Tampere\"},\n \"description\"\n \"Healthy and delicious Vegan version of a double pepperoni pizza with some jalapenos to spice it up\"}\n
"},{"location":"building-api/plumatic-schema/","title":"Plumatic schema - defining the shape of data","text":"

As an API is an external system then it is important to define the shape of data coming into and leaving the application.

Plumatic schema is a simple way to define the shape of data in Clojure without having to define fixed static types.

"},{"location":"building-api/plumatic-schema/#dice-roll-result","title":"Dice roll result","text":"

In this example. our API is related to a game and we call our API to get the result of a dice roll

(s/defschema DiceRollResult\n  {:result s/Int})\n
"},{"location":"building-api/plumatic-schema/#customer","title":"Customer","text":"

Most business systems (and most systems in general) have some concept of a user or customer. Here we define a schema for a valid customer.

In this example, a valid customer lives in one of two cities, as defined using an schema enumeration (enum)

(s/defschema Customer {:id      s/Str,\n                       :name    s/Str\n                       :address {:street s/Str\n                                 :city   (s/enum :maidstone :dover)}})\n
"},{"location":"building-api/plumatic-schema/#pizza-order","title":"Pizza Order","text":"

We can make the data be as specific or as general as we need. Enumerations allow us to limit the set of valid options. If there were a lot of options then it may be useful to define them as a data structure in their own namespace.

(s/defschema Pizza\n  {:name           s/Str\n   :size           (s/enum :L :M :S)\n   :origin         {:country (s/enum :FI :PO)\n                    :city    s/Str}\n   (s/optional-key\n     :description) s/Str})\n\n(s/defschema Customer {:id      s/Str,\n                       :name    s/Str\n                       :address {:street s/Str\n                                 :city   (s/enum :maidstone :dover)}})\n
"},{"location":"building-api/plumatic-schema/#legitimate-ferry-company","title":"Legitimate Ferry Company","text":"

We can use the data we define to ensure that something is valid. For example, if a Ferry company uses this API to register themselves as a business, we can ensure we capture the number of ferries they have.

In the logic of our API we can use the number of ferries value to check if we should register this company. If it has no ferries, then we shouldn't register the company.

(s/defschema FerryCompany\n  {:name              s/Str\n   :number-of-ferries Long\n   :country           (s/enum :UK :France :Netherlands)\n   (s/optional-key\n     :description)    s/Str})\n
"},{"location":"building-api/ring-mock/","title":"ring-mock","text":"

ring-mock is a testing library for server-side applications

Ring-Mock creates Ring request maps to assist with defining tests in Clojure.

"},{"location":"building-api/ring-mock/#installation","title":"Installation","text":"

Add the following development dependency to your project.clj file:

[ring/ring-mock \"0.3.2\"]\n
"},{"location":"building-api/ring-mock/#examples","title":"Examples","text":"
(ns your-app.core-test\n  (:require [clojure.test :refer :all]\n            [your-app.core :refer :all]\n            [ring.mock.request :as mock]))\n\n(deftest your-handler-test\n  (is (= (your-handler (mock/request :get \"/doc/10\"))\n         {:status  200\n          :headers {\"content-type\" \"text/plain\"}\n          :body    \"Your expected result\"})))\n\n(deftest your-json-handler-test\n  (is (= (your-handler (-> (mock/request :post \"/api/endpoint\")\n                           (mock/json-body {:foo \"bar\"})))\n         {:status  201\n          :headers {\"content-type\" \"application/json\"}\n          :body    {:key \"your expected result\"}})))\n
"},{"location":"building-api/ring-swagger/","title":"ring-swagger","text":"

ring-swagger is a Swagger 2.0 implementation for Clojure/Ring using Plumatic Schema (support for clojure.spec via spec-tools.

  • Transforms deeply nested Schemas into Swagger JSON Schema definitions
  • Extended & symmetric JSON & String serialization & coercion
  • Middleware for handling Schemas Validation Errors & Publishing swagger-data
  • Local api validator
  • Swagger artifact generation
  • swagger.json via ring.swagger.swagger2/swagger-json
  • Swagger UI bindings. (get the UI separately as jar or from NPM)
"},{"location":"building-api/ring-swagger/#documentation","title":"Documentation","text":"
  • ring-swagger API documentation
"},{"location":"building-api/swagger/","title":"Swagger - self describing APIs","text":""},{"location":"building-api/terminology/","title":"Terminology","text":""},{"location":"building-api/terminology/#application-programming-interface-api","title":"Application Programming Interface (API)","text":"

An API defines how to use another piece of software. The API shows you the public functions and data you can use with your own software, allowing you to do more with less code.

https://en.wikipedia.org/wiki/Application_programming_interface

"},{"location":"building-api/terminology/#uniform-resource-identifier-uri","title":"Uniform Resource Identifier (URI)","text":"

A Uniform Resource Identifier (URI) is a string of characters that unambiguously identifies a particular resource. To guarantee uniformity, all URIs follow a predefined set of syntax rules,[1] but also maintain extensibility through a separately defined hierarchical naming scheme (e.g. \"http://\").

https://en.wikipedia.org/wiki/Uniform_Resource_Identifier

"},{"location":"building-api/terminology/#uniform-resource-locator-url","title":"Uniform Resource Locator (URL)","text":"

A web page and the images and videos it contains all have their own URL, a specific address where they can be found on the Internet.

A URL is a specific form of URI for web pages and the content that they contain.

https://en.wikipedia.org/wiki/URL

"},{"location":"building-api/testing-api/","title":"Testing our API","text":"

We used the clojure-test option when we created the project, so we will use this built in library.

"},{"location":"building-api/testing-api/#writing-tests","title":"Writing tests","text":"

Writing tests is just the same as other Clojure applications.

(deftest a-test\n\n  (testing \"Test GET request to /hello?name={a-name} returns expected response\"\n    (let [response (app (-> (mock/request :get  \"/api/plus?x=1&y=2\")))\n          body     (parse-body (:body response))]\n      (is (= (:status response) 200))\n      (is (= (:result body) 3)))))\n
"},{"location":"building-api/testing-api/#using-helper-functions","title":"Using helper functions","text":"

It is good practice to create helper functions to extract out common code into its onw function. This saves on duplication, reduces maintenance and should improve the readability of your tests.

Here is an example of a helper function that reads data in the form of JSON and creates a Clojure map for us to work with.

(defn parse-body [body]\n  (cheshire/parse-string (slurp body) true))\n
"},{"location":"building-api/testing-api/#hintcheshire-api","title":"HINT::Cheshire API","text":"

See the parse-string description in the Cheshire API documentation

"},{"location":"building-api/testing-api/#including-test-libraries-in-the-namespace","title":"Including test libraries in the namespace","text":"

Including the testing libraries is standard :require statements.

(ns my-api.core-test\n  (:require [cheshire.core :as cheshire]\n            [clojure.test :refer :all]\n            [my-api.handler :refer :all]\n            [ring.mock.request :as mock]))\n
"},{"location":"building-api/testing-api/#ringmock-library","title":"ring.mock library","text":"

A library to help you mock parts of your server-side application. This works just as well for APIs as web applications.

"},{"location":"building-api/testing-api/#hintwriting-files-in-clojure-with-spit","title":"HINT::Writing files in Clojure with spit","text":"

spit is a simple function that will write files.

"},{"location":"building-api/end-to-end-testing/","title":"End to end API testing","text":"

There are several tools for testing your API.

Tools Description OpenAPI (Swagger) Provides live documentation of an API and ability to run API calls curl Command line tool for talking to the web (client side) Insomnia.rest HTTP and GraphQL toolbelt for debugging APIs (client side) Postman Collaborative platform for API development

Open API should be built into any API you build as it provides living documentation of your API as you develop, as well as a way for developers to test queries against your API.

curl is the classic command line tool for testing anything on the web. Its an excellent tool for one off tests or for writing a batch of tests in a script.

Insomnia is a great tool to help you debug your API and generate code for API calls in over 30 different programming languages.

Postman is aimed more at the corporate developer or someone dealing with a large set of APIs. It requires more setup although provides more features.

"},{"location":"building-api/end-to-end-testing/curl/","title":"curl","text":""},{"location":"building-api/end-to-end-testing/curl/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":"

Pull requests are welcome

"},{"location":"building-api/end-to-end-testing/httpie/","title":"HTTPie","text":""},{"location":"building-api/end-to-end-testing/postman/","title":"Postman","text":""},{"location":"building-api/end-to-end-testing/postman/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":"

Pull requests are welcome

"},{"location":"building-api/end-to-end-testing/swagger/","title":"Swagger","text":""},{"location":"building-api/end-to-end-testing/swagger/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":"

Pull requests are welcome

"},{"location":"building-api/projects/game-scoreboard/","title":"Create a new project with compojure-api template","text":"

You can quickly create the basics of a server-side webapp with the compojure-api template for Leiningen.

lein new compojure-api game-scoreboard +clojure-test\n

This command creates a new Clojure project in a directory called game-scoreboard.

"},{"location":"building-api/projects/game-scoreboard/#update-clojure-version","title":"Update Clojure version","text":"

Edit the project.clj file and update the org.clojure/clojure dependency to 1.10.0

"},{"location":"building-api/projects/game-scoreboard/defining-scoreboard/","title":"Defining Scores and a Scoreboard","text":"

We use the Plumatic schema to define what a score looks like, as well as what the overall scoreboard looks like.

"},{"location":"building-api/projects/game-scoreboard/defining-scoreboard/#scores","title":"Scores","text":"

A score is an whole number (Integer) that represents the score achieved for a particular game

(schema/defschema Score\n  {:player-id   schema/Uuid\n   :score       schema/Int\n\n   (schema/optional-key\n     :gravitar) schema/Str})\n
"},{"location":"building-api/projects/game-scoreboard/defining-scoreboard/#leaderboard","title":"Leaderboard","text":"

The Leader board is a collection of scores for a game. The scoreboard is ordered by highest value by default.

"},{"location":"building-api/projects/game-scoreboard/defining-scoreboard/#player","title":"Player","text":""},{"location":"building-api/projects/game-scoreboard/defining-scoreboard/#player-accounts","title":"Player accounts","text":""},{"location":"building-api/projects/game-scoreboard/defining-scores/","title":"Defining Scores","text":""},{"location":"building-api/projects/game-scoreboard/defining-scores/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"building-api/projects/game-scoreboard-ui/","title":"Game Scoreboard UI","text":"

Create a Web user interface for the Scoreboard so we can test out the API and do something interesting with the results.

  • Create a project using figwheel-main
"},{"location":"building-api/projects/game-scoreboard-ui/create-project/","title":"Create new project using figwheel-main","text":"

Create a new project using figwheel-main, the newest version of figwheel.

Include the reagent library to make the project a single page app in the style of react.js.

lein new figwheel-main game-scoreboard-ui -- --reagent\n

Change into the 'game-scoreboard-ui' directory and run 'lein fig:build'

cd game-scoreboard-ui\nlein fig:build\n

Your default browser will open at localhost:9500

"},{"location":"building-api/reitit/","title":"Reitit routing library","text":"

Reitit routing for client and server-side routing

Supports use of ring specification and middleware

Provides content negotiation and data validation.

"},{"location":"building-api/reitit/#library-dependency","title":"Library Dependency","text":"

Include the bundled distribution containing all modules

Clojure CLILeiningen deps.edn
metosin/reitit {:mvn/version \"0.7.0-alpha5\"}\n
[metosin/reitit \"0.7.0-alpha5\"]\n
"},{"location":"building-api/reitit/#require-namespace","title":"Require namespace","text":"

Require the reitit.ring namespace to provide routing and ring specification support.

ProjectREPL
(:require \n [reitit.ring :as ring])\n
(require '[reitit.ring :as ring])\n
"},{"location":"building-api/reitit/#router-function","title":"Router function","text":"

Takes all requests and delegates them to handler functions

Routes are defined as a vector of vectors structure, with each nested vector containing a string key defining unique paths that match a specific request.

Each key has a configuration map to define a handler for a specific HTTP method (:get :ost etc.)

(def router\n  (ring/ring-handler\n   (ring/router\n    [[\"/\" {:get welcome}]]\n    [\"/status\" {:get status}])))\n

Passing system configuration argument

Component systems (donut, integrant) use a system configuration (hash-map) which is passed to the router component during start up.

Define the router as a function to recieve the system configuration as an argument and make it available in part or full to the handler functions.

(defn app\n  [system-config]\n  (ring/ring-handler\n   (ring/router\n    [[\"/api\"\n      [\"/v1\"\n       (scoreboard/routes system-config)]]]\n\n    ;; - middleware, coersion & content negotiation\n    router-configuration)))\n
"},{"location":"building-api/reitit/#handler-functions","title":"Handler functions","text":"

Handler functions are Clojure functions that take a request map.

(defn welcome [_]\n  {:status 200 :body \"Awesome Reitit Ring\"})\n

constantly function is commonly used for handlers that do not use the request data initially. constantly returns an anonymous function that takes the ring request hash-map.

(def status\n  (constantly \n    (ring.util.response/response \n     {:application \"practicalli awesome-api Service\" :status \"Alive\"})))\n

Use _ argument name when request not used

Handlers might not use the data in a request to return a response. By convention the _ character is used for the argument name when the request data is not used.

System Status handler

An example status report handler namespace

(ns practicalli.awesome-api.api.system-admin\n  \"Gameboard API system administration handlers\"\n  (:require [ring.util.response :refer [response]]))\n\n(def status\n  \"Service status report for external monitoring services, e.g. Statuscake\n  Return:\n  - `constantly` returns an anonymous function that returns a ring response hash-map\"\n  (constantly (response {:application \"practicalli awesome-api Service\" :status \"Alive\"})))\n

"},{"location":"clojure-databases/","title":"Clojure Databases","text":"Database Description Juxt Crux Bi-temporal schema-less high performance CQRS style database Onyx Cognitect Datomic (commercial product)"},{"location":"clojure-databases/crux/","title":"Crux - bi-temporal schema-less document database","text":"

Crux is a general purpose database with graph-oriented bi-temporal indexes. Datalog, SQL & EQL queries are supported along with Java, HTTP & Clojure APIs. The Datalog query interface that can be used to express complex joins and recursive graph traversals.

"},{"location":"clojure-databases/crux/#getting-started","title":"Getting Started","text":"

Follow the Crux Earth Assignment Tutorial, in either the self-contained Next-Journal environment or as your own Clojure project.

{% tabs clojure=\"Clojure CLI tools\", lein=\"Leiningen\" %}

{% content \"clojure\" %} Using the Clojure CLI tools and practicalli/clojure-deps-edn configuration, create a new project:

clojure -X:project/new :template app :name practicalli/crux-demo\n

{% content \"lein\" %} Using the Leiningen build tool, create a new project:

lein new app practicalli/crux-demo\n

{% endtabs %}

Install Crux as a library in a Clojure project or use the pre-built docker image.

Note: to have more than one set of tabs in a page, simply create unique id's for the tabs, e.g. practicalli2

Experiment with the Crux-labs workshop project, which contains examples of using Crux.

"},{"location":"clojure-databases/crux/#resources","title":"Resources","text":"
  • Library dependency (clojars)
  • Reference Documentation
  • Community discussions (Zulip)
  • GitHub discussions

{% youtube %} https://www.youtube.com/watch?v=JkZfQZGLPTA

"},{"location":"clojure-databases/crux/#unbundled-architectural-overview","title":"Unbundled architectural overview","text":"

Crux follows an unbundled architectural, decoupled components communicating via an immutable log and document store. crux-rocksdb is the high performance default data store, with a range of storage options available for embedded usage and cloud scaling.

Crux embraces the transaction log as the central point of coordination when running as a distributed system. Use of a separate document store enables simple eviction of active and historical data to assist with technical compliance for information privacy regulations.

This design makes it feasible and desirable to embed Crux nodes directly within your application processes, which reduces deployment complexity and eliminates round-trip overheads when running complex application queries.

"},{"location":"full-app/","title":"Building a full database backed app","text":""},{"location":"introduction/contributing/","title":"Contributing to Practicalli","text":"

Practicalli books are written in markdown and use MkDocs to generate the published website via a GitHub workflow. MkDocs can also run a local server using the make docs target from the Makefile

By submitting content ideas and corrections you are agreeing they can be used in this book under the Creative Commons Attribution ShareAlike 4.0 International license. Attribution will be detailed via GitHub contributors.

All content and interaction with any persons or systems must be done so with respect and within the Practicalli Code of Conduct.

"},{"location":"introduction/contributing/#book-status","title":"Book status","text":""},{"location":"introduction/contributing/#submit-and-issue-or-idea","title":"Submit and issue or idea","text":"

If something doesnt seem quite right or something is missing from the book, please raise an issue via the GitHub repository explaining in as much detail as you can.

Raising an issue before creating a pull request will save you and the maintainer time.

"},{"location":"introduction/contributing/#considering-a-pull-request","title":"Considering a Pull request?","text":"

Before investing any time in a pull request, please raise an issue explaining the situation. This can save you and the maintainer time and avoid rejected pull requests.

Please keep pull requests small and focused, as they are much quicker to review and easier to accept. Ideally PR's should be for a specific page or at most a section.

A PR with a list of changes across different sections will not be merged, it will be reviewed eventually though.

"},{"location":"introduction/contributing/#thank-you-to-everyone-that-has-contributed","title":"Thank you to everyone that has contributed","text":"

A huge thank you to Rich Hickey and the team at Cognitect for creating and continually guiding the Clojure language. Special thank you to Alex Miller who has provided excellent advice on working with Clojure and the CLI tooling.

The Clojure community has been highly supportive of everyone using Clojure and I'd like to thank everyone for the feedback and contributions. I would also like to thank everyone that has joined in with the London Clojurins community, ClojureBridgeLondon, Clojurians Slack community, Clojurians Zulip community and Clojureverse community.

Thank you to everyone who sponsors the Practicalli websites and videos and for the Clojurists Together sponsorship, it helps me continue the work at a much faster pace.

Special thanks to Bruce Durling for getting me into Cloure in the first place.

"},{"location":"introduction/overview/","title":"Overview of Clojure Web Services","text":"

A Web service receives data, does something with that data and returns data as a result. This is the essence of how a function works in Clojure. So its really simple to design web apps with Clojure.

Clojure data is managed via immutable data structures. The majority of the Clojure code will be stateless or managing state with immutable data structures, therefore code for services will be less complex and less prone to conflicts.

Relevant changes to data is persisted to a data store, e.g. PostgreSQL, Datomic or XTDB

So reliable services are very easy to build and simple to scale via parallelism.

"},{"location":"introduction/overview/#libraries-over-frameworks","title":"Libraries over frameworks","text":"

Clojure takes a modular approach to building services, assembling commonly used libraries from the community.

Java Interoperability is simple in Clojure, so its trivial to use Java libraries or any libraries that run on the JVM (Scala, Jython, etc.).

Web services in Clojure are typically built from a collection of highly focused libraries. Each library has a specific focus and enables a modular approach, as you can swap components & libraries easily should there be value in a different approach.

Common libraries for web app development include:

  • Ring - a web application library
  • Compojure - an simple way to define routes for your ring webapp
  • Hiccup or Selmer- to generate HTML from Clojure data structures
  • Pedestal
  • jdbc.next - a modern low-level Clojure wrapper for JDBC access to databases
  • clojure.java.jdbc - simple low level SQL library
  • Korma, YesQL, hugSql - database abstraction layers
  • Prismatic Schema - database schema mapping
  • Migratus - database migrations (and all the things)

Large frameworks constrain the design of a service, forcing development to live inside the constraints of that framework as it can be difficult to break out of the design the framework imposes.

"},{"location":"introduction/overview/#project-templates","title":"Project templates","text":"

Projects can be created from templates to avoid starting from scratch each time.

deps-newclj-new

deps-new creates Clojure CLI projects from templates and is very simple to create your own templates

clojure -T:project/create :template app :name practicalli/gameboard\n

clj-new supports a very wide range of templates although has a more involved design when it comes to creating your own templates. Maintained templates used by clj-new should support both Clojure CLI and Leiningen

clojure -T:project/new :template luminus :name practicalli/gameboard\n

Convert Leiningen project to Clojure CLI

Add a deps.edn file that contains a {} hash-map with a :deps key that is associated with a hash-map of library dependencies, the same dependencies from the project.clj configuration file updated to the Clojure CLI format, e.g. {org.clojure/clojure {:mvn/version \"1.11.3\"}}

There are many great templates to try that provide insight into building webapps in Clojure.

  • compojure - a common web application approach with ring and compojure
  • compojure-api - quickly build API's with ring, compojure and openapi (swagger) for self-documentation
  • luminus - a flexible template to create server-side and full stack web applications
  • pedestal-service - an opinionated, extensible & scalable framework
  • duct - data-oriented production-grade server-side web applications
  • JUXT Edge - a curated base project to build your own applications and services

You can find a range of project templates by searching for lein-template on Clojars.org. There is also a guide to writing templates on Leiningen.org

"},{"location":"introduction/repl-workflow/","title":"REPL Driven Development","text":"

Always be REPL'ing

Coding without a REPL feels limiting. The REPL provides fast feedback from code as its crafted, testing assumptions and design choices every step of the journey to a solution - John Stevenson, Practical.li

Clojure is a powerful, fun and highly productive language for developing applications and services. The clear language design is supported by a powerful development environment known as the REPL (read, evaluate, print, loop). The REPL gives you instant feedback on what your code does and enables you to test either a single expression or run the whole application (including tests).

REPL driven development is the foundation of working with Clojure effectively

An effective Clojure workflow begins by running a REPL process. Clojure expressions are written and evaluated immediately to provide instant feedback. The REPL feedback helps test the assumptions that are driving the design choices.

  • Read - code is read by the Clojure reader, passing any macros to the macro reader which converts those macros into Clojure code.
  • Evaluate - code is compiled into the host language (e.g. Java bytecode) and executed
  • Print - results of the code are displayed, either in the REPL or as part of the application.
  • Loop - the REPL is a continuous process that evaluates code, either a single expression or the whole application.

Design decisions and valuable data from REPL experiments can be codified as specifications and unit tests

Practicalli REPL Reloaded Workflow

The principles of REPL driven development are implemented in practice using the Practicalli REPL Reloaded Workflow and supporting tooling. This workflow uses Portal to inspect all evaluation results and log events, hot-load libraries into the running REPL process and reloads namespaces to support major refactor changes.

"},{"location":"introduction/repl-workflow/#evaluating-source-code","title":"Evaluating source code","text":"

A REPL connected editor is the primary tool for evaluating Clojure code from source code files, displaying the results inline.

Source code is automatically evaluated in its respective namespace, removing the need to change namespaces in the REPL with (in-ns) or use fully qualified names to call functions.

Evaluate Clojure in a Terminal UI REPL

Entering expressions at the REPL prompt evaluates the expression immediately, returning the result directly underneath

"},{"location":"introduction/repl-workflow/#rich-comment-blocks-living-documentation","title":"Rich Comment blocks - living documentation","text":"

The (comment ,,,) function wraps code that is only run directly by the developer using a Clojure aware editor.

Expressions in rich comment blocks can represent how to use the functions that make up the namespace API. For example, starting/restarting the system, updating the database, etc. Expressions provide examples of calling functions with typical arguments and make a project more accessible and easier to work with.

Clojure Rich Comment to manage a service

(ns practicalli.gameboard.service)\n\n(defn app-server-start [port] ,,,)\n(defn app-server-start [] ,,,)\n(defn app-server-restart [] ,,,)\n\n(defn -main\n  \"Start the service using system components\"\n  [& options] ,,,)\n\n(comment\n  (-main)\n  (app-server-start 8888)\n  (app-server-stop)\n  (app-server-restart 8888)\n\n  (System/getenv \"PORT\")\n  (def environment (System/getenv))\n  (def system-properties (System/getProperties))\n  ) ; End of rich comment block\n

Rich comment blocks are very useful for rapidly iterating over different design decisions by including the same function but with different implementations. Hide clj-kondo linter warnings for redefined vars (def, defn) when using this approach.

;; Rich comment block with redefined vars ignored\n#_{:clj-kondo/ignore [:redefined-var]}\n(comment\n  (defn value-added-tax []\n    ;; algorithm design - first idea)\n\n  (defn value-added-tax []\n    ;; algorithm design - second idea)\n\n  ) ;; End of rich comment block\n

The \"Rich\" in the name is an honourary mention to Rich Hickey, the author and benevolent dictator of Clojure design.

"},{"location":"introduction/repl-workflow/#design-journal","title":"Design Journal","text":"

A journal of design decisions makes the code easier to understand and maintain. Code examples of design decisions and alternative design discussions are captured, reducing the time spent revisiting those discussions.

Journals simplify the developer on-boarding processes as the journey through design decisions are already documented.

A Design Journal is usually created in a separate namespace, although it may start as a rich comment at the bottom of a namespace.

A journal should cover the following aspects

  • Relevant expressions use to test assumptions about design options.
  • Examples of design choices not taken and discussions why (saves repeating the same design discussions)
  • Expressions that can be evaluated to explain how a function or parts of a function work

The design journal can be used to create meaningful documentation for the project very easily and should prevent time spent on repeating the same conversations.

Example design journal

Design journal for TicTacToe game using Reagent, ClojureScript and Scalable Vector Graphics

"},{"location":"introduction/repl-workflow/#viewing-data-structures","title":"Viewing data structures","text":"

Pretty print shows the structure of results from function calls in a human-friendly form, making it easier for a developer to parse and more likely to notice incorrect results.

Tools to view and navigate code

  • Cider inspector is an effective way to navigate nested data and page through large data sets.
  • Portal Inspector to visualise many kinds of data in many different forms.

"},{"location":"introduction/repl-workflow/#code-style-and-idiomatic-clojure","title":"Code Style and idiomatic Clojure","text":"

Clojure aware editors should automatically apply formatting that follows the Clojure Style guide.

Live linting with clj-kondo suggests common idioms and highlights a wide range of syntax errors as code is written, minimizing bugs and therefore speeding up the development process.

Clojure LSP is build on top of clj-kondo

Clojure LSP uses clj-kondo static analysis to provide a standard set of development tools (format, refactor, auto-complete, syntax highlighting, syntax & idiom warnings, code navigation, etc).

Clojure LSP can be used with any Clojure aware editor that provides an LSP client, e.g. Spacemacs, Doom Emacs, Neovim, VSCode.

Clojure Style Guide

The Clojure Style guide provides examples of common formatting approaches, although the development team should decide which of these to adopt. Emacs clojure-mode will automatically format code and so will Clojure LSP (via cljfmt). These tools are configurable and should be tailored to the teams standard.

"},{"location":"introduction/repl-workflow/#data-and-function-specifications","title":"Data and Function specifications","text":"

Clojure spec is used to define a contract on incoming and outgoing data, to ensure it is of the correct form.

As data structures are identified in REPL experiments, create data specification to validate the keys and value types of that data.

;; ---------------------------------------------------\n;; Address specifications\n(spec/def ::house-number string?)\n(spec/def ::street string?)\n(spec/def ::postal-code string?)\n(spec/def ::city string?)\n(spec/def ::country string?)\n(spec/def ::additional string?)\n\n(spec/def ::address   ; Composite data specification\n  (spec/keys\n   :req-un [::street ::postal-code ::city ::country]\n   :opt-un [::house-number ::additional]))\n;; ---------------------------------------------------\n

As the public API is designed, specifications for each functions arguments are added to validate the correct data is used when calling those functions.

Generative testing provides a far greater scope of test values used incorporated into unit tests. Data uses clojure.spec to randomly generate data for testing on each test run.

"},{"location":"introduction/repl-workflow/#test-driven-development-and-repl-driven-development","title":"Test Driven Development and REPL Driven Development","text":"

Test Driven Development (TDD) and REPL Driven Development (RDD) complement each other as they both encourage incremental changes and continuous feedback.

Test Driven Development fits well with Hammock Time, as good design comes from deep thought

  • RDD enables rapid design experiments so different approaches can easily and quickly be evaluated .
  • TDD focuses the results of the REPL experiments into design decisions, codified as unit tests. These tests guide the correctness of specific implementations and provide critical feedback when changes break that design.

Unit tests should support the public API of each namespace in a project to help prevent regressions in the code. Its far more efficient in terms of thinking time to define unit tests as the design starts to stabilize than as an after thought.

clojure.test library is part of the Clojure standard library that provides a simple way to start writing unit tests.

Clojure spec can also be used for generative testing, providing far greater scope in values used when running unit tests. Specifications can be defined for values and functions.

Clojure has a number of test runners available. Kaocha is a test runner that will run unit tests and function specification checks.

Automate local test runner

Use kaocha test runner in watch mode to run tests and specification check automatically (when changes are saved)

clojure -X:test/watch\n

"},{"location":"introduction/repl-workflow/#continuous-integration-and-deployment","title":"Continuous Integration and Deployment","text":"

Add a continuous integration service to run tests and builds code on every shared commit. Spin up testable review deployments when commits pushed to a pull request branch, before pushing commits to the main deployment branch, creating an effective pipeline to gain further feedback.

  • CircleCI provides a simple to use service that supports Clojure projects.
  • GitHub Workflows and GitHub actions marketplace to quickly build a tailored continuous integration service, e.g. Setup Clojure GitHub Action.
  • GitLab CI

"},{"location":"introduction/repl-workflow/#live-coding-with-data-stuart-halloway","title":"Live Coding with Data - Stuart Halloway","text":"

There are few novel features of programming languages, but each combination has different properties. The combination of dynamic, hosted, functional and extended Lisp in Clojure gives developers the tools for making effective programs. The ways in which Clojure's unique combination of features can yield a highly effective development process.

Over more than a decade we have developed an effective approach to writing code in Clojure whose power comes from composing many of its key features. As different as Clojure programs are from e.g. Java programs, so to can and should be the development experience. You are not in Kansas anymore!

This talk presents a demonstration of the leverage you can get when writing programs in Clojure, with examples, based on my experiences as a core developer of Clojure and Datomic.

"},{"location":"introduction/requirements/","title":"Requirements","text":"

Practicalli provides an install guide for Clojure and a wide selection of Clojure aware editors

Recommended development tools for this guide are:

  • Java OpenJDK version 17
  • Clojure CLI and Practicalli Clojure CLI Config
  • A Clojure aware editor

Code examples can be used with any Clojure build tool, although this guide focuses on using Clojure CLI tools. Some examples use Leiningen and will be updated to Clojure CLI, although the Clojure code will be the same.

"},{"location":"introduction/requirements/#additional-development-tools","title":"Additional Development tools","text":"

To complete all the steps in this guide, especially around deployment tasks, additional development tools and services are required.

Development Tools Version Test (command line) Git client latest git --version Docker Desktop latest docker --version Postgres database latest

GitHub and GitHub actions will be predominantly used in this guide, although more use of CircleCI and GitLab will also be introduced. CircleCI is a developer focused service for Continuous Integration, developed with Clojure, providing obs that package up common workflows such as deploying to specific Cloud services.

Continuous Integration Services GitHub Actions GitLab CI CircleCI Heroku deployment to be deprecated

Heroku has been used to simplify deployment directly from source code using existing build packs. Heroku now requires a commercial license for deployment so this content is to be deprecated.

"},{"location":"introduction/requirements/#persistence-alternatives","title":"Persistence Alternatives","text":"

Practicalli is considering other persistent storage approaches for this guide and any contributions in this regard is much appreciated

  • Crux - an open source document database with bitemporal graph queries
  • Datomic - a commercial transactional database with a flexible data model, elastic scaling, and rich queries.
  • Amazon Aurora - MySQL and PostgreSQL compatible cloud native relational database
  • Amazon DynamoDB with Clojure Faraday library - for persisting JSON like data structures
"},{"location":"introduction/requirements/#leiningen-approach-to-be-archived","title":"Leiningen approach (to be archived)","text":"

Install Leiningen for the Leiningen Todo App project and test the Leiningen install by running the command lein version in a terminal application.

"},{"location":"introduction/writing-tips/","title":"Writing tips for MkDocs","text":"

Making the docs more engaging using the mkdocs-material theme reference guide

Configuring Colors

Material for MkDocs - Changing the colors lists the primary and accent colors available.

HSL Color Picker for codes to modify the theme style, overriding colors in docs/assets/stylesheets/extra.css

"},{"location":"introduction/writing-tips/#hypertext-links","title":"Hypertext links","text":"

Links open in the same browser window/tab by default.

Add {target=_blank} to the end of a link to configure opening in a new tab

[link text](url){target=_blank}\n
"},{"location":"introduction/writing-tips/#buttons","title":"Buttons","text":"

Convert any link into a button by adding {.md-button} class names to end of the markdown for a link, which uses .md-button-primary by default. Include target=_blank for buttons with links to external sites.

[link text](http://practical.li/blog){.md-button target=_blank}\n

Or specify a different class

[link text](http://practical.li/blog){.md-button .md-button-primary}\n

Add an icon to the button

Practicalli Issues Practicalli Blog

[:fontawesome-brands-github: Practicalli Issues](http://practical.li/blog){ .md-button .md-button-primary }\n[:octicons-heart-fill-24: Practicalli Blog](http://practical.li/blog){ .md-button .md-button-primary }\n

Search all supported icons

"},{"location":"introduction/writing-tips/#youtube-video","title":"YouTube video","text":"

Use an iframe element to include a YouTube video, wrapping in a paragraph tag with center alignment to place the video in a centered horizontal position

<p style=\"text-align:center\">\n<iframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/embed/rQ802kSaip4\" title=\"YouTube video player\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe>\n</p>\n

mkdocs material does not have direct support for adding a YouTube video via markdown.

"},{"location":"introduction/writing-tips/#admonitions","title":"Admonitions","text":"

Supported admonition types

Note

Use !!! followed by NOTE

Adding a title

Use !!! followed by NOTE and a \"title in double quotes\"

Shh, no title bar just the text... Use !!! followed by NOTE and a \"\" empty double quotes

Abstract

Use !!! followed by ABSTRACT

Info

Use !!! followed by INFO

Tip

Use !!! followed by TIP

Success

Use !!! followed by SUCCESS

Question

Use !!! followed by QUESTION

Warning

Use !!! followed by WARNING

Failure

Use !!! followed by FAILURE

Danger

Use !!! followed by DANGER

Bug

Use !!! followed by BUG

Example

Use !!! followed by EXAMPLE

Quote

Use !!! followed by QUOTE

"},{"location":"introduction/writing-tips/#collapsing-admonitions","title":"Collapsing admonitions","text":"Note

Collapse those admonitions using ??? instead of !!!

Replace with a title

Use ??? followed by NOTE and a \"title in double quotes\"

Expanded by default

Use ???+, note the + character, followed by NOTE and a \"title in double quotes\"

"},{"location":"introduction/writing-tips/#inline-blocks","title":"Inline blocks","text":"

Inline blocks of text to make a very specific callout within text

Info

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.

Adding something to then end of text is probably my favourite

Info

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.

"},{"location":"introduction/writing-tips/#code-blocks","title":"Code blocks","text":"

Code blocks include a copy icon automatically

Syntax highlighting in code blocks

(defn my-function  ; Write a simple function\n  \"With a lovely doc-string\"\n  [arguments]\n  (map inc [1 2 3]))\n

Give the code block a title using title=\"\" after the backtics and language name

src/practicalli/gameboard.clj
(defn my-function\n  \"With a lovely doc-string\"\n  [arguments]\n  (map inc [1 2 3]))\n

We all like line numbers, especially when you can set the starting line

src/practicalli/gameboard.clj
(defn my-function\n  \"With a lovely doc-string\"\n  [arguments]\n  (map inc [1 2 3]))\n

Add linenums=42 to start line numbers from 42 onward

clojure linenums=\"42\" title=\"src/practicalli/gameboard.clj\"\n
"},{"location":"introduction/writing-tips/#annotations","title":"Annotations","text":"

Annotations in a code block help to highlight important aspects. Use the comment character for the language followed by a space and a number in brackets

For example, in a shell code block, use # (1) where 1 is the number of the annotation

Use a number after the code block to add the text for the annotation, e.g. 1.. Ensure there is a space between the code block and the annotation text.

ls -la $HOME/Downloads  # (1)\n
  1. I'm a code annotation! I can contain code, formatted text, images, ... basically anything that can be written in Markdown.

Code blocks with annotation, add ! after the annotation number to suppress the # character

(defn helper-function\n  \"Doc-string with description of function purpose\" ; (1)!\n  [data]\n  (merge {:fish 1} data)\n  )\n
  1. Always include a doc-string in every function to describe the purpose of that function, identifying why it was added and what its value is.

GitHub action example with multiple annotations

name: ci # (1)!\non:\n  push:\n    branches:\n      - master # (2)!\n      - main\npermissions:\n  contents: write\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-python@v4\n        with:\n          python-version: 3.x\n      - run: pip install mkdocs-material # (3)!\n      - run: mkdocs gh-deploy --force\n
  1. You can change the name to your liking.

  2. At some point, GitHub renamed master to main. If your default branch is named master, you can safely remove main, vice versa.

  3. This is the place to install further [MkDocs plugins] or Markdown extensions with pip to be used during the build:

    pip install \\\n  mkdocs-material \\\n  mkdocs-awesome-pages-plugin \\\n  ...\n
"},{"location":"introduction/writing-tips/#highlight-lines-in-code-blocks","title":"Highlight lines in code blocks","text":"

Add highlight line meta data to a code block after the opening backticks and code block language.

hl_lines=\"2\" highlights line 2 in the codeblock

(defn my-function\n  \"With a lovely doc-string\"\n  [arguments]\n  (map\n   inc\n   [1 2 3]))\n
"},{"location":"introduction/writing-tips/#embed-external-files","title":"Embed external files","text":"

--8<-- in a code block inserts code from a source code file or other text file

Specify a local file from the root of the book project (the directory containing mkdocs.yml)

Scheduled Version Check GitHub Workflow from source code file scheduled version check
\n
Practicalli Project Templates Emacs project configuration - .dir-locals.el
((clojure-mode . ((cider-preferred-build-tool . clojure-cli)\n                  (cider-clojure-cli-aliases . \":test/env:dev/reloaded\"))))\n

Code example reuse

Use an embedded local or external file (URL) when the same content is required in more than one place in the book.

An effective way of sharing code and configuration mutliple times in a book or across multiple books.

"},{"location":"introduction/writing-tips/#content-tabs","title":"Content tabs","text":"

Create in page tabs that can also be

Setting up a project

Clojure CLILeiningen
clojure -T:project/new :template app :name practicalli/gameboard\n
lein new app practicalli/gameboard\n

Or nest the content tabs in an admonition

Run a terminal REPL

Clojure CLILeiningen
clojure -T:repl/rebel\n
lein repl\n
"},{"location":"introduction/writing-tips/#diagrams","title":"Diagrams","text":"

Neat flow diagrams

Diagrams - Material for MkDocs

graph LR\n  A[Start] --> B{Error?};\n  B -->|Yes| C[Hmm...];\n  C --> D[Debug];\n  D --> B;\n  B ---->|No| E[Yay!];

UML Sequence Diagrams

sequenceDiagram\n  Alice->>John: Hello John, how are you?\n  loop Healthcheck\n      John->>John: Fight against hypochondria\n  end\n  Note right of John: Rational thoughts!\n  John-->>Alice: Great!\n  John->>Bob: How about you?\n  Bob-->>John: Jolly good!

state transition diagrams

stateDiagram-v2\n  state fork_state <<fork>>\n    [*] --> fork_state\n    fork_state --> State2\n    fork_state --> State3\n\n    state join_state <<join>>\n    State2 --> join_state\n    State3 --> join_state\n    join_state --> State4\n    State4 --> [*]

Class diagrams - not needed for Clojure

Entity relationship diagrams are handy though

erDiagram\n  CUSTOMER ||--o{ ORDER : places\n  ORDER ||--|{ LINE-ITEM : contains\n  LINE-ITEM {\n    customer-name string\n    unit-price int\n  }\n  CUSTOMER }|..|{ DELIVERY-ADDRESS : uses
"},{"location":"introduction/writing-tips/#keyboard-keys","title":"Keyboard keys","text":"

Represent key bindings with Keyboard keys. Each number and alphabet character has their own key.

  • 1 ++1++ for numbers
  • l ++\"l\"++ for lowercase character
  • U ++u++ for uppercase character or ++\"U\"++ for consistency

Punctionation keys use their name

  • Space ++spc++
  • , ++comma++
  • Left ++arrow-left++

For key sequences, place a space between each keyboard character

  • Space g s ++spc++ ++\"g\"++ ++\"s\"++

For key combinations, use join they key identifies with a +

  • Meta+X ++meta+x++
  • Ctrl+Alt+Del ++ctrl+alt+del++

MkDocs keyboard keys reference

"},{"location":"introduction/writing-tips/#images","title":"Images","text":"

Markdown images can be appended with material tags to set the size of the image, whether to appear on light or dark theme and support lazy image loading in browsers

SizeLazy LoadingAlignTheme SpecificAll Image Attributes

{style=\"height:150px;width:150px\"} specifies the image size

![Kitty Logo](https://raw.githubusercontent.com/practicalli/graphic-design/live/icons/kitty-light.png#only-dark){style=\"height:150px;width:150px\"}\n

{loading=lazy} specifies an image should lazily load in the browser

![Kitty Logo](https://raw.githubusercontent.com/practicalli/graphic-design/live/icons/kitty-light.png){loading=lazy}\n

{aligh=left} or {aligh=right} specifies the page alignment of an image.

![Kitty Logo](https://raw.githubusercontent.com/practicalli/graphic-design/live/icons/kitty-light.png#only-dark){align=right}\n![Kitty Logo](https://raw.githubusercontent.com/practicalli/graphic-design/live/icons/kitty-dark.png#only-light){align=right}\n

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.

![Kitty Logo](image/kitty-light.png#only-dark) or ![Kitty Logo](image/kitty-light.png#only-light) specifies the theme the image should be shown, allowing different versions of images to be shown based on the theme.

![Kitty Logo](https://raw.githubusercontent.com/practicalli/graphic-design/live/icons/kitty-light.png#only-dark){style=\"height:150px;width:150px\"}\n![Kitty Logo](https://raw.githubusercontent.com/practicalli/graphic-design/live/icons/kitty-dark.png#only-light){style=\"height:150px;width:150px\"}\n
Use the theme toggle in the top nav bar to see the icon change between light and dark.

Requires the color pallet toggle

Alight right, lazy load and set image to 150x150

![Kitty Logo](https://raw.githubusercontent.com/practicalli/graphic-design/live/icons/kitty-light.png#only-dark){align=right loading=lazy style=\"height:64px;width:64px\"}\n![Kitty Logo](https://raw.githubusercontent.com/practicalli/graphic-design/live/icons/kitty-dark.png#only-light){align=right loading=lazy style=\"height:64px;width:64px\"}\n

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor massa, nec semper lorem quam in massa.

"},{"location":"introduction/writing-tips/#lists","title":"Lists","text":"

Task lists

  • Lorem ipsum dolor sit amet, consectetur adipiscing elit
  • Vestibulum convallis sit amet nisi a tincidunt
    • In hac habitasse platea dictumst
    • In scelerisque nibh non dolor mollis congue sed et metus
    • Praesent sed risus massa
  • Aenean pretium efficitur erat, donec pharetra, ligula non scelerisque

Task List example

- [x] Lorem ipsum dolor sit amet, consectetur adipiscing elit\n- [ ] Vestibulum convallis sit amet nisi a tincidunt\n    * [x] In hac habitasse platea dictumst\n    * [x] In scelerisque nibh non dolor mollis congue sed et metus\n    * [ ] Praesent sed risus massa\n- [ ] Aenean pretium efficitur erat, donec pharetra, ligula non scelerisque\n
"},{"location":"introduction/writing-tips/#tooltips","title":"Tooltips","text":"

The humble tool tip

Hover me

with references

Hover me

Icon tool tip with a title

"},{"location":"introduction/writing-tips/#abreviations","title":"Abreviations","text":"

The HTML specification is maintained by the W3C.

[HTML]: Hyper Text Markup Language [W3C]: World Wide Web Consortium

"},{"location":"introduction/writing-tips/#magic-links","title":"Magic links","text":"

MagicLink can auto-link HTML, FTP, and email links. It can auto-convert repository links (GitHub, GitLab, and Bitbucket) and display them in a more concise, shorthand format.

Email Practicalli

Practicalli Neovim

"},{"location":"libraries/reitit/","title":"Reitit - fast data driven routing for Clojure and ClojureScript","text":""},{"location":"libraries/reitit/constructing-routes/","title":"Reitit: Constructing routes","text":"

Create a simple Clojure project

clojure -T:project/new :template app :name practicalli/reitit-routing\n

Require reitit

(require '[reitit.core :as reitit])\n

Define several simple routes using reitit router

Routes are defined as a collection (vector) of vectors, with each vector defining the path of the route and an optional name.

reitit.core/router creates a router from the collection of vectors and an optional hash-map of routes configuration options (e.g middleware)

(def router\n    (reitit/router\n     [[\"/api/ping\" ::ping]\n      [\"/api/game-scoreboard/:score-id\" ::game-score]]))\n

names are used to provide a unique way of referring to a route throughout the whole project, as they are a namespace qualified keyword

Selects implementation based on route details. The following options are available:

Key description :path Base-path for routes :routes Initial resolved routes (default []) :data Initial route data (default {}) :spec clojure.spec definition for a route data, see reitit.spec on how to use this :syntax Path-parameter syntax as keyword or set of keywords (default #{:bracket :colon}) :expand Function of arg opts => data to expand route arg to route data (default reitit.core/expand) :coerce Function of route opts => route to coerce resolved route, can throw or return nil :compile Function of route opts => result to compile a route handler :validate Function of routes opts => () to validate route (data) via side-effects :conflicts Function of {route #{route}} => () to handle conflicting routes :exception Function of Exception => Exception to handle creation time exceptions (default reitit.exception/exception) :router Function of routes opts => router to override the actual router implementation

Routes can be found by either path or name

"},{"location":"micro-framework/","title":"Micro-frameworks","text":"

Introducing common micro-frameworks (curated libraries and configuration) as a basis for your own projects.

  • Luminus
  • Pedestal
  • JUXT Edge
"},{"location":"micro-framework/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":"

Pull requests are welcome

"},{"location":"micro-framework/edge/","title":"JUXT Edge","text":"

JUXT Edge is a foundation for Clojure projects, using leading edge libraries and with upgrade path as the Edge project evolves.

Projects are created with Clojure CLI and tools.deps

"},{"location":"micro-framework/edge/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":"

Pull requests are welcome

"},{"location":"micro-framework/luminus/","title":"Luminus","text":"

Luminus is a clojure micro-framework based on a set of lightweight libraries. It ams to provide a robust and configurable template to generate web services and applications.

Luminus also supports ClojureScript for browser based and Mobile UI's.

"},{"location":"micro-framework/luminus/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":"

Pull requests are welcome

"},{"location":"micro-framework/luminus/#hintluminus-uses-leiningen","title":"Hint::Luminus uses Leiningen","text":"

TODO: review how easy it would be to convert this to a tools.deps project.

"},{"location":"micro-framework/pedestal/","title":"Pedestal","text":""},{"location":"micro-framework/pedestal/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":"

Pull requests are welcome

"},{"location":"micro-services/","title":"Clojure Microservices","text":"

Clojure Microservices are an architectural design approach, with advantages and constraints.

Domain Driven Design (DDD) princilples are relevant to microservice design.

Effective testing is essential to avoid regressions and help ensure a consistent API across all services. Changes should be additional rather than breaking where ever possible.

Aspects of a Microservice

A microservice is a natural concequence of applying the single responsibility princilple at the architecture level

A microservice typicaly has its own data store, persistence layer or connection to an event stream

A microservice should to be loosley coupled to be effecitve to use and maintain

Avoid breaking API changes

The internal implementation of any microservice should be readily changed without breaking an established (shared) API.

Once a microservice API is published to the system, only additional changes should be made to avoid breaking other services that depend on the microservice.

Where breaking changes are the only remaining option, extensive communication is essential across the organisation.

Page work in progress"},{"location":"micro-services/#anatomy-of-a-microservice","title":"Anatomy of a microservice","text":"
  • Resource
  • service layer
  • domain model
  • Repositories
  • Persistent layer | Gateway
"},{"location":"micro-services/#gateway","title":"Gateway","text":"

Abstract away the communication layer between micro-services

"},{"location":"micro-services/#ddd-bounded-context","title":"DDD Bounded context","text":"

grouping your domain model in to self contained logical chunks

  • small islands of concepts with relationships between them
  • connection over rest or lightweight event bus
  • can be deployed quickly (hours not days/weeks)
  • understand the concequence of changes to a microservice
"},{"location":"micro-services/#resilience","title":"Resilience","text":"

Microservices should not fail or cause others to fail

Comprehensive documentation should be maintained for each microservice, optionally generating and publishing the API documentation from the code of the service itself, e.g. swagger, backstage.io

Testing should focus on maintaining a robust API, ensuring changes are additive to avoid breaking the overall system of microservices.

  • unit tests are self-contained, testing the handler functions that compose the overall API
  • integration tests ensure external services continue to provide expected results and shape of responses
  • end-to-end tests are very challenging when the system is first evolving, as design can change rapidly

unit tests should not delay the rate at which you deploy your microservices

"},{"location":"project-url-shortner/","title":"Project: URL Shortner as a Service","text":"

In this section we will build a webservice to create short url's for web addresses, as with services such as bit.ly.

The web service will also manage the redirection of your browser from the short url to the real web address.

This project will take the simplest approach and is therefore not attempting to build a production ready service.

"},{"location":"project-url-shortner/add-alias-to-database/","title":"add alias to database","text":""},{"location":"project-url-shortner/add-static-resources/","title":"Add static resources","text":""},{"location":"project-url-shortner/alias-generator/","title":"Alias generator","text":""},{"location":"project-url-shortner/compojure-template/","title":"Compojure Template","text":""},{"location":"project-url-shortner/compojure-template/#compojure-api","title":"Compojure API","text":"

https://weavejester.github.io/compojure/index.html

"},{"location":"project-url-shortner/create-database/","title":"create database","text":""},{"location":"project-url-shortner/create-html-form/","title":"Create HTML Form","text":""},{"location":"project-url-shortner/create-project/","title":"Create project","text":"

To create a web service we can use two commonly used libraries in Clojure, ring and compojure.

Ring provides many low-level functions to manage web requests and responses as well as providing an embedded web server (ie. Jetty). Most importantly it abstracts away all the complicated details of HTTP communication. So as a developer of the web app you mostly focus on processing a request map and returning a response map.

Compojure provides a simple way to define routes for your application, eg what function is called when a browser requests a specific url.

lein new compojure shorturl-service\n\ncd shorturl-service\n

Open the project.clj file in an editor and take a look at the dependencies added to the project.

(defproject shorturl-service \"0.1.0-SNAPSHOT\"\n  :description \"FIXME: write description\"\n  :url \"http://example.com/FIXME\"\n  :min-lein-version \"2.0.0\"\n  :dependencies [[org.clojure/clojure \"1.8.0\"]\n                 [compojure \"1.5.1\"]\n                 [ring/ring-defaults \"0.2.1\"]]\n  :plugins [[lein-ring \"0.9.7\"]]\n  :ring {:handler shorturl-service.handler/app}\n  :profiles\n  {:dev {:dependencies [[javax.servlet/servlet-api \"2.5\"]\n                        [ring/ring-mock \"0.3.0\"]]}})\n

Apart from Clojure itself, the compojure and ring libraries have been added.

The :plugins section adds lein-ring which allows us to run the server using the command lein ring server

The :ring section defines the default function to call when running the project

The :profiles section adds libraries useful for development and testing.

"},{"location":"project-url-shortner/create-project/#note-create-a-new-project-using-leiningen-and-the-compojure-template-then-go-into-the-project-created","title":"Note:: Create a new project using Leiningen and the compojure template, then go into the project created.","text":""},{"location":"project-url-shortner/delete-alias-from-database/","title":"delete alias from database","text":""},{"location":"project-url-shortner/design-data-structure/","title":"Design data structure","text":"

One of the first decisions is how to design the data structure to hold our short url and full url addresses.

"},{"location":"project-url-shortner/design-data-structure/#the-simplest-approach","title":"The simplest approach","text":"

We could create a really simple data structure with a vector

[\"goouk\" \"https://duckduckgo.com/\"]\n

In order to hold multiple short url mappings, we could have a vector of vectors

[[\"duckduckgo\" \"https://duckduckgo.com/\"]\n [\"practicalli\" \"https://practical.li\"]\n [\"slashdot\" \"https://slashdot.com\"]]\n
Although the above is nice and simple, it does not provide any context for the data.

"},{"location":"project-url-shortner/design-data-structure/#using-a-map-for-context","title":"Using a map for context","text":"

The keys in a map can add specific meaning and context to the design of the data structure.

Here are two examples, the first with keys as strings the second as keys as keywords.

{\"short-url\" \"practicalli\" \"full-url\" \"http://practical.li\"}\n\n{:short-url \"practicalli\" :full-url \"http://practical.li\"}\n

As keys need to be unique, then if we have multiple short url mappings to contain, then we need another data structure for each map.

[{:short-url \"practicalli\" :full-url \"http://practical.li\"}\n {:short-url \"slashdot\" :full-url \"https://slashdot.org\"}]\n

In the above design we cannot use a single map as all the keys in a map need to be unique

"},{"location":"project-url-shortner/design-data-structure/#simplifying-the-map","title":"Simplifying the map","text":"

As keys must be unique in a map, we cannot have multiple keys called :short-url, therefore using that design we cant have a single map.

We could simplify the map and remove the current keys and use the value for the short-url as the key and the full url as the value. This means we could just have a single map for all our short-url mappings.

{\"practicalli\" \"http://practical.li\"\n \"slashdot\"    \"https://slashdot.org\"\n \"duckduckgo\"  \"https://duckduckgo.com/\"}\n

Using Clojure keywords for the keys would also allow us to look up the full url addresses using the feature of maps that make keywords act like functions. This feature of the keyword in a map is just like calling the get function on the map with a specific key.

(def url-map\n  {:practicalli \"http://practical.li\"\n   :slashdot \"https://slashdot.org\"\n   :duck-duck-go \"https://duckduckgo.com/\"})\n\n(get url-map :practicalli)\n;; => \"http://practicalli.co.uk\"\n\n(url-map :practicalli)\n;; => \"http://practicalli.co.uk\"\n\n(:practicalli url-map )\n;; => \"http://practicalli.co.uk\"\n
"},{"location":"project-url-shortner/disable-anti-forgery-check/","title":"Disable anti-forgery check","text":"

The ring-defaults library provides sensible Ring middleware defaults, especially in terms of security. The ring-defaults library is included in the

Anyone can send a GET request to a ring webapp, however with ring-defaults included then only pages / URLs from the webapp itself are allowed to POST.

Ring uses an anti-forgery token that needs to be setup in the project, otherwise you get the dreaded \"Invalid Anti-forgery token\" error message.

To keep things simple we are going to turn off the anti-forgery settings provided by Ring-Defaults so we can make our POST without the CSRF protection.

(def app\n  (wrap-defaults\n  app-routes\n  (assoc-in site-defaults [:security :anti-forgery] false)))\n
"},{"location":"project-url-shortner/disable-anti-forgery-check/#note-edit-the-definition-of-app-in-srcshorturl-servicehandlerclj-and-replace-site-defaults-with-a-function-to-set-anti-forgery-to-false-in-site-defaults","title":"Note:: Edit the definition of app in src/shorturl-service/handler.clj and replace site-defaults with a function to set :anti-forgery to false in site-defaults.","text":""},{"location":"project-url-shortner/disable-anti-forgery-check/#ring-middleware-defaults","title":"Ring middleware defaults","text":"

There are a number of ring middleware defaults that define some common middleware functions for your app.

For example, here is the site-defaults definition

(def site-defaults\n  \"A default configuration for a browser-accessible website, based on current\n  best practice.\"\n  {:params    {:urlencoded true\n               :multipart  true\n               :nested     true\n               :keywordize true}\n   :cookies   true\n   :session   {:flash true\n               :cookie-attrs {:http-only true}}\n   :security  {:anti-forgery   true\n               :xss-protection {:enable? true, :mode :block}\n               :frame-options  :sameorigin\n               :content-type-options :nosniff}\n   :static    {:resources \"public\"}\n   :responses {:not-modified-responses true\n               :absolute-redirects     true\n               :content-types          true\n               :default-charset        \"utf-8\"}})\n
"},{"location":"project-url-shortner/disable-anti-forgery-check/#a-rough-guide-to-security-middleware-from-ring","title":"A rough guide to security middleware from ring","text":"
  • :anti-forgery - Set to true to add CSRF protection via the ring-anti-forgery library.
  • :content-type-options - Prevents attacks based around media-type confusion. See: wrap-content-type-options.
  • :frame-options - Prevents your site from being placed in frames or iframes. See: wrap-frame-options.
  • :hsts - If true, enable HTTP Strict Transport Security. See: wrap-hsts.
  • :ssl-redirect - If true, redirect all HTTP requests to the equivalent HTTPS URL. A map with an :ssl-port option may be set instead, if the HTTPS server is on a non-standard port. See: wrap-ssl-redirect.
  • :xss-protection - Enable the X-XSS-Protection header that tells supporting browsers to use heuristics to detect XSS attacks. See: wrap-xss-protection.

See more details of ring middleware defaults

"},{"location":"project-url-shortner/disable-anti-forgery-check/#adding-other-functions-as-middleware","title":"Adding other functions as middleware","text":"

ring just uses function composition for middleware you can simply wrap your own function calls around the call to wrap defaults, so long as those functions deal with request/response maps appropriately.

(def app\n  (my-additional-middleware\n    (wrap-defaults app-routes site-defaults)\n  arguments to my additional middleware))\n

Or use the threading macro

(def app\n  (-> (wrap-defaults app-routes site-defaults)\n      (friend-stuff arg arg)\n      (other-middleware arg arg arg))\n
"},{"location":"project-url-shortner/get-alias-from-database/","title":"get alias from database","text":""},{"location":"project-url-shortner/html-form/","title":"HTML Form","text":""},{"location":"project-url-shortner/if-let-function/","title":"if-let function","text":""},{"location":"project-url-shortner/named-alias-handler/","title":"Named alias handler","text":""},{"location":"project-url-shortner/persist-aliases/","title":"Persist aliases","text":""},{"location":"project-url-shortner/postgres-setup/","title":"Postgres setup","text":""},{"location":"project-url-shortner/redirect-to-full-url/","title":"Redirect short URL to full web address","text":"

Adding a redirect is very easy to do with ring, as the ring library provides a function called redirect that takes a url as an argument

(ns shorturl-service.handler\n  (:require [compojure.core :refer :all]\n            [compojure.route :as route]\n            [ring.middleware.defaults :refer [wrap-defaults site-defaults]]\n            [ring.handler.dump :refer [handle-dump]]\n            [ring.util.response :refer [redirect]]))\n
"},{"location":"project-url-shortner/redirect-to-full-url/#note-include-the-ringutilresponseredirect-function-into-the-shorturl-servicehandler-namespace-so-that-we-can-simply-call-the-redirect-function","title":"Note:: Include the ring.util.response/redirect function into the shorturl-service.handler namespace so that we can simply call the redirect function","text":""},{"location":"project-url-shortner/redis-setup/","title":"Redis setup","text":""},{"location":"project-url-shortner/refacor-hiccup-form/","title":"Refactor: Hiccup form","text":""},{"location":"project-url-shortner/return-short-url/","title":"Return short URL","text":""},{"location":"project-url-shortner/return-url-aliases/","title":"Return URL aliases","text":""},{"location":"project-url-shortner/run-project/","title":"Run project","text":"

The compojure template provides a working webserver and simple webapp out of the box.

lein ring server\n

If you have not used Compojure or Ring previously, then it may take a few seconds to download their libraries from the Internet before starting the Jetty web server.

You should see a output after the leiningen command showing you that the server has started

2016-07-15 13:44:02.242:INFO:oejs.Server:jetty-7.6.13.v20130916\n2016-07-15 13:44:02.313:INFO:oejs.AbstractConnector:Started SelectChannelConnector@0.0.0.0:3000\nStarted server on port 3000\n

Your default browser should also open at http://localhost:3000 with a message saying \"Hello World\". If your browser does not open then check for errors in the terminal where you ran the leiningen command.

"},{"location":"project-url-shortner/run-project/#note-run-the-project-to-start-the-server-and-webapp","title":"Note:: Run the project to start the server and webapp","text":""},{"location":"project-url-shortner/test-app-reloading/","title":"Test app reloading","text":"

The compojure template added several middleware functions to our project to make the webapp easier to work with. The wrap-reload middleware picks up changes to our code and will automatically load them into our running webapp. This provides rapid feedback on your coding.

(defroutes app-routes\n  (GET \"/\" [] \"Hello reloaded World\")\n  (route/not-found \"Not Found\"))\n

Now, refresh your browser and see the changes made.

"},{"location":"project-url-shortner/test-app-reloading/#note-make-a-simple-change-to-the-app-routes-function-in-srcshorturl-servicehandlerclj-file-eg-change-the-hello-world-string-to-hello-reloaded-world","title":"Note:: Make a simple change to the app-routes function in src/shorturl-service/handler.clj file, eg. change the \"Hello World\" string to \"Hello reloaded World\"","text":""},{"location":"project-url-shortner/test-app-reloading/#hint-you-should-only-need-to-restart-the-server-again-if-you-add-libraries-or-define-code-outside-of-the-scope-of-the-app-or-if-your-code-crashes-the-server-but-i-am-sure-that-wont-happen","title":"Hint:: You should only need to restart the server again if you add libraries or define code outside of the scope of the app. Or if your code crashes the server, but I am sure that wont happen :)","text":""},{"location":"project-url-shortner/using-ring-redirect/","title":"Using Ring Redirect","text":""},{"location":"project-url-shortner/whats-in-a-request/","title":"What is in a Request","text":"

The ring library converts the HTTP request into a clojure map, making it really easy to extract some or all of the values out of the request map.

"},{"location":"project-url-shortner/whats-in-a-request/#ring-parameters","title":"ring parameters","text":"

The wrap-params middleware function adds support for url-encoded parameters.

URL-encoded parameters are the primary way browsers pass values to web applications. These parameters are sent when a user submits a form.

When applied to a handler, the parameter middleware adds three new keys to the request map:

  • :query-params - A map of parameters from the query string
  • :form-params - A map of parameters from submitted form data
  • :params - A merged map of all parameters
"},{"location":"project-url-shortner/whats-in-a-request/#viewing-ring-parameters","title":"viewing ring parameters","text":"

You could write a function to simply display all the parameters as the body of the response. However Ring already provides a function to do this called handle-dump

To use the handle-dump function, first include the function in the namespace using (:require [ring.handler.dump :refer [handle-dump]])

(ns shorturl-service.handler\n  (:require [compojure.core :refer :all]\n            [compojure.route :as route]\n            [ring.middleware.defaults :refer [wrap-defaults site-defaults]]\n            [ring.handler.dump :refer [handle-dump]]))\n

As we have multiple :require statements we can simply chain them all together as above.

"},{"location":"project-url-shortner/whats-in-a-request/#note-add-the-ringhandlerdumphandle-dump-function-into-the-shorturl-servicehandler-namespace-along-with-other-require-statements","title":"Note:: Add the ring.handler.dump/handle-dump function into the shorturl-service.handler namespace along with other require statements","text":""},{"location":"projects/","title":"Project Guides","text":"

Follow a step-by-step project guide and build a live services which can also be deployed.

"},{"location":"projects/banking-on-clojure/","title":"Banking on Clojure web application","text":"

Work In Progress

Project actively being developed as part of the Practicalli Study group WebApps.

Code so far is shared on practicalli/banking-on-clojure-webapp GitHub repository

Building a Banking application using Clojure, spec, H2 (development) & Postgresql (live) databases and next.jdbc for SQL queries (migratus for db migrations).

The system infrastructure uses Jetty or HTTP-kit (making this switchable at runtime) and a component life cycle system (probably mount).

"},{"location":"projects/banking-on-clojure/#application-design-in-progress","title":"Application Design (in progress)","text":"

Data Specifications created using clojure.spec.alpha

  • Customer Details
  • Account holder
  • Bank account
  • Multiple Bank accounts
  • Credit Card
  • Mortgate

Functions and function specifications using clojure.spec.alpha

  • register-account-holder
  • open-credit-account
  • open-savings-account
  • open-credit-card-account
  • open-mortgage-account
  • Make a payment
  • Send account notification
  • Check for overdraft

Functions with specifications are instrumented to check arguments passed during function calls.

Generative testing is carried out via the kaocha test runner

"},{"location":"projects/banking-on-clojure/#development-workflow","title":"Development Workflow","text":"
  • Write a failing test
  • write mock data
  • write an function definition that returns the argument
  • run tests - tests should fail
  • write a spec for the functions argument - customer
  • write a spec for the return value
  • write a spec for relationship between args and return value
  • replace the mock data with generated values from specification
  • update functions and make tests pass
  • instrument functions
  • run specification checks
"},{"location":"projects/banking-on-clojure/account-overview-page/","title":"Initial design","text":"

The initial design is a simple copy/paste approach to see what the results look like in the web page. Once the design has been established, the code will be refactored to reduce duplication.

(defn accounts-overview-page\n  [request]\n  (response\n    (html5\n      {:lang \"en\"}\n      [:head\n       (include-css \"https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css\")]\n      [:body\n       [:section {:class \"hero is-info\"}\n        [:div {:class \"hero-body\"}\n         [:div {:class \"container\"}\n          [:h1 {:class \"title\"} \"Banking on Clojure\"]\n          [:p {:class \"subtitle\"}\n           \"Making your money immutable\"]]]]\n\n       [:section {:class \"section\"}\n        [:article {:class \"media\"}\n         [:figure {:class \"media-left\"}\n          [:p {:class \"image is-64x64\"}\n           [:img {:src \"https://raw.githubusercontent.com/jr0cket/developer-guides/master/clojure/clojure-bank-coin.png\"}]]]\n         [:div {:class \"media-content\"}\n          [:div {:class \"content\"}\n           [:h3 {:class \"subtitle\"}\n            \"Current Account : &lambda;1,000,000\"]\n           [:p \"Account number: 123456789   Sort code: 01-02-01\"]]]\n         [:div {:class \"media-right\"}\n          (link-to {:class \"button is-primary\"} \"/transfer\" \"Transfer\")\n          (link-to {:class \"button is-info\"} \"/payment\" \"Payment\")]]\n\n        [:article {:class \"media\"}\n         [:figure {:class \"media-left\"}\n          [:p {:class \"image is-64x64\"}\n           [:img {:src \"https://raw.githubusercontent.com/jr0cket/developer-guides/master/clojure/clojure-bank-coin.png\"}]]]\n         [:div {:class \"media-content\"}\n          [:div {:class \"content\"}\n           [:h3 {:class \"subtitle\"}\n            \"Savings Account : &lambda;1,000,000 \"]\n           [:p \"Account number: 123454321    Sort code: 01-02-01\"]]]\n         [:div {:class \"media-right\"}\n          (link-to {:class \"button is-primary\"} \"/transfer\" \"Transfer\")\n          (link-to {:class \"button is-info\"} \"/payment\" \"Payment\")]]\n\n        [:article {:class \"media\"}\n         [:figure {:class \"media-left\"}\n          [:p {:class \"image is-64x64\"}\n           [:img {:src \"https://raw.githubusercontent.com/jr0cket/developer-guides/master/clojure/clojure-bank-coin.png\"}]]]\n         [:div {:class \"media-content\"}\n          [:div {:class \"content\"}\n           [:h3 {:class \"subtitle\"}\n            \"Tax Free Savings Account : &lambda;1,000,000 \"]]]\n         [:div {:class \"media-right\"}\n          (link-to {:class \"button is-primary\"} \"/transfer\" \"Transfer\")\n          (link-to {:class \"button is-info\"} \"/payment\" \"Payment\")]]\n\n        [:article {:class \"media\"}\n         [:figure {:class \"media-left\"}\n          [:p {:class \"image is-64x64\"}\n           [:img {:src \"https://raw.githubusercontent.com/jr0cket/developer-guides/master/clojure/clojure-bank-coin.png\"}]]]\n         [:div {:class \"media-content\"}\n          [:div {:class \"content\"}\n           [:h3 {:class \"subtitle\"}\n            \"Mortgage Account : &lambda;1,000,000 \"]\n           [:p \"Mortgage Reference: 98r9e8r79wr87e9232\"]]]\n         [:div {:class \"media-right\"}\n          (link-to {:class \"button is-primary\"} \"/transfer\" \"Transfer\")\n          (link-to {:class \"button is-info\"} \"/payment\" \"Payment\")]]\n\n        ]])))\n
"},{"location":"projects/banking-on-clojure/account-overview-page/#refactor-page-layout-with-functions","title":"Refactor page layout with functions","text":"

Duplicate markup code for pages can be wrapped in a helper function that generated the correct code, usually given a few specific arguments.

Its usually more effective to create the design with duplicate code first and then see the sections of code that are duplicated.

  (defn unordered-list [items]\n    [:ul\n     (for [i items]\n       [:li i])])\n

Many lines of code can now be reduced to a single line when adding an unordered list to a page defined with hiccup.

  [:div\n   (unordered-list [\"collection\" \"of\" \"list\" \"items\"])]\n

Applying this technique to the bank accounts overview page. Find the code that is repeated the most and define a function containing that code.

The function should take arguments, typically a map so its more flexible in what arguments must be passed and increases the usability of the function

(defn bank-account-media-object\n  [account-details]\n  [:article {:class \"media\"}\n   [:figure {:class \"media-left\"}\n    [:p {:class \"image is-64x64\"}\n     [:img {:src \"https://raw.githubusercontent.com/jr0cket/developer-guides/master/clojure/clojure-bank-coin.png\"}]]]\n   [:div {:class \"media-content\"}\n    [:div {:class \"content\"}\n     [:h3 {:class \"subtitle\"}\n      (str (:account-type account-details) \" : &lambda;\" (:account-value account-details))]\n     [:p\n      (str \"Account number: \" (:account-number account-details) \" Sort code: \" (:account-sort-code account-details))]]]\n   [:div {:class \"media-right\"}\n    (link-to {:class \"button is-primary\"} \"/transfer\" \"Transfer\")\n    (link-to {:class \"button is-info\"} \"/payment\" \"Payment\")]])\n

Calling the bank-account-media-object function with a map will generate the same resulting page.

  (bank-account-media-object\n    {:account-type      \"Current Account\"\n     :account-number    \"123456789\"\n     :account-value     \"i1,000,000\"\n     :account-sort-code \"01-02-01\"})\n

Result of calling the bank-account-media-object with that map:

[:article {:class \"media\"}\n [:figure {:class \"media-left\"}\n  [:p {:class \"image is-64x64\"}\n   [:img {:src \"https://raw.githubusercontent.com/jr0cket/developer-guides/master/clojure/clojure-bank-coin.png\"}]]]\n [:div {:class \"media-content\"}\n  [:div {:class \"content\"}\n   [:h3 {:class \"subtitle\"} \"Current Account : &lambda;\" \"i1,000,000\"]\n   [:p \"Account number: \" {:account-number {:account-type \"Current Account\", :account-number \"123456789\", :account-value \"i1,000,000\", :account-sort-code \"01-02-01\"}} \" Sort code: \" {:account-sort-code {:account-type \"Current Account\", :account-number \"123456789\", :account-value \"i1,000,000\", :account-sort-code \"01-02-01\"}}]]]\n   [:div {:class \"media-right\"}\n    [:a {:href #object[java.net.URI 0x3516357f \"/transfer\"], :class \"button is-primary\"} (\"Transfer\")]\n    [:a {:href #object[java.net.URI 0x4cf62d27 \"/payment\"], :class \"button is-info\"} (\"Payment\")]]]\n

If some key/value pairs are not included, the function will still work. Worst case is that there will be gaps where expected values would otherwise be included in the page.

Instead of many duplicate lines of code, the accounts-overview-page is reduce to less than half the code is far more readable as it is expressed as data in a hash-map.

(defn accounts-overview-page\n  [request]\n  (response\n    (html5\n      {:lang \"en\"}\n      [:head\n       (include-css \"https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css\")]\n      [:body\n       [:section {:class \"hero is-info\"}\n        [:div {:class \"hero-body\"}\n         [:div {:class \"container\"}\n          [:h1 {:class \"title\"} \"Banking on Clojure\"]\n          [:p {:class \"subtitle\"}\n           \"Making your money immutable\"]]]]\n\n       [:section {:class \"section\"}\n        (bank-account-media-object {:account-type  \"Current Account\" :account-number    \"123456789\"\n                                    :account-value \"1,234\"       :account-sort-code \"01-02-01\"})\n\n        (bank-account-media-object {:account-type  \"Savings Account\" :account-number    \"123454321\"\n                                    :account-value \"2,000\"       :account-sort-code \"01-02-01\"})\n\n        (bank-account-media-object {:account-type  \"Tax Free Savings Account\" :account-number    \"123454321\"\n                                    :account-value \"20,000\"                :account-sort-code \"01-02-01\"})\n\n        (bank-account-media-object {:account-type  \"Mortgage Account\" :account-number    \"98r9e8r79wr87e9232\"\n                                    :account-value \"354,000\"          :account-sort-code \"01-02-01\"})\n\n        ]])))\n
"},{"location":"projects/banking-on-clojure/account-overview-page/#wrap-results-in-page-heading","title":"Wrap results in page heading","text":"

The heading is common in all pages, so it can be extracted to a function that processes the result of all handlers, this is referred to as middleware.

"},{"location":"projects/banking-on-clojure/clojure-server-project/","title":"Create Clojure server project","text":"

Initially the project is configured with a simple application sever using http-kit and routing defined by compojure library. The ring library is used to generate well-formed responses.

"},{"location":"projects/banking-on-clojure/clojure-server-project/#create-project","title":"Create project","text":"

Create a Clojure CLI project from the app template

Practicalli Clojure CLI ConfigAlias Definition

Using :project/create alias from Practicalli Clojure CLI Config

clojure -T:project/create :template app :name practicalli.banking-on-clojure/service :target-dir banking-on-clojure\n

Add an alias definition to the user configuration for Clojure CLI, eg. $XDG_CONFIG_HOME/clojure/deps.edn or $HOME/.clojure/deps.edn

:project/create\n{:replace-deps {io.github.seancorfield/deps-new {:git/tag \"v0.4.13\" :git/sha \"879c4eb\"}}\n :exec-fn      org.corfield.new/create\n :exec-args    {:template app :name practicalli/playground}}\n

Consider using Practicalli Clojure CLI Config to simply add a wide range of tools for Clojure CLI

"},{"location":"projects/banking-on-clojure/clojure-server-project/#library-dependencies","title":"Library Dependencies","text":"

Add the http-kit, compojure and ring libraries to the project configuration

deps.edn
{:paths [\"src\" \"resources\"]\n :deps {org.clojure/clojure {:mvn/version \"1.11.3\"}\n        http-kit/http-kit   {:mvn/version \"2.3.0\"}\n        ring/ring           {:mvn/version \"1.9.6\"}}}\n
"},{"location":"projects/banking-on-clojure/clojure-server-project/#configure-namespace","title":"Configure namespace","text":"

Add org.httpkit.server, compojure and ring.util.response as required namespaces

src/practicalli/banking_on_clojure/service.clj
(ns practicalli.banking-on-clojure\n  (:gen-class)\n  (:require [org.httpkit.server :as app-server]\n            [compojure.core :refer [defroutes GET]]\n            [ring.util.response :refer [response]]))\n
"},{"location":"projects/banking-on-clojure/clojure-server-project/#routing-and-request-handlers","title":"Routing and Request handlers","text":"

compojure provides an abstraction for routing. The defroutes function directs requests to handlers, which are Clojure functions that take a request hash-map as an argument.

The routing is based on the http protocol (GET, POST, etc) and URL.

src/practicalli/banking_on_clojure/service.clj
(defn welcome-page\n  [request]\n  (response \"Banking on Clojure\"))\n\n(defroutes app\n  (GET \"/\" [] welcome-page))\n
"},{"location":"projects/banking-on-clojure/clojure-server-project/#defining-the-application-server-system","title":"Defining the application server system","text":"

A clojure.core/atom is used to hold a reference to application server instance for stopping/restarting the server.

src/practicalli/banking_on_clojure/service.clj
(defonce app-server-instance (atom nil))\n

A function to start the application server on a given HTTP port number.

The start process sends a timestamped log message to standard out before starting the application server.

The app-server-instance is updated with a reference to the running application server.

src/practicalli/banking_on_clojure/service.clj
(defn app-server-start\n  \"Start the application server and log the time of start.\"\n\n  [http-port]\n  (println (str (java.util.Date.)\n                \" INFO: Starting server on port: \" http-port))\n  (reset! app-server-instance\n          (app-server/run-server #'app {:port http-port})))\n

A function to stop the application server, send out a timestamped log message and remove the application server reference.

src/practicalli/banking_on_clojure/service.clj
(defn app-server-stop\n  \"Gracefully shutdown the server, waiting 100ms.  Log the time of shutdown\"\n  []\n  (when-not (nil? @app-server-instance)\n    (@app-server-instance :timeout 100)\n    (reset! app-server-instance nil)\n    (println (str (java.util.Date.)\n                  \" INFO: Application server shutting down...\"))))\n

A function to restart the application server, which simply calls the stop and start functions.

src/practicalli/banking_on_clojure/service.clj
(defn app-server-restart\n  \"Convenience function to stop and start the application server\"\n\n  [http-port]\n  (app-server-stop)\n  (app-server-start http-port))\n

A -main function that will be called from the command line, taking an optional HTTP port. If a port number is no provided as an argument, an operating system environment variable called PORT is used or the default 8888 is used.

Using an operating system environment variable is important when deploying the application to a cloud environment.

src/practicalli/banking_on_clojure/service.clj
(defn -main\n  \"Select a value for the http port the app-server will listen to\n  and call app-server-start\n\n  The http port is either an argument passed to the function,\n  an operating system environment variable or a default value.\"\n\n  [& [http-port]]\n  (let [http-port (Integer. (or http-port (System/getenv \"PORT\") \"8888\"))]\n    (app-server-start http-port)))\n
"},{"location":"projects/banking-on-clojure/clojure-server-project/#repl-driven-development-helpers","title":"REPL driven development helpers","text":"

A comment block is added at the end of the code to show how to start/stop/restart the web application, along with a few useful expressions.

src/practicalli/banking_on_clojure/service.clj
(comment\n  ;; Start application server - via `-main` or `app-server-start`\n  (-main)\n  (app-server-start 8888)\n\n  ;; Stop / restart application server\n  (app-server-stop)\n  (app-server-restart 8888)\n\n  ;; Get PORT environment variable from Operating System\n  (System/getenv \"PORT\")\n\n  ;; Get all environment variables\n  ;; use a data inspector to view environment-variables name\n  (def environment-variables\n    (System/getenv))\n\n  ;; Check values set in the default system properties\n  (def system-properties\n    (System/getProperties))\n  )\n
"},{"location":"projects/banking-on-clojure/clojure-server-project/#the-complete-code-so-far","title":"The Complete code so far","text":"src/practicalli/banking_on_clojure/service.clj
(ns practicalli.banking-on-clojure\n  (:gen-class)\n  (:require [org.httpkit.server :as app-server]\n            [compojure.core :refer [defroutes GET]]\n            [ring.util.response :refer [response]]))\n\n;; ---------------------------------------\n;; Request handlers\n\n(defn welcome-page\n  [request]\n  (response \"Banking on Clojure\"))\n;; ---------------------------------------\n\n;; ---------------------------------------\n;; Request Routing\n\n(defroutes app\n  (GET \"/\" [] welcome-page))\n;; ---------------------------------------\n\n;; ---------------------------------------\n;; System\n\n;; Reference to application server instance for stopping/restarting\n(defonce app-server-instance (atom nil))\n\n\n(defn app-server-start\n  \"Start the application server and log the time of start.\"\n\n  [http-port]\n  (println (str (java.util.Date.)\n                \" INFO: Starting server on port: \" http-port))\n  (reset! app-server-instance\n          (app-server/run-server #'app {:port http-port})))\n\n\n(defn app-server-stop\n  \"Gracefully shutdown the server, waiting 100ms.  Log the time of shutdown\"\n  []\n  (when-not (nil? @app-server-instance)\n    (@app-server-instance :timeout 100)\n    (reset! app-server-instance nil)\n    (println (str (java.util.Date.)\n                  \" INFO: Application server shutting down...\"))))\n\n\n(defn app-server-restart\n  \"Convenience function to stop and start the application server\"\n\n  [http-port]\n  (app-server-stop)\n  (app-server-start http-port))\n\n\n(defn -main\n  \"Select a value for the http port the app-server will listen to\n  and call app-server-start\n\n  The http port is either an argument passed to the function,\n  an operating system environment variable or a default value.\"\n\n  [& [http-port]]\n  (let [http-port (Integer. (or http-port (System/getenv \"PORT\") \"8888\"))]\n    (app-server-start http-port)))\n;; ---------------------------------------\n\n;; ---------------------------------------\n;; REPL driven development helpers\n\n(comment\n  ;; Start application server - via `-main` or `app-server-start`\n  (-main)\n  (app-server-start 8888)\n\n  ;; Stop / restart application server\n  (app-server-stop)\n  (app-server-restart 8888)\n\n  ;; Get PORT environment variable from Operating System\n  (System/getenv \"PORT\")\n\n  ;; Get all environment variables\n  ;; use a data inspector to view environment-variables name\n  (def environment-variables\n    (System/getenv))\n\n  ;; Check values set in the default system properties\n  (def system-properties\n    (System/getProperties))\n  )\n;; ---------------------------------------\n
"},{"location":"projects/banking-on-clojure/clojure-spec-generate-mock-data/","title":"Clojure spec generate mock data","text":""},{"location":"projects/banking-on-clojure/clojure-spec-generate-mock-data/#generate-database-record-data-from-clojure-specifications","title":"Generate database record data from Clojure Specifications","text":"

The Clojure specifications developed for the banking-on-clojure application can be used to generate random data that can be used to test the database schema and CRUD functions.

In the database schema design page the next.jdbc.sql functions were used to define a generic function for inserting records into a database

(defn insert-record\n  [table record-data db-spec]\n  (with-open [connection (jdbc/get-connection db-spec)]\n    (jdbc-sql/insert! connection table record-data)))\n

The call to this function uses a value for record-data that is almost identical to the value generated from the practicalli.banking-on-clojure/account-holder specification. The only difference is the key name styles.

(create-record db-specification-dev\n                 :public.account_holders\n                 {:account_holder_id      (java.util.UUID/randomUUID)\n                  :first_name             \"Rachel\"\n                  :last_name              \"Rocketpack\"\n                  :email_address          \"rach@rocketpack.org\"\n                  :residential_address    \"1 Ultimate Question Lane, Altar IV\"\n                  :social_security_number \"BB104312D\"})\n
"},{"location":"projects/banking-on-clojure/clojure-spec-generate-mock-data/#generating-data-from-clojure-specifications","title":"Generating data from Clojure Specifications","text":"

clojure.spec.alpha/gen takes a spec and returns a reference to a generator for that specification.

clojure.spec.gen.alpha/generate returns a random value using the spec generator.

Generate a value from the account-holder specification

(spec-gen/generate (spec/gen ::account-holder))\n

Create a simple helper function in practicalli/specifications-banking.clj to generating mock data from the specifications relevant to the database.

(defn mock-data-account-holder\n  []\n  (spec-gen/generate (spec/gen ::account-holder)))\n

The practicalli.database-access/create-record function can now be passed a generated record-data argument using (practicalli.specifications-banking/mock-data-account-holder)

(create-record\n  db-specification-dev\n  :public.account_holders\n  (practicalli.specifications-banking/mock-data-account-holder))\n
"},{"location":"projects/banking-on-clojure/clojure-spec-generate-mock-data/#clojure-and-database-naming-disparity","title":"Clojure and database naming disparity","text":"

When calling create-record with a specification there is a disparity between the names of the keys from the clojure specification and the names of the columns in database.

  • Clojure uses kebab-case
  • Database uses snake_case

Most relational databases will not accept column names in kebab-case.

Do we have to compromise the Clojure style kebab-case just for the database? Do we have to create our own generators or transform code to convert the specs generated?

"},{"location":"projects/banking-on-clojure/clojure-spec-generate-mock-data/#automatically-converting-key-names","title":"Automatically converting key names","text":"

Clojure uses kebab-case for key names in Clojure specs (and all names in general).

Relational databases use snake_case for table and column names and most databases will not support kebab-case.

It is simple to convert between the two cases, as its simply a string replacement for - with _. clj-commons/camel-snake-kebab is a library that converts between each of the naming styles.

camel-snake-kebab.core/->snake_case takes a name and returns it in snake_case

next.jdbc support conversion between camel-case names when clj-commons/camel-snake-kebab is added to the project dependencies.

The next.jdbc.sql CRUD functions take an optional configuration hash-map as the fourth argument. When ,,, is on the class path, next.jdbc has two hash-maps available that will define functions to use from the ,,, library to do the name conversions.

  • next.jdbc/snake-kebab-opts for unqualified Clojure keywords
  • next.jdbc/unqualified-snake-kebab-opts for unqualified Clojure keywords
"},{"location":"projects/banking-on-clojure/clojure-spec-generate-mock-data/#refactor-crud-functions-to-automatically-convert-case","title":"Refactor CRUD functions to automatically convert case","text":"

Refactor the CRUD functions to include the snake-kebab-opts hash-map as an argument.

Only those functions that take keywords as part of the argument need to be changed, so next.jdbc.sql/query does not need to change and therefore the practicalli.database-access/read-record remains unchanged.

Refactor the practicalli.database-access/create-record function

(defn create-record\n  \"Insert a single record into the database using a managed connection.\n  Arguments:\n  - table - name of database table to be affected\n  - record-data - Clojure data representing a new record\n  - db-spec - database specification to establish a connection\"\n  [db-spec table record-data]\n  (with-open [connection (jdbc/get-connection db-spec)]\n    (jdbc-sql/insert! connection table record-data jdbc/snake-kebab-opts)))\n

Refactor the practicalli.database-access/update-record function

(defn update-record\n  \"Insert a single record into the database using a managed connection.\n  Arguments:\n  - table - name of database table to be affected\n  - record-data - Clojure data representing a new record\n  - db-spec - database specification to establish a connection\n  - where-clause - column and value to identify a record to update\"\n  [db-spec table record-data where-clause]\n  (with-open [connection (jdbc/get-connection db-spec)]\n    (jdbc-sql/update! connection table record-data where-clause jdbc/snake-kebab-opts)))\n
"},{"location":"projects/banking-on-clojure/clojure-spec-generate-mock-data/#disparity-between-spec-namespace-and-database-design","title":"Disparity between spec namespace and database design","text":"

A qualified keyword is where that keyword has a namespace, eg. :practicalli/name rather than :name

Using qualified keywords is recommended so that they can be unique across the application (and ideally multiple applications).

When using a database, the table name can be used to qualify the results returned from queries to the database. However, if the table names are different to the clojure.spec specification then it is harder to test

To resolve this issue, either the specifications should be refactored or the database names. I suspect probably both would benefit from some redesign now experience has been gained in using them.

"},{"location":"projects/banking-on-clojure/continuous-integration/","title":"Continuous Integration with CirceCI","text":"

The application infrastructure has been established and now the main body of the development can commence. Therefore it is very valuable to establish a continuous integration pipeline.

Practicalli Clojure: Continuous Integration with CircleCI covers in detail how to use Continuous Integration with Clojure projects (deps.edn and Leiningen).

"},{"location":"projects/banking-on-clojure/continuous-integration/#using-kaocha-test-runner","title":"Using kaocha test runner","text":"

LambdaIsland kaocha test runner is used as the unit test runner as it will also run generative tests where functions have specifications defined.

Add a :test/run alias to the deps.edn file in the root of the project.

The configuration runs Kaocha without test randomisation for a consistent test order and stops the test runner if a test fails, ensuring time is not spent running tests after a failure.

:test/run\n{:extra-paths [\"test\"]\n :extra-deps {lambdaisland/kaocha {:mvn/version \"1.60.977\"}}\n :exec-fn kaocha.runner/exec-fn\n :exec-args {:randomize? false\n             :fail-fast? true}}\n

Create the file bin/kaocha in the root of the project and make it executable (e.g. chmod a+x bin/kaocha)

#!/usr/bin/env bash\n\n## Script to run the kaocha test runner\n## for unit tests and clojure spec generative tests\n\nclojure -X:test/run \"$@\"\n
"},{"location":"projects/banking-on-clojure/continuous-integration/#configure-circleci-pipeline","title":"Configure CircleCI pipeline","text":"

Configure a pipeline to use a docker image with Java 17 and the latest Clojure CLI tools

The configuration uses the Kaocha Orb to simplify the configuration required to use the Kaocha test runner from within CircleCI.

A run step will call the kaocha script that is included in the project code repository and run the unit tests. If function specifications are present in the project, generative tests will also be run.

version: 2.1  # circleci configuration version\n\norbs:\n  kaocha: lambdaisland/kaocha@0.0.3 # Org settings > Security > uncertified orbs\n\njobs:    # basic units of work in a run\n  build: # runs not using Workflows must have a `build` job as entry point\n    working_directory: ~/build    # directory where steps will run\n    docker:                       # run the steps with Docker\n      - image: cimg/clojure:1.10  # image is primary container where `steps` are run\n    environment:                  # environment variables for primary container\n      JVM_OPTS: -Xmx3200m         # limit the maximum heap size to prevent out of memory errors\n    steps:                        # commands that comprise the `build` job\n      - checkout                  # check out source code to working directory\n      - restore_cache:            # restores saved cache if checksum hasn't changed since the last run\n          key: banking-on-clojure-webapp-{{ checksum \"deps.edn\" }}\n      - run: clojure -X:test/runner\n      - save_cache:               # generate and store cache in the .m2 directory using a key template\n          paths:\n            - ~/.m2\n            - ~/.gitlibs\n          key: banking-on-clojure-webapp-{{ checksum \"deps.edn\" }}\n      - run: bin/kaocha --reporter kaocha.report/documentation --no-randomize --no-color --plugin kaocha.plugin.alpha/spec-test-check\n

Enable 3rd Party Orbs

Enable 3rd Party Orbs in Organisation > Security settings

"},{"location":"projects/banking-on-clojure/create-records/","title":"Create database records","text":"

Several options were explored when designing database query functions. Using next.jdbc.sql functions provides a Clojure data structures approach, where as next.jdbc/execute! uses specific SQL statement code.

Take the SQL approach if generating SQL statements directly.

Take the Clojure approach if to generate SQL statements from Clojure data structures.

"},{"location":"projects/banking-on-clojure/create-records/#generic-create-record-function","title":"Generic create record function","text":"

Use the generic create function from the database schema design section

(defn create-record\n  \"Insert a single record into the database using a managed connection.\n  Arguments:\n  - table - name of database table to be affected\n  - record-data - Clojure data representing a new record\n  - db-spec - database specification to establish a connection\"\n  [db-spec table record-data]\n  (with-open [connection (jdbc/get-connection db-spec)]\n    (jdbc-sql/insert! connection table record-data)))\n
"},{"location":"projects/banking-on-clojure/create-records/#create-a-new-account_holder-record","title":"Create a new account_holder record","text":"

Call the create-record function with the development database specification, the account holder table name and a Clojure hash-map of the record data.

Each key in the map represents a column name and the value associated with the key is the value to be inserted in the record for its column.

(create-record db-specification-dev\n               \"public.account_holders\"\n               {:account_holder_id      (java.util.UUID/randomUUID)\n                :first_name             \"Rachel\"\n                :last_name              \"Rocketpack\"\n                :email_address          \"rach@rocketpack.org\"\n                :residential_address    \"1 Ultimate Question Lane, Altar IV\"\n                :social_security_number \"BB104312D\"})\n
"},{"location":"projects/banking-on-clojure/create-records/#create-account-record","title":"Create account record","text":"

Create a new record in the public.accounts table.

(create-record db-specification-dev\n               \"public.accounts\"\n               {:account_id        (java.util.UUID/randomUUID)\n                :account_number    \"1234567890\"\n                :account_sort_code \"102010\"\n                :account_name      \"Current\"\n                :current_balance   100\n                :last_updated      \"2020-09-11\"\n                :account_holder_id (java.util.UUID/randomUUID)})\n
"},{"location":"projects/banking-on-clojure/create-records/#create-transaction-record","title":"Create transaction record","text":"

Create a record in the public.transaction_history table.

(create-record db-specification-dev\n               \"public.transaction_history\"\n               {:transaction_id        (java.util.UUID/randomUUID)\n                :transaction_reference \"Salary\"\n                :transaction_date      \"2020-09-11\"\n                :account_number        \"1234567890\"})\n

Generating example data from Clojure Spec

Clojure Spec: generate mock database data

"},{"location":"projects/banking-on-clojure/cyclic-load-dependency/","title":"Cyclic Load Dependency","text":"

A cyclic load dependency is where one namespace requires one or more other namespaces that then require the original namespace, forming a loop when resolve all the required namespaces. When there are cyclic namespace dependencies a warning is returned when evaluating any of the namespaces involved.

A good way to spot cyclic load dependencies it to regularly run a test runner on the code, or even set up a test runner to watch for changes to the file system which then triggers an automatic test run. For example, kaocha --watch.

"},{"location":"projects/banking-on-clojure/cyclic-load-dependency/#tips-on-avoiding-cyclic-load-dependencies","title":"Tips on avoiding cyclic load dependencies","text":"
  • Add comment sections describing the purpose of the different parts of code, creating logical separation of code. Refactor of namespaces is far easier to see and helps ensure related code is kept together.

  • Ensure test and specification code is in its own namespace. Keeping the right level of abstraction in tests and clojure spec test.check can help avoid issues.

  • Use require to add dependent namespaces, preferably using a meaningful :as alias or using :refer for only the function names used. (:require ,,, :refer :all) or (use ,,) expressions can pull in too many other namespaces and cause issues

  • A cyclic load dependency error message is a good point to review and refactor the application design.

  • Where two namespaces are interdependent on each other, factor out shared code into a third namespace required by each of the other two. This breaks the dependency between the two original namespaces.

"},{"location":"projects/banking-on-clojure/cyclic-load-dependency/#example-banking-on-clojure","title":"Example - Banking on Clojure","text":"

The practicalli.banking-on-clojure.handler namespace contains functions that need to access data in the database, so the practicalli.database-access namespace is required.

(ns practicalli.banking-on-clojure.handler\n  (:require\n   ;; Web Application\n   [ring.util.response :refer [response]]\n   [hiccup.core :refer [html]]\n   [hiccup.page :refer [html5 include-js include-css]]\n   [hiccup.element :refer [link-to]]\n\n   ;; Data access\n   [practicalli.database-access :as data-access]))\n

The practicalli.database-access namespace required the practicalli.banking-on-clojure.specification namespace to use the specifications for generating data

(ns practicalli.database-access\n  (:require [next.jdbc :as jdbc]\n            [next.jdbc.sql :as jdbc-sql]\n            [next.jdbc.specs :as jdbc-spec]\n\n            [practicalli.banking-on-clojure.specification]))\n

In the practicalli.banking-on-clojure.specification namespace, a functional specification had been defined for the register-account-holder function, which required the practicalli.banking-on-clojure.handler to be required. Even though the specifications testing had logically moved to the database-access namespace, this functional specification remained.

(ns practicalli.banking-on-clojure.specification\n  (:require\n   ;; Clojure Specifications\n   [clojure.spec.alpha :as spec]\n   [clojure.spec.gen.alpha :as spec-gen]\n   [clojure.spec.test.alpha :as spec-test]\n\n   ;; Helper namespaces\n   [clojure.string]\n\n   [practicalli.banking-on-clojure.handler :as handler]))\n

This set of require expressions lead to a cyclic load dependency error.

-> indicate that a namespace requires the namespace it is pointing too.

Removing the require of practicalli.banking-on-clojure.handler from the practicalli.banking-on-clojure.specification namespace breaks the cyclic dependency.

"},{"location":"projects/banking-on-clojure/cyclic-load-dependency/#testing-the-database-on-ci","title":"Testing the database on CI","text":"

Need to create the tables before the tests can run. - update the schema so the create tables can run without failure, check if table exist and if not create it.

"},{"location":"projects/banking-on-clojure/database-queries/","title":"Defining Database Queries","text":"

Using the SQL statement for Inserting records as an example, several different approached are covered for defining database queries. The options are similar for update and delete queries.

All options use the with-open function to wrap the connection to the database, to automatically close that connection once the function has completed.

Approach Description next.jdbc/execute! function Simple approached used previously for creating tables Defining a generic function Pass SQL statements and connection into a single function, using def to define sql statements next.jdbc.sql/* functions Generate SQL statements from Clojure data structures, e.g. hash-maps, vectors, etc. Generic function with next.jdbc.sql/* functions Generic insert, update and delete functions that take a Clojure data structures"},{"location":"projects/banking-on-clojure/database-queries/#example-sql-queries-from-dbeaver","title":"Example SQL queries from DBeaver","text":"

Using the DBeaver tool the basic form of an insert command is generated from Generate SQL > DDL

INSERT INTO PUBLIC.ACCOUNT_HOLDERS\n (ACCOUNT_HOLDER_ID, FIRST_NAME, LAST_NAME, EMAIL_ADDRESS, RESIDENTIAL_ADDRESS, SOCIAL_SECURITY_NUMBER)\nVALUES(?, '', '', '', '', '');\n
"},{"location":"projects/banking-on-clojure/database-queries/#using-the-general-execute-command","title":"Using the general execute! command","text":"

Using the general jdbc/execute! is the same form as used previously to create, show and drop database tables.

(defn persist-account-holder\n  \"Persist a new account holder record\"\n  [account-holder-id db-spec]\n  (with-open [connection (jdbc/get-connection db-spec)]\n    (jdbc/execute!\n      connection\n      [(str \"insert into account_holders(\n               account_holder_id,first_name,last_name,email_address,residential_address,social_security_number)\n             values(\n               '\" account-holder-id \"', 'Jenny', 'Jetpack', 'jen@jetpack.org', '42 Meaning Lane, Altar IV', 'AB101112C' )\")])) )\n

Call the function with a randomly generated UUID value for the account_holder_id and the database details in the form of the development database specification.

(persist-account-holder (java.util.UUID/randomUUID) db-specification-dev)\n
"},{"location":"projects/banking-on-clojure/database-queries/#using-a-generic-function-approach","title":"Using a generic function approach","text":"

Write a Clojure function that takes in any SQL statement and executes that against a specific database specification.

(defn database-update\n  [sql-statement db-spec ]\n  (with-open [connection (jdbc/get-connection db-spec)]\n    (jdbc/execute! connection sql-statement)))\n

Refactor sql statements into their own vars, so they can be reused.

(def account-holder-jenny\n  [(str \"insert into account_holders(account_holder_id,first_name,last_name,email_address,residential_address,social_security_number)\n    values('\" (java.util.UUID/randomUUID) \"', 'Jenny', 'Jetpack', 'jen@jetpack.org', '42 Meaning Lane, Altar IV', 'AB101112C' )\")])\n

Update the database using the name of the SQL statement

(database-update account-holder-jenny db-specification-dev)\n

Limitation of def

The def expression must be evaluated each time a new value for the account_holder_id is required. The first time the def is evaluated, the java-util.UUID/randomUUID function is evaluated to a specific value and that value is cached.

Using the account-holder-jenny name in other code will use the cache until the def expression is forcefully evaluated (by the developer or by restarting the REPL).

"},{"location":"projects/banking-on-clojure/database-queries/#using-nextjdbc-friendly-functions","title":"Using next.jdbc friendly functions","text":"

Using next.jdbc.sql functions. For example:

(jdbc-sql/insert! ds :address {:name \"A. Person\" :email \"albert@person.org\"})\n

For the banking-on-clojure project this would take the form

(defn add-account-holder\n  [account-holder-id data-source]\n  (jdbc-sql/insert!\n    data-source\n    :table-name {:column-name \"value\" ,,,}))\n

In this example, the next.jdbc insert! function is used to add an account holder record.

(defn add-account-holder\n  [account-holder-id db-spec]\n  (with-open [connection (jdbc/get-connection db-spec)]\n    (jdbc-sql/insert!\n      connection\n      :public.account_holders {:account_holder_id      account-holder-id\n                               :first_name             \"Rachel\"\n                               :last_name              \"Rocketpack\"\n                               :email_address          \"rach@rocketpack.org\"\n                               :residential_address    \"1 Ultimate Question Lane, Altar IV\"\n                               :social_security_number \"BB104312D\"})))\n

Calling the function with generated data.

(add-account-holder (java.util.UUID/randomUUID) db-specification-dev)\n
"},{"location":"projects/banking-on-clojure/database-queries/#generic-insert-function-with-nextjdbcsql","title":"Generic insert function with next.jdbc.sql","text":"
(defn insert-record\n  [table record-data db-spec]\n  (with-open [connection (jdbc/get-connection db-spec)]\n    (jdbc-sql/insert! connection table record-data)))\n

The data to pass in looks familiar. Its the table name plus a data structure that looks like a specification for an account holder.

:public.account_holders\n\n{:account_holder_id      (java.util.UUID/randomUUID)\n :first_name             \"Rachel\"\n :last_name              \"Rocketpack\"\n :email_address          \"rach@rocketpack.org\"\n :residential_address    \"1 Ultimate Question Lane, Altar IV\"\n :social_security_number \"BB104312D\"}\n

So the specifications already defined can be used to generate mock data for the database.

"},{"location":"projects/banking-on-clojure/database-tables/","title":"Design and Create Database Tables","text":"

The design for Banking On Clojure database contains three tables, account_holders, accounts and transaction_history.

account_holders contains a unique entry for every customer in the bank.

accounts contains every account created for the bank. Each account has an account holder, a current balance and a date of when the current balance was last updated.

transaction_history contains every transaction that takes place in the bank. Each transaction is related to a specific account. The current balance for an account is built from all the transactions for a specific account. The number of transactions used to calculate the current balance is reduced by using the last_updated value from accounts.

"},{"location":"projects/banking-on-clojure/database-tables/#organising-the-code","title":"Organising the code","text":"

The SQL statements that create the database tables will be bound to a suitable name using def. The value will be a vector containing the string of the SQL statement.

The SQL statements are generated by the DBever database management tool. The tables are created in DBeaver and the DDL script is exported for each table and pasted in the Clojure code.

A create-tables helper function executes the given SQL statements on a specific data source, from within a transaction so that all table are either created or none are created.

db-specification-dev is a name bound to the database specification map for the development database. This should eventually end up as an aero configuration along with the production database specification.

(def db-specification-dev {:dbtype \"h2\" :dbname \"banking-on-clojure\"})\n
"},{"location":"projects/banking-on-clojure/database-tables/#define-account-holders-table","title":"Define ACCOUNT-HOLDERS table","text":"

Define the SQL statement to create a table to hold all the ACCOUNT-HOLDERS.

The design includes all the customer details plus from the Banking on Clojure specifications and the account_holder_id that uniquely identifies the customer.

(def schema-account-holders-table\n  [\"CREATE TABLE PUBLIC.ACCOUNT_HOLDERS(\n     ACCOUNT_HOLDER_ID UUID(16) NOT NULL,\n     FIRST_NAME VARCHAR(32),\n     LAST_NAME VARCHAR(32),\n     EMAIL_ADDRESS VARCHAR(32),\n     RESIDENTIAL_ADDRESS VARCHAR(255),\n     SOCIAL_SECURITY_NUMBER VARCHAR(32),\n     CONSTRAINT CONSTRAINT_3 PRIMARY KEY (ACCOUNT_HOLDER_ID))\"])\n

UUID inefficient for large data sets

Using random data, like uuid, for indexes can be inefficient especially for larger data sets. There may be data types for each specific database that provide more efficient ways of managing unique ids. For the scope of this project, using a UUID is acceptable.

"},{"location":"projects/banking-on-clojure/database-tables/#define-accounts-table","title":"Define ACCOUNTS table","text":"

Define the SQL statement to create the ACCOUNTS

Each account is associated with an ACCOUNT_HOLDER_ID so that all the accounts belonging to a customer can be easily found.

The current balance holds the value of credit in an account at the time of the last updated date. The current_balance is calculated from the values in the transaction_history and updates to the current_balance will update the last_updated value. This provides a very simplistic mechanism for quickly presenting the value of an account.

(def schema-accounts-table\n  [\"CREATE TABLE PUBLIC.ACCOUNTS(\n     ACCOUNT_ID UUID(16) NOT NULL,\n     ACCOUNT_NUMBER INTEGER NOT NULL AUTO_INCREMENT,\n     ACCOUNT_SORT_CODE VARCHAR(6),\n     ACCOUNT_NAME VARCHAR(32),\n     CURRENT_BALANCE VARCHAR(255),\n     LAST_UPDATED DATE,\n     ACCOUNT_HOLDER_ID VARCHAR(100) NOT NULL,\n     CONSTRAINT ACCOUNTS_PK PRIMARY KEY (ACCOUNT_ID))\"] )\n
"},{"location":"projects/banking-on-clojure/database-tables/#define-transaction_history-table","title":"Define TRANSACTION_HISTORY table","text":"

Define the SQL statement to create the ACCOUNTS

All transactions include a value of the transaction, a reference to explain the purpose of the transaction, a date the transaction occurred, the account the transaction comes from and the account the transaction goes to.

(def schema-transaction-history-table\n  [\"CREATE TABLE PUBLIC.TRANSACTION_HISTORY(\n     TRANSACTION_ID UUID(16) NOT NULL,\n     TRANSACTION_VALUE INTEGER NOT NULL,\n     TRANSACTION_REFERENCE VARCHAR(32),\n     TRANSACTION_DATE DATE,\n     ACCOUNT_FROM INTEGER,\n     ACCOUNT_TO INTEGER,\n     CONSTRAINT TRANSACTION_HISTORY_PK PRIMARY KEY (TRANSACTION_ID))\"])\n

Constraint Naming

Constraints are used to add Primary Keys to database tables. Each constraint needs an identifier which is included in error reporting when there are issues. It is recommended to use meaningful names for identifiers to trace the source of errors and also support maintenance of the overall database design.

"},{"location":"projects/banking-on-clojure/database-tables/#execute-table-schema-in-the-development-database","title":"Execute Table schema in the development database","text":"

Define a function to use the table creation SQL statements and execute them on the given database.

The function uses with-open to create and manage a connection, closing that connection when the function has completed.

(defn create-table\n  \"Establish a connection to the data source and create a table within a transaction.\n  Close the database connection.\n  Arguments:\n  - table-schemas: a vector containing an sql statements to create a table\"\n  [table-schema data-spec]\n\n  (with-open [connection (jdbc/get-connection data-spec)]\n    (jdbc/execute! connection  table-schemas)))\n

with-open - managing resources

with-open ensures that resources get closed and clearly defines the scope of the using the resource.

This helps the developer avoid using Clojure\u2019s lazy sequences in a with-open block. Within the scope of the with-open expression it is important to make sure that the result is eagerly evaluated to avoid accessing the resource after it\u2019s closed, or fall foul of the \"ResultSet closed\" or \"transaction closed\" errors.

Refactor the function to execute all the SQL statements in a transaction, so either all the databases are created or none are.

Using transactions can help prevent databases becoming in an inconsistent state due to only partial completion of a set of SQL statements.

(defn create-tables\n  \"Establish a connection to the data source and create all tables within a transaction.\n  Close the database connection.\n  Arguments:\n  - table-schemas: a vector of sql statements, each creating a table\"\n  [table-schemas data-spec]\n\n  (with-open [connection (jdbc/get-connection data-spec)]\n    (jdbc/with-transaction [transaction connection]\n      (doseq [sql-statement table-schemas]\n        (jdbc/execute! transaction sql-statement) ))))\n

Refactor for connection pools

with-open function can be removed from the create-tables function when using a connection pool, passing the existing connection from the pool to the jdbc/with-transaction function.

Calling the create-tables function will create the database tables in the development database.

The H2 database writes the tables to disk in the banking-on-clojure.mv.db file. Unless the table is dropped, there is no need to evaluate this function again.

"},{"location":"projects/banking-on-clojure/database-tables/#viewing-tables-in-the-database","title":"Viewing tables in the database","text":"
(defn information-tables\n  [data-source]\n  (jdbc/execute!\n    data-source\n    [\"select * from information_schema.tables\"])\n

Create a helper function to show the schema of any particular table. The function takes a table name and using str to combine table name with the rest of the SQL statement.

(defn show-schema\n  [table-name]\n  (jdbc/execute!\n    data-source\n    [(str \"show columns from \" table-name)]))\n

A specific schema can be viewed by calling the show-schema function

(show-schema \"accounts\")\n

Refactor the show-schema function to take the data source as an argument as well as the table name. The function is then usable for development, staging and production data sources (although its not generally advisable to update production database from a REPL in the development environment once its live)

(defn show-schema\n  [data-source table-name]\n  (jdbc/execute! data-source [(str \"show columns from \" table-name)]))\n

The connection is not manged though, so refactor again and add the with-open command to ensure the connection is closed once the function has finished.

(defn show-schema\n  [db-spec table-name]\n  (with-open [connection (jdbc/get-connection db-spec)]\n    (jdbc/execute! connection [(str \"SHOW COLUMNS FROM \" table-name)])))\n
"},{"location":"projects/banking-on-clojure/database-tables/#removing-dropping-database-tables","title":"Removing (dropping) database tables","text":"

A helper function for removing tables from the database, dropping the data and the schema of the table.

drop-table function only differs from the show-schema function in the specific SQL statement it uses.

(defn drop-table\n  [db-spec table-name]\n  (with-open [connection (jdbc/get-datasource db-spec)]\n    (jdbc/execute! connection [(str \"DROP TABLE \" table-name)])))\n

A generic helper function could be used if the SQL statement were also an argument.

"},{"location":"projects/banking-on-clojure/database-tables/#managing-database-tables-from-the-repl","title":"Managing database tables from the REPL","text":"

When designing the database schema it can be useful to iterate quickly around the design. Using a Rich Comment Block to hold expressions to create show and drop tables is an effective way to manage the database schema quickly.

(comment  ;; Managing Schemas\n\n  ;; Create all tables in the development database\n  (create-tables [schema-account-holders-table schema-accounts-table schema-transaction-history-table]\n                 db-specification-dev)\n\n  ;; View application table schema in development database\n  (show-schema data-source-dev \"PUBLIC.ACCOUNT_HOLDERS\")\n  (show-schema data-source-dev \"PUBLIC.ACCOUNTS\")\n  (show-schema data-source-dev \"PUBLIC.TRANSACTION_HISTORY\")\n\n  ;; View database system schema in development database\n  (show-schema data-source-dev \"INFORMATION_SCHEMA.TABLES\")\n\n  ;; Remove tables from the development database\n  (drop-table data-source-dev \"PUBLIC.ACCOUNT_HOLDERS\")\n  (drop-table data-source-dev \"PUBLIC.ACCOUNTS\")\n  (drop-table data-source-dev \"PUBLIC.TRANSACTION_HISTORY\"))\n

Manage database schema with Migratus

Migratus provides an elegant approach to evolving database schema

Common errors

Syntax error in SQL statement can occur if the SQL statement is not correct, the most common cause is a missing comma.

Databases do not always support exactly the same SQL syntax, especially around types and more advanced features. SQL statements may not work exactly the same for each database. Using tools like DBever will generated SQL expressions for specific databases.

"},{"location":"projects/banking-on-clojure/delete-records/","title":"Delete Records in the database","text":"

Using next.jdbc.sql functions provides a Clojure data structures approach, where as next.jdbc/execute! uses specific SQL statement code.

{% tabs clojure=\"next.jdbc.sql functions\", sql=\"next.jdbc/execute!\" %}

{% content \"clojure\" %}

"},{"location":"projects/banking-on-clojure/delete-records/#generic-delete-record-function","title":"Generic delete record function","text":"

Use the generic delete function from the database schema design section

(defn delete-record\n  \"Insert a single record into the database using a managed connection.\n  Arguments:\n  - table - name of database table to be affected\n  - record-data - Clojure data representing a new record\n  - db-spec - database specification to establish a connection\"\n  [db-spec table where-clause]\n  (with-open [connection (jdbc/get-connection db-spec)]\n    (jdbc-sql/delete! connection table where-clause)))\n
"},{"location":"projects/banking-on-clojure/delete-records/#delete-an-existing-account_holder-record","title":"Delete an existing account_holder record","text":"

Call the delete-record function with the development database specification, the table name and a where clause to locate the specific record to delete. This where clause should use a unique value, e.g. the primary key for the table.

  (delete-record db-specification-dev :public.account_holders {:account_holder_id \"0bed6afe-6740-46a1-b924-36ef192eac66\"})\n

If the record deletion is successful then :update-count 1 value is returned

  ;; => #:next.jdbc{:update-count 1}\n
"},{"location":"projects/banking-on-clojure/delete-records/#deleting-an-existing-account-record","title":"Deleting an existing account record","text":"

Update an existing record in the public.accounts table, providing new values for current_balance and last_updated columns.

(delete-record db-specification-dev :public.accounts {:account_number \"1234567890\"})\n
"},{"location":"projects/banking-on-clojure/delete-records/#deleting-an-existing-transaction-record","title":"Deleting an existing transaction record","text":"

Update an existing record in the public.transaction_history table.

(delete-record db-specification-dev :public.transaction_history {:transaction_id  \"8ac89cfc-6874-4ebe-9ee4-59b8c5e971ff\"})\n

{% content \"sql\" %}

"},{"location":"projects/banking-on-clojure/delete-records/#insert-account_holders","title":"Insert account_holders","text":""},{"location":"projects/banking-on-clojure/delete-records/#insert-accounts","title":"Insert accounts","text":""},{"location":"projects/banking-on-clojure/delete-records/#insert-transactions","title":"Insert transactions","text":"

{% endtabs %}

"},{"location":"projects/banking-on-clojure/delete-records/#hintgenerating-example-data-from-clojure-spec","title":"Hint::Generating example data from Clojure Spec","text":"

Clojure Spec: generate mock database data

"},{"location":"projects/banking-on-clojure/deployment-pipeline/","title":"Deployment Pipeline Approach","text":"

Using the Heroku Application platform cloud simplifies the deployment of the Clojure web application.

"},{"location":"projects/banking-on-clojure/deployment-pipeline/#12-factor-approach","title":"12 Factor approach","text":"

Following the 12 factor principles, the deployment is driven by source code to multiple environments.

"},{"location":"projects/banking-on-clojure/deployment-pipeline/#heroku-pipelines","title":"Heroku pipelines","text":"

Using Heroku Pipelines the staging environment is promoted to production rather than being rebuilt

The Heroku dashboard can be used to promote the application into production, once the staging application is signed off.

"},{"location":"projects/banking-on-clojure/deployment-pipeline/#heroku-build-process","title":"Heroku Build process","text":"

The build process starts when commits are pushed to Heroku, either directly or via a continuous integration service (eg. CircleCI).

"},{"location":"projects/banking-on-clojure/deployment-via-ci/","title":"Deployment via Continuous Integration","text":"

Deployment will be via a workflow to the CircleCI configuration that deploys the application to a staging environment on successful completion of running all tests in the project. Once the staging application is approved, the application build can be promoted to production.

"},{"location":"projects/banking-on-clojure/deployment-via-ci/#add-heroku-orb-to-circleci-configuration","title":"Add Heroku orb to CircleCI configuration","text":"

Edit .circleci/config.yml and add the heroku orb and a workflow to Heroku. The workflow has a dependency on the build job, so that will take place first.

The Heroku workflow will build the application from source code using the heroku/deploy-via-git. Only changes pushed to the live branch of the GitHub repository will be used in the Heroku deploy workflow.

version: 2.1  # circleci configuration version\n\norbs:\n  kaocha: lambdaisland/kaocha@0.0.1 # Org settings > Security > uncertified orbs\n  heroku: circleci/heroku@1.2.6 # Invoke the Heroku orb\n\nworkflows:\n  heroku_deploy:\n    jobs:\n      - build\n      - heroku/deploy-via-git: # Use the pre-configured job, deploy-via-git\n          requires:\n            - build\n          filters:\n            branches:\n              only: live\n\njobs:    # basic units of work in a run\n  build: # runs not using Workflows must have a `build` job as entry point\n    working_directory: ~/build # directory where steps will run\n    docker:                                                      # run the steps with Docker\n      - image: circleci/clojure:openjdk-11-tools-deps-1.10.1.727 # image is primary container where `steps` are run\n    environment:            # environment variables for primary container\n      JVM_OPTS: -Xmx3200m   # limit the maximum heap size to prevent out of memory errors\n    steps:             # commands that comprise the `build` job\n      - checkout       # check out source code to working directory\n      - restore_cache: # restores saved cache if checksum hasn't changed since the last run\n          key: banking-on-clojure-webapp-{{ checksum \"deps.edn\" }}\n      - run: clojure -R:test:runner -Spath\n      - save_cache:    # generate and store cache in the .m2 directory using a key template\n          paths:\n            - ~/.m2\n            - ~/.gitlibs\n          key: banking-on-clojure-webapp-{{ checksum \"deps.edn\" }}\n      - run: bin/kaocha --reporter kaocha.report/documentation --no-randomize --no-color --plugin kaocha.plugin.alpha/spec-test-check\n
"},{"location":"projects/banking-on-clojure/deployment-via-ci/#add-depstar-to-build-an-uberjar","title":"Add depstar to build an uberjar","text":"

The depstar tool creates a Java archive (jar) package of the application. The deps.edn configuration in the root of the project already contains an uberjar alias for this tool.

Check the project builds the uberjar locally:

clojure -X:project/uberjar\n

This will be the same command used in the build script

"},{"location":"projects/banking-on-clojure/deployment-via-ci/#create-a-custom-build-behaviour","title":"Create a custom build behaviour","text":"

Heroku build scripts use Leiningen by default. Configure Heroku to build with Clojure Tools, create a custom build file which will run instead of Leiningen.

Create a file called bin/build script in the root of the project

#!/usr/bin/env bash\nclojure -X:project/uberjar\n

Create an empty project.clj file so that Heroku recognized the project as Clojure.

"},{"location":"projects/banking-on-clojure/deployment-via-ci/#define-how-to-run-the-application","title":"Define how to run the application","text":"

Create a Procfile file in the root of the project directory containing the command to run the application.

Use the $PORT as an argument to the command. Heroku automatically assigns a port number for an application to listen upon when creating a contain in which the application will run. This port number is set using the PORT environment variable and is available to the application on startup. Using the PORT environment variable ensures the Clojure application will receive requests.

web: java -jar banking-on-clojure.jar $PORT\n
"},{"location":"projects/banking-on-clojure/deployment-via-ci/#specifying-a-java-version","title":"Specifying a Java version","text":"

Create a system.properties and specify the Java version to use for the application. Java 1.8 is the default version use on Heroku, however, our development environment is Java 11, so add a property to set the Java runtime to version 11.

java.runtime.version=17\n
"},{"location":"projects/banking-on-clojure/deployment-via-ci/#circleci-environment-variables","title":"CircleCI Environment Variables","text":"

Open the CircleCI and select project settings > Environment Variables

Add environment variables to define where the Heroku application can be found and a token to provide access.

Environment Variable Value HEROKU_API_KEY name of the application created on Heroku HEROKU_APP_NAME API key found in Account Settings > API Key"},{"location":"projects/banking-on-clojure/deployment-via-ci/#heroku-pipeline-configuration","title":"Heroku Pipeline configuration","text":"

Login to the Heroku dashboard and create a new pipeline called banking-on-clojure-webapp

In the Heroku dashboard, open the application Settings and add a Config Vars using the name CLOJURE_CLI_VERSION with a value of 1.10.1.727

Using Heroku Pipelines the staging environment is promoted to production rather than being rebuilt

The Heroku dashboard can be used to promote the application into production, once the staging application is signed off.

"},{"location":"projects/banking-on-clojure/deployment-via-ci/#push-changes-to-trigger-build","title":"Push changes to trigger build","text":"

Commit the changes and push them to the GitHub repository.

git push heroku live:main\n

Heroku only deploys code pushed to the main (or master) branch of the remote. Pushing code to another branch of the heroku remote has no effect. Using the live:local form will push the local live branch to the remote main branch on Heroku.

This triggers a build by CircleCI. The build downloads the dependencies and runs the unit tests. If the tests pass, then the Heroku deploy workflow starts.

The two stages can be seen in the dashboard as the pipeline runs.

Now visit the deployed Heroku application to see it in action.

"},{"location":"projects/banking-on-clojure/deployment-via-ci/#troubleshooting","title":"Troubleshooting","text":"

If there are issues, then use the Heroku toolbelt to look at the logs. In a command line terminal, issue the login command which opens a web browser to login to Heroku. Once logged in, run the heroku logs command to view the latest logs

heroku login\n\nheroku logs --app banking-on-clojure\n

The logs can also be viewed live, as the application is being deployed by including the --tail option when running the heroku logs command in a terminal

heroku logs --app banking-on-clojure --tail\n

The example Heroku logs show that the banking-on-clojure is using the default port number if non is supplied as an argument, rather than Heroku assigned port. Heroku therefore considers the application as unresponsive and sets it status to crashed, tearing down the container the application is running in.

These logs were generated before adding the $PORT to the command in the Procfile.

"},{"location":"projects/banking-on-clojure/deployment-via-ci/#no-forced-pushes","title":"No forced pushes","text":"

Heroku doesn't like force Git pushes coming via CircleCI.

To get around this, either don't do force pushes to GitHub, or add the Heroku repository for the project as a remote to local git repository.

Heroku repository details in heroku dashboard Settings under App Information

Changes can now be pushed, ideally using force-with-lease to Heroku repository.

"},{"location":"projects/banking-on-clojure/deployment-via-ci/#stopping-the-application","title":"Stopping the application","text":"

An application can be run for free on Heroku with the monthly free credits provided. However, to make the most out of these free credits then applications not in use should be shut down

Run the following command in the root of the Clojure project.

heroku ps:stop banking-on-clojure\n
"},{"location":"projects/banking-on-clojure/development-database/","title":"Provision the development database","text":"

To keep the development environment self-contained the H2 in-memory database will be used for development of the application. Postgresql database will be used for testing and live environments hosted in the Cloud.

"},{"location":"projects/banking-on-clojure/development-database/#add-h2-database-library-dependency","title":"Add H2 database library dependency","text":"

Edit the deps.edn file in the root of the project directory. In the :deps hash-map, add next.jdbc library as a main dependency.

As the H2 database is only used for development create a :dev alias include an :extra-deps section for the H2 driver

{:deps\n {org.clojure/clojure        {:mvn/version \"1.10.1\"}\n  org.seancorfield/next.jdbc {:mvn/version \"1.1.569\"}}}\n\n{:aliases\n  {:dev\n   {:extra-deps {com.h2database/h2 {:mvn/version \"1.4.200\"}}}}}\n
"},{"location":"projects/banking-on-clojure/development-database/#create-a-namespace-for-database-access","title":"Create a namespace for database access","text":"

Create a new file in the project called src/practicalli/database-access.clj which will contain the code for accessing the database.

Require the next.jdbc namespace using the jdbc alias.

(ns practicalli.database-access\n  (:require [next.jdbc :as jdbc]))\n

next.jdbc will use the database specification to look up the driver namespace for the specific database. Therefore the database driver namespaces do not need to be explicitly required in the namepace.

"},{"location":"projects/banking-on-clojure/development-database/#specifying-the-database-and-connection","title":"Specifying the database and connection","text":"

H2 in-memory database is used as a self-contained database, providing a simple way to start evaluating the schema and queries as they are designed.

Use next.jdbc library to define a database specification, represented as a map. For the H2 database only the database type and database name are required. No roles or credentials are used to access the database as it is only running locally.

Use the database specification to create a connection

;; Database specification and connection\n\n;; Development environment\n;; H2 in-memory database\n(def db-specification-dev {:dbtype \"h2\" :dbname \"banking-on-clojure\"})\n\n;; Database connection\n(def data-source-dev (jdbc/get-datasource db-specification-dev))\n

The database specification is used to create a database connection. A general name can be used here as only one database will be used for one environment.

Aero for multiple environment configuration

juxt/aero is a library for managing configurations across multiple environments in a single EDN file. aero can be used to hold the details of each database specification for every environment (dev, staging, live).

(read-config (clojure.java.io/resource \"config.edn\")) with the configuration file in the resources directory of the classpath. This is accessible from the Jar and the REPL.

"},{"location":"projects/banking-on-clojure/development-database/#using-connections-effectively","title":"Using connections effectively","text":"

Use the with-open function to manage the database connections and ensure the connects are closed after the sql queries complete.

(with-open [connection (jdbc/get-connection data-source-dev)]\n  (jdbc/execute! connection [\"SQL statement\"]))\n

When multiple SQL queries should be run together, the with-open function enables reuse of the connection and ensure the connection is cleaned up once the SQL statements are complete.

(with-open [connection (jdbc/get-connection data-source-dev)]\n  (jdbc/execute! connection [\"SQL statement\"])\n  (reduce my-fn init-value (jdbc/plan connection [\"SQL statement\"]))\n  (jdbc/execute! connection [\"SQL statement\"]))\n

Close JDBC Connections

next.jdbc uses raw Java JDBC types so it is important to close connections to avoid issues.

"},{"location":"projects/banking-on-clojure/generate-data-from-specs/","title":"Generate Data Using Clojure Spec","text":"

Test data to populate the database can be generated using the specifications previously defined using Clojure Spec.

"},{"location":"projects/banking-on-clojure/honeysql/","title":"HoneySQL","text":"

HoneySQL is a library for writing SQL as Clojure data structures to programmatically query databases (develop and runtime) without string bashing.

"},{"location":"projects/banking-on-clojure/honeysql/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"projects/banking-on-clojure/instrument-next-jdbc-functions/","title":"clojure.spec: Instrument next.jdbc functions","text":"

Clojure specifications are available for all next.jdbc functions contained in namespaces next.jdbc, next.jdbc.connection, next.jdbc.prepare, and next.jdbc.sql.

Instrumenting the functions with their specifications will check the arguments passed to a function conform to the appropriate specification. If the arguments conform, the next.jdbc function will evaluate with those arguments. If the arguments do not conform, then an error is returned.

Instrumenting specifications provided additional details when errors occur, helping diagnose the issue quickly.

"},{"location":"projects/banking-on-clojure/instrument-next-jdbc-functions/#require-the-nextjdbc-specifications","title":"Require the next.jdbc specifications","text":"

Require the next.jdbc.specs namespace in the project, typically in the namespace where next.jdbc is also required.

(ns practicalli.database-access\n  (:require\n    [next.jdbc :as jdbc]\n    [next.jdbc.specs :as jdbc-spec]))\n

In a Rich Comment block, call the instrument function from next.jdbc.specs namespace. This will instrument the specifications for all functions across all the next.jdbc namespaces.

(comment\n\n  (jdbc-spec/instrument)\n)\n
Instrumented functions are typically used during development, not in staging or production. Only calling instrument manually from a rich comment block ensures the developer controls when functions are instrumented.

"},{"location":"projects/banking-on-clojure/instrument-next-jdbc-functions/#runtime-checking","title":"Runtime checking","text":"

With instrumentation enabled, any calls to next.jdbc functions will have the arguments checked to ensure they conform to the specification.

For example, the instrumented execute! function will generate an error if passed an SQL statement as a string, rather than a vector containing a string.

(jdbc/execute! data-source \"SELECT * FROM account_holders\")\n\nCall to #'next.jdbc/execute! did not conform to spec.\n

The :problems section of the instrumented function error includes the :path [:sql :sql-params] and :pred vector? for the :val \"SELECT * FROM account_holders\".

Without the instrumented specification, assistance, the less helpful error message ClassCastException is the only assistance when debugging the issue.

Example from Banking on Clojure

"},{"location":"projects/banking-on-clojure/instrument-next-jdbc-functions/#turning-off-instrumentation-for-nextjdbc","title":"Turning off instrumentation for next.jdbc","text":"

unstrument function removes the instrumentation from the functions. Typically this is called from a rich comment block too, as its not common to run instrumented functions outside of the development environment.

(comment\n\n  (jdbc-spec/instrument)\n  (jdbc-spec/unstrument)\n\n)\n
"},{"location":"projects/banking-on-clojure/namespace-design/","title":"Namespace design","text":"

A common approach to namespace design is to start with the main namespace for the application and migrate code to new namespaces as the codebase grows.

"},{"location":"projects/banking-on-clojure/namespace-design/#basic-principles","title":"Basic Principles","text":"

Basic principles of namespace design include

  • focus namespaces on specific logical areas of the application
  • avoid circular references between namespaces (i.e. two namespaces require each other)
  • abstract code into namespaces to avoid the uber-namespace (unless the application fits into ~100 lines of code)
  • require the minimum number of namespaces
  • use meaningful names for namespace aliases (if naming is hard, think again about splitting a namespace)
  • use comment sections to separate code into logical groupings as its developed, highlighting potential sections of code that could split into its own namespace.
"},{"location":"projects/banking-on-clojure/namespace-design/#example-web-application-namespace-design","title":"Example web application namespace design","text":"

A general design that forms the basis of many web application projects

"},{"location":"projects/banking-on-clojure/namespace-design/#main-application-namespace","title":"Main application namespace","text":"

The main application namespace is typically used for code that manages the system, for example starting the application server, database, etc. These services are often managed by component lifecycle services (mount, integrant, component).

Routing is usually a part of the main application namespace, especially when there are a modest number of routes and a routing library such as compojure is used. If routing becomes more extensive, then a separate routing namespace is warranted.

"},{"location":"projects/banking-on-clojure/namespace-design/#handlers-and-custom-middleware","title":"Handlers and custom middleware","text":"

Handlers define the business logic, data and presentation that turns requests into responses. Start with a single namespace for handlers and segregate if the complexity grows sufficiently.

Middleware used directly with handlers is required in the handler namespace.

Middleware for the overall system may appear in the main application namespace, to wrap the application instance.

"},{"location":"projects/banking-on-clojure/namespace-design/#ui-pages-and-templates","title":"UI pages and templates","text":"

Avoid adding complexity to the handlers by moving common web page / html generation code to its own namespace.

A single namespace provides a focused view for refactoring presentation code into templates and generators.

"},{"location":"projects/banking-on-clojure/namespace-design/#data-queries","title":"Data queries","text":"

A namespace to design all the queries for a data source, which could be database, api's, file systems, etc.

SQL queries to relational database are defined here.

"},{"location":"projects/banking-on-clojure/namespace-design/#data-sources","title":"Data sources","text":"

Details of data sources, from databases, api's or any sources of information to be processed.

"},{"location":"projects/banking-on-clojure/production-database/","title":"Production Database - Heroku Postgres","text":"

Heroku provides a on-demand PostgreSQL service with very good support tooling. The Hobby plan is free to use with a free Heroku account (no credit card needed).

PostgreSQL provides a production grade relational database with support for JSON an other common data types. As its such a feature rich database

PostgreSQL instance runs outside the application code (unlike H2 database).

"},{"location":"projects/banking-on-clojure/production-database/#provision-a-database","title":"Provision a database","text":"

Login to Heroku using your free account.

Two Heroku apps have already been created for the banking on Clojure project pipeline, staging and live, so a database should be provisioned for both apps.

Select the staging app from the pipeline dashboard.

In the Overview section, click Configure Add-ons

Start typing Postgres in the Add-ons text box to see the matching add-ons available. Select Heroku Postgres.

Select the Hobby plan in the pop-up window. The Hobby plan is free and limited to 10,000 rows. The plan can be upgraded ones the app starts making money (or funding is raised).

The Postgres database is immediately provisioned and available for use.

"},{"location":"projects/banking-on-clojure/production-database/#database-configuration-on-heroku","title":"Database Configuration on Heroku","text":"

Provisioning an Heroku postgres database adds a DATABASE_URL Config Var (Heroku Environment Variable) to the application the database is attached to.

In the Heroku dashboard, view the Settings and select Show Config Vars

Click on the pencil icon to see the full connection string, which takes the following form:

postgres://username:password@host:port/database-name\n

This is not a correct JDBC connection string, but it can be used to generate one.

"},{"location":"projects/banking-on-clojure/production-database/#generate-the-jdbc-connection","title":"Generate the JDBC connection","text":"

Use the Heroku CLI tool to get the JDBC connection string for the database.

For the Banking on Clojure app, the following

heroku run echo \\$JDBC_DATABASE_URL --app banking-on-clojure-staging\n

This returns the correct JDBC connection string in the form:

\"jdbc:postgresql://<hostname>:port/<database-name>?user=<username>&password=<password>&sslmode=require\"\n

This jdbc connection string is generated from the DATABASE_URL config var that is added to the heroku app when a database is provisioned.

There are several applications attached to the Git repository, so its required to specify which application to run. The run command runs a container for the app, a Linux system that has the database attached. Once the echo command is complete the container is shut down and discarded automatically.

"},{"location":"projects/banking-on-clojure/production-database/#viewing-the-database-details-on-heroku-dashboard","title":"Viewing the database details on Heroku dashboard","text":"

This will switch over to the data.heroku.com website, so you may be prompted to login again.

"},{"location":"projects/banking-on-clojure/production-database/#adding-postgresql-driver-to-clojure-project","title":"Adding Postgresql driver to Clojure project","text":"

Add the latest postgresql jdbc driver to the deps.edn file in the banking on clojure project

 :deps\n {org.clojure/clojure {:mvn/version \"1.10.1\"}\n\n  ;; Web Application\n  http-kit        {:mvn/version \"2.3.0\"}\n  ring/ring-core  {:mvn/version \"1.8.1\"}\n  ring/ring-devel {:mvn/version \"1.8.1\"}\n  compojure       {:mvn/version \"1.6.1\"}\n  hiccup          {:mvn/version \"2.0.0-alpha2\"}\n\n  ;; Database\n  org.seancorfield/next.jdbc    {:mvn/version \"1.1.569\"}\n  com.h2database/h2         {:mvn/version \"1.4.200\"}\n  org.postgresql/postgresql {:mvn/version \"42.2.16\"}}\n

The Postgresql jdbc driver library will be used by next.jdbc

"},{"location":"projects/banking-on-clojure/production-database/#creating-a-staging-data-source","title":"Creating a staging data source","text":"

Add a JDBC_DATABASE_URL environment variable to hold the JDBC connection string to the Heroku database

(def data-source-postgresql\n    (jdbc/get-datasource (System/getenv \"JDBC_DATABASE_URL\")) )\n

Now the same SQL queries created for the H2 database can be tested on with PostgreSQL.

"},{"location":"projects/banking-on-clojure/production-database/#testing-queries-with-postgresql","title":"Testing queries with PostgreSQL","text":"

In practice is seems there are noticeable differences between H2 and PostgreSQL, especially in terms of schema definitions.

For example, to create a table the table namespace should be supplied, in this case public. The table creation syntax also as an IF clause, so if the database table already exists then the SQL statement does not try to create it and cause an error.

(jdbc/execute!\n    data-source-postgresql\n    [\"CREATE TABLE IF NOT EXISTS public.account_holder (\n      user_id serial PRIMARY KEY,\n      username VARCHAR ( 50 ) UNIQUE NOT NULL,\n      password VARCHAR ( 50 ) NOT NULL,\n      email VARCHAR ( 255 ) UNIQUE NOT NULL,\n      created_on TIMESTAMP NOT NULL,\n      last_login TIMESTAMP\"])\n

The PostgreSQL syntax for creating tables is:

  CREATE TABLE [IF NOT EXISTS] table_name (\n  column1 datatype(length) column_constraint,\n  column2 datatype(length) column_constraint,\n  column3 datatype(length) column_constraint,\n  table_constraints);\n
"},{"location":"projects/banking-on-clojure/production-database/#hintuse-dbeaver-to-generate-sql","title":"Hint::Use DBeaver to generate SQL","text":"

DBeaver is a free and comprehensive database tool that will generate SQL statements from database designs.

"},{"location":"projects/banking-on-clojure/production-database/#resources","title":"Resources","text":"
  • Heroku Postgres Credentials
  • Heroku: Database Connection Pooling with Clojure
"},{"location":"projects/banking-on-clojure/read-records/","title":"Read Database Records","text":"

Using next.jdbc.sql functions provides a Clojure data structures approach, where as next.jdbc/execute! uses specific SQL statement code.

"},{"location":"projects/banking-on-clojure/read-records/#generic-read-record-function","title":"Generic read record function","text":"

Use the generic create function from the database schema design section

(defn read-record\n  \"Insert a single record into the database using a managed connection.\n  Arguments:\n  - table - name of database table to be affected\n  - record-data - Clojure data representing a new record\n  - db-spec - database specification to establish a connection\"\n  [db-spec sql-query]\n  (with-open [connection (jdbc/get-connection db-spec)]\n    (jdbc-sql/query connection sql-query)))\n
"},{"location":"projects/banking-on-clojure/read-records/#read-account_holder-records","title":"Read account_holder records","text":"

Call the read-record function with the development database specification and a Clojure vector containing a string of the SQL select statement.

Return all the records from a specific table

  (read-record db-specification-dev [\"select * from public.account_holders\"])\n

Return records that match a specific where clause

  (read-record db-specification-dev [\"select * from public.account_holders where first_name = ?\" \"Rachel\"])\n
"},{"location":"projects/banking-on-clojure/read-records/#read-account-records","title":"Read account records","text":"

Create a new record in the public.accounts table.

Return all the records from a specific table

  (read-record db-specification-dev [\"select * from public.accounts\"])\n

Return records that match a specific where clause

  (read-record db-specification-dev [\"select * from public.accounts where account_number = ?\" \"1234567890\"])\n
"},{"location":"projects/banking-on-clojure/read-records/#read-transaction-history-records","title":"Read transaction history records","text":"

Create a record in the public.transaction_history table.

  (read-record db-specification-dev [\"select * from public.transaction_history\"])\n

Return records that match a specific where clause

  (read-record db-specification-dev [\"select * from public.transaction_history where transaction_date = ?\" \"2020-09-11\"])\n

Generating example data from Clojure Spec

Clojure Spec: generate mock database data

"},{"location":"projects/banking-on-clojure/refactor-handler/","title":"Refactor to handlers namespace","text":"

Create a new namespace called practicalli.banking-on-clojure.handler which will contain handler functions for the routes to be defined in the application.

Additional libraries will be used to create the responses, which will only be required in the new namespace.

Create a new file called src/practicalli/banking_on_clojure/handler.clj

Move the [ring.util.response :refer [response]] require and the handler function from src/practicalli/banking_on_clojure/service.clj to src/practicalli/banking_on_clojure/handler.clj

src/practicalli/banking_on_clojure/handler.clj
(ns practicalli.banking-on-clojure.handler\n  \"Handler functions to satisfy requests to the service\"\n  (:require\n    [ring.util.response :refer [response]]))\n
src/practicalli/banking_on_clojure/handler.clj
(defn welcome-page\n  \"Main page layout for the service\"\n  [request]\n  (response \"Banking on Clojure\"))\n

Require the practicalli.banking-on-clojure.handler namespace in practicalli.banking-on-clojure namespace, using the alias handler

src/practicalli/banking_on_clojure/service.clj
(ns practicalli.banking-on-clojure\n  (:gen-class)\n  (:require [org.httpkit.server :as app-server]\n            [compojure.core :refer [defroutes GET]]\n            [ring.util.response :refer [response]]\n            [practicalli.handler :as handler]))\n

Update the request routing code to use the new alias for handlers

src/practicalli/banking_on_clojure/service.clj
(defroutes app\n  (GET \"/\" [] handler/welcome-page))\n

Restart the server to pick up the changes

src/practicalli/banking_on_clojure/service.clj
(app-server-restart \"8888\")\n

Check the server is still working by visiting http://localhost:8888/

"},{"location":"projects/banking-on-clojure/spec-generative-testing/","title":"Generative testing","text":"

Specifications define the shape of data used for the application. The specifications are defined across two namespaces, general data specifications in practicalli.specifications and banking specific specs in the practicalli.specifications-banking namespace.

Basic customer details

(spec/def ::first-name string?)\n(spec/def ::last-name string?)\n(spec/def ::email-address string?)\n\n;; residential address values\n(spec/def ::house-name-number (spec/or :string string?\n                                       :number int?))\n(spec/def ::street-name string?)\n(spec/def ::post-code string?)\n(spec/def ::county string?)\n

countries of the world as a set, containing a string for each country defined in the practicalli.specifications namespace

(spec/def ::country :practicalli.specifications/countries-of-the-world)\n
(spec/def ::residential-address (spec/keys :req [::house-name-number ::street-name ::post-code]\n                                           :opt [::county ::country]))\n
(spec/def ::social-security-id-uk string?)\n(spec/def ::social-security-id-usa string?)\n\n(spec/def ::social-security-id (spec/or ::social-security-id-uk\n                                        ::social-security-id-usa))\n
;; composite customer details specification\n(spec/def ::customer-details\n  (spec/keys\n    :req [::first-name ::last-name ::email-address ::residential-address ::social-security-id]))\n
"},{"location":"projects/banking-on-clojure/spec-generative-testing/#banking-data-specifications","title":"Banking data specifications","text":"

The specifications-banking sets the overall context for the specifications defined in the namespace.

account-id is a unique identification across all accounts in the bank. The type of value used is a universally unique identifier (UUID) is a 128-bit number used to identify information in computer systems. Clojure uses a #uuid tag literal

(spec/def ::account-id uuid?)\n\n;; Account holder - composite specification\n(spec/def ::account-holder\n  (spec/keys\n    :req [::account-id\n          ::first-name\n          ::last-name\n          ::email-address\n          ::residential-address\n          ::social-security-id]))\n
"},{"location":"projects/banking-on-clojure/ui-handler-functions/","title":"UI Handler Functions","text":"

Taking an outside-in approach, the main parts of the website user interface will be created using Hiccup and Bulma CSS library. Mock data will be used then wired up to the database as that is designed.

Inside-out development

When writing a web service for an existing database design, taking an inside-out approach may be more effective.

An inside-out approach would include * generating Clojure Specifications for values from the database schema * define database access functions * defin handlers to expose values from the database, validated via specifications * define routes to interact with the values along business functions * create UI elements for use with the handlers to make a functioning and responsive application

The following request handlers will be created for the banking-on-clojure application

  • welcome-page
  • register-account-holder
  • accounts-overview-page
  • account-history
  • money-transfer
  • money-payment

All handlers are very similar to the welcome page, which highlights that some common template should be created for all handlers to use.

The accounts-overview-page is the main page for the application, so will be designed in more detail.

"},{"location":"projects/banking-on-clojure/ui-handler-functions/#account-overview-page-handler","title":"account-overview-page handler","text":"

This is the page customers will view by default when logging in.

src/practicalli/banking_on_clojure/handlers.clj
(defn accounts-overview-page\n  \"Overview of each bank account owned by the current customer.\n\n  Using Bulma media object style\n  https://bulma.io/documentation/layout/media-object/\n\n  Request hash-map is not currently used\"\n\n  [request]\n  (response\n    (html5\n      {:lang \"en\"}\n      [:head\n       (include-css \"https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css\")]\n      [:body\n       [:section {:class \"hero is-info\"}\n        [:div {:class \"hero-body\"}\n         [:div {:class \"container\"}\n          [:h1 {:class \"title\"} \"Banking on Clojure\"]\n          [:p {:class \"subtitle\"}\n           \"Making your money immutable\"]]]]\n\n       [:section {:class \"section\"}\n        (bank-account-media-object {:account-type  \"Current Account\" :account-number    \"123456789\"\n                                    :account-value \"1,234\"           :account-sort-code \"01-02-01\"})\n\n        (bank-account-media-object {:account-type  \"Savings Account\" :account-number    \"123454321\"\n                                    :account-value \"2,000\"           :account-sort-code \"01-02-01\"})\n\n        (bank-account-media-object {:account-type  \"Tax Free Savings Account\" :account-number    \"123454321\"\n                                    :account-value \"20,000\"                   :account-sort-code \"01-02-01\"})\n\n        (bank-account-media-object {:account-type  \"Mortgage Account\" :account-number    \"98r9e8r79wr87e9232\"\n                                    :account-value \"354,000\"          :account-sort-code \"01-02-01\"})\n\n        ]])))\n

This handler uses a helper function to reduce the amount of hiccup code.

src/practicalli/banking_on_clojure/handlers.clj
(defn bank-account-media-object\n  [account-details]\n  [:article {:class \"media\"}\n   [:figure {:class \"media-left\"}\n    [:p {:class \"image is-64x64\"}\n     [:img {:src \"https://raw.githubusercontent.com/jr0cket/developer-guides/master/clojure/clojure-bank-coin.png\"}]]]\n   [:div {:class \"media-content\"}\n    [:div {:class \"content\"}\n     [:h3 {:class \"subtitle\"}\n      (str (:account-type account-details) \" : &lambda;\" (:account-value account-details))]]\n\n    [:div {:class \"field is-grouped\"}\n     [:div {:class \"control\"}\n      [:div {:class \"tags has-addons\"}\n       [:span {:class \"tag\"} \"Account number\"]\n       [:span {:class \"tag is-success is-light\"} (:account-number account-details)]]]\n\n     [:div {:class \"tags has-addons\"}\n      [:span {:class \"tag\"} \"Sort Code\"]\n      [:span {:class \"tag is-success is-light\"} (:account-sort-code account-details)]]]]\n\n   [:div {:class \"media-right\"}\n    (link-to {:class \"button is-primary\"} \"/transfer\" \"Transfer\")\n    (link-to {:class \"button is-info\"} \"/payment\" \"Payment\")]])\n
"},{"location":"projects/banking-on-clojure/unit-testing-the-database/","title":"Unit Testing with the Database","text":""},{"location":"projects/banking-on-clojure/unit-testing-the-database/#unit-testing-using-clojure-spec","title":"Unit testing using clojure spec","text":"

Require the clojure.spec namespaces * [clojure.spec.alpha :as spec] for the core spec functions, including gen for specification generators * [clojure.spec.gen.alpha :as spec-gen] for generate and sample functions to generate values from specifications * [clojure.spec.test.alpha :as spec-test] for running check on instrumented function definitions * [practicalli.specifications-banking] for the banking related specifications

(ns practicalli.database-access-test\n  (:require\n   ;; Unit testing\n   [clojure.test :refer [deftest is testing]]\n\n   ;; Clojure Specifications\n   [clojure.spec.alpha :as spec]\n   [clojure.spec.test.alpha :as spec-test]\n   [clojure.spec.gen.alpha :as spec-gen]\n   [practicalli.specifications-banking]\n\n   ;; System under test\n   [practicalli.database-access :as SUT])\n  )\n

A simpler test to check that a map of generated values is returned when calling new-account-holder

(deftest new-account-holder-test\n  (testing \"Registered account holder is valid specification\"\n    (is (map? (SUT/new-account-holder\n                (spec-gen/generate\n                  (spec/gen :practicalli.specifications-banking/customer-details)))))))\n

This test alone will fail in a CI environment as the application does not create the database tables automatically

"},{"location":"projects/banking-on-clojure/unit-testing-the-database/#in-a-continuous-integration-environment","title":"In a continuous integration environment","text":"

A Continuous Integration environment will be empty to start with, so when using an embedded database the database and database tables need to be created each time the tests run.

Creating the tables each time the tests are run could lead to errors if the database already has those tables defined

Add IF NOT EXISTS to the CREATE TABLE SQL statement so that the create-tables! function returns nil rather than an SQL error.

(def schema-account-holders-table\n  [\"CREATE TABLE IF NOT EXISTS PUBLIC.ACCOUNT_HOLDERS(\n     ACCOUNT_HOLDER_ID UUID DEFAULT RANDOM_UUID() NOT NULL,\n     FIRST_NAME VARCHAR(32),\n     LAST_NAME VARCHAR(32),\n     EMAIL_ADDRESS VARCHAR(32) NOT NULL,\n     RESIDENTIAL_ADDRESS VARCHAR(255),\n     SOCIAL_SECURITY_NUMBER VARCHAR(32),\n     CONSTRAINT ACCOUNT_HOLDERS_PK PRIMARY KEY (ACCOUNT_HOLDER_ID))\"])\n

Longer term: Run migratus scripts to establish the database schema each time.

"},{"location":"projects/banking-on-clojure/unit-tests/","title":"Unit tests","text":"

In Clojure web applications the handler functions are the main focus of the unit tests.

Create the file test/practicalli/request-handler-test.clj to contain the unit tests.

Define the request-handler-test namespace, including other namespaces that should be required.

The ring.mock.request library is added to simulate request calls to the handlers.

(ns practicalli.request-handler-test\n  (:require [practicalli.request-handler :as SUT]\n            [clojure.test :refer [deftest is testing]]\n            [ring.mock.request :as mock]))\n

Add unit tests for each handler that will be created in practicalli.request-handler.

A simple starting point for tests is to check the correct HTTP status code is being returned.

ring.mock.request library contains a request function that will generate a mock request hash-map from a given HTTP protocol (:get :post etc.) and a address.

(deftest welcome-page-test\n  (testing \"Testing elements on the welcome page\"\n    (is (= 200\n           (:status (SUT/welcome-page (mock/request :get \"/\")))))))\n
"},{"location":"projects/banking-on-clojure/unit-tests/#ensuring-a-status-code-in-handlers","title":"Ensuring a status code in handlers","text":"

The ring.util.response namespace is added to the practicalli.request-handlers namespace. This provide the response function which wraps the body (i.e. web page content) with a correctly formed response hash-map.

(response \"Web page content to be added to the response hash-map\")\n

The practicalli.request-handlers namespace definition

(ns practicalli.request-handler\n  (:require [ring.util.response :refer [response]]\n            [hiccup.core :refer [html]]\n            [hiccup.page :refer [html5 include-js include-css]]\n            [hiccup.element :refer [link-to]]))\n

The welcome page (GET \"/\") content is defined with hiccup code to generate a HTML page (html5). This content is wrapped by the response function to return a response hash-map.

(defn welcome-page\n  [request]\n  (response\n    (html5\n      {:lang \"en\"}\n      [:head\n       (include-css \"https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css\")]\n      [:body\n       [:section {:class \"hero is-info\"}\n        [:div {:class \"hero-body\"}\n         [:div {:class \"container\"}\n          [:h1 {:class \"title\"} \"Banking on Clojure\"]\n          [:p {:class \"subtitle\"}\n           \"Making your money immutable\"]]]]\n\n       [:section {:class \"section\"}\n        [:div {:class \"container\"}\n         (link-to {:class \"button is-primary\"} \"/accounts\"    \"Login\")\n         (link-to {:class \"button is-danger\"}  \"/register\" \"Register\")\n         [:p {:class \"content\"}\n          \"Manage your money without unexpected side-effects using a simple made easy banking service\"]\n         [:img {:src \"https://raw.githubusercontent.com/jr0cket/developer-guides/master/clojure/clojure-piggy-bank.png\"}]]]])))\n
"},{"location":"projects/banking-on-clojure/unit-tests/#using-kaocha-to-run-tests","title":"Using Kaocha to run tests","text":"

Kaocha was added as part of the continuous integration configuration as a local binary. This kaocha binary can be called to run the tests and check that all the handlers are returning the right status code.

bin/kaocha\n

kaocha should return the results of running all unit tests in the project.

If a handler does not return the correct status code, then kaocha will highlight the error in the unit test results.

kaocha will show a summary of the results when all the tests are successful.

"},{"location":"projects/banking-on-clojure/update-records/","title":"Update Records in the database","text":"

Several options were explored when designing database query functions. Using next.jdbc.sql functions provides a Clojure data structures approach, where as next.jdbc/execute! uses specific SQL statement code.

Take the SQL approach if generating SQL statements directly.

Take the Clojure approach if to generate SQL statements from Clojure data structures.

"},{"location":"projects/banking-on-clojure/update-records/#generic-update-record-function","title":"Generic update record function","text":"

Use the generic create function from the database schema design section

(defn update-record\n  \"Insert a single record into the database using a managed connection.\n  Arguments:\n  - table - name of database table to be affected\n  - record-data - Clojure data representing a new record\n  - db-spec - database specification to establish a connection\n  - where-clause - column and value to identify a record to update\"\n  [db-spec table record-data where-clause]\n  (with-open [connection (jdbc/get-connection db-spec)]\n    (jdbc-sql/update! connection table record-data where-clause)))\n
"},{"location":"projects/banking-on-clojure/update-records/#update-an-existing-account_holder-record","title":"Update an existing account_holder record","text":"

Call the update-record function with the development database specification, the account holder table name and a Clojure hash-map of the record data.

Each key in the map represents a column name and the value associated with the key is the value to be inserted in the record for its column.

  (update-record db-specification-dev\n                 :public.account_holders\n                 {:EMAIL_ADDRESS \"rachel+update@rockketpack.org\"}\n                 {:account_holder_id \"f6d6c3ba-c5cc-49de-8c85-21904f8c5b4d\"})\n

If the update is successful then :update-count 1 value is returned

  ;; => #:next.jdbc{:update-count 1}\n
"},{"location":"projects/banking-on-clojure/update-records/#update-an-existing-account-record","title":"Update an existing account record","text":"

Update an existing record in the public.accounts table, providing new values for current_balance and last_updated columns.

(update-record db-specification-dev\n               \"public.accounts\"\n               {:current_balance   242\n                :last_updated      \"2020-09-12\"}\n               {:account_number \"1234567890\"})\n
"},{"location":"projects/banking-on-clojure/update-records/#update-an-existing-transaction-record","title":"Update an existing transaction record","text":"

Update an existing record in the public.transaction_history table.

(update-record db-specification-dev\n               \"public.transaction_history\"\n               {:transaction_reference \"Salary bonus\"\n                :transaction_date      \"2020-09-12\"}\n               {:transaction_id  \"8ac89cfc-6874-4ebe-9ee4-59b8c5e971ff\"})\n

Generating example data from Clojure Spec

Clojure Spec: generate mock database data

"},{"location":"projects/game-scoreboard-api/","title":"Game Scoreboard API","text":"

Building an API from scratch using

  • Reitit
  • muuntaja
  • reitit-ring middleware
  • Integrant REPL - reloaded workflow
  • Integrant - runtime component lifecycle
  • mulog - event logging
  • Auth0 - authentication and authorisation
"},{"location":"projects/leiningen/todo-app/","title":"TODO web application - Leiningen project","text":"

A simple todo list server side web application

The TODO work for the todo project includes:

  • Leiningen - project configuration and build (add Clojure CLI tools)
  • ring - jetty application server and request/response management (review)
  • compojure - routing of requests (review)
  • hiccup - HTML content written using Clojure (review)
  • Postgresql - relational database for todo items (add next.jdbc library)
  • CircleCI - continuous testing and integration
  • Heroku - deployment to staging and production (pipeline)
"},{"location":"projects/leiningen/todo-app/#todocontent-update-through-winter-2020","title":"TODO::Content update through Winter 2020","text":"

This is the original project example developed several years ago, so some of the library versions may be out of date.

Practicalli recommends using next.jdbc to talk to postgresql and examples of this are covered in the new content being created for the banking on clojure project.

"},{"location":"projects/leiningen/todo-app/compojure/","title":"Compojure","text":"

To make our webapp more useful we will add more functionality, which will require more routes.

Compojure is a library that works with Ring to manage

  • routing - running different code depending on the URL path received
  • http method switching - running different code based on the HTTP method (GET, POST, PUT, DELETE)

Compojure also has convenience functions that make ring responses easier to generate.

In this section we will update our project to use Compojure.

"},{"location":"projects/leiningen/todo-app/compojure/#leiningen-templates","title":"Leiningen Templates","text":"

Templates can be used to create a project with a given set of dependencies as well as Clojure code.

There is a compojure template that gives you a basic running web application. To use this template to create a new project use the following command, substituting your own project-name

lein new compojure project-name\n

This project contains ring and compojure. The dependency for ring is ring/site-defaults which includes some sensible default settings for your application, eg security settings such as anti-forgery.

See the definition of ring/site-defaults for further information.

"},{"location":"projects/leiningen/todo-app/compojure/#resources","title":"Resources","text":"
  • Learn X in minutes - Compojure - using httpkit (performance & scalability)
  • Webapps with Compojure & OM
  • StackOverflow - What's the \u201cbig idea\u201d behind compojure routes?
"},{"location":"projects/leiningen/todo-app/compojure/about/","title":"About route","text":""},{"location":"projects/leiningen/todo-app/compojure/about/#note-write-an-about-route-and-handler-that-gives-you-information-about-the-app","title":"Note:: Write an about route and handler that gives you information about the app.","text":"
(defn about\n  \"Information about the website developer\"\n  [request]\n  {:status 200\n   :headers {}\n   :body \"I am an awesome Clojure developer, well getting there...\"})\n

Add a route to call the about handler function

(defroutes app\n  (GET \"/\"        [] welcome)\n  (GET \"/goodbye\" [] goodbye)\n  (GET \"/about\"   [] about)\n  (not-found \"<h1>This is not the page you are looking for</h1>\n              <p>Sorry, the page you requested was not found!</p>\"))\n

"},{"location":"projects/leiningen/todo-app/compojure/adding-dependency/","title":"Add Compojure as a dependency","text":"

Edit your project configuration project.clj and add the current version of Compojure

The project.clj file should look as follows:

(defproject todo-list \"0.1.0-SNAPSHOT\"\n  :description \"A Todo List server-side webapp using Ring & Compojure\"\n  :url \"https://github.com/practicalli/clojure-todo-list-example\"\n  :license {:name \"Creative Commons Attribution Share-Alike 4.0 International\"\n            :url  \"https://creativecommons.org\"}\n  :dependencies [[org.clojure/clojure \"1.10.1\"]\n                 [ring \"1.8.0\"]\n                 [compojure \"1.6.1\"]]\n  :repl-options {:init-ns todo-list.core}\n  :main todo-list.core\n  :profiles {:dev\n             {:main todo-list.core/-dev-main}})\n

As we are adding a library to the project we need to restart the web server.

Ctrl-c in the terminal to stop the server and lein run 8000 to restart the web server

"},{"location":"projects/leiningen/todo-app/compojure/adding-dependency/#hintsearch-clojarsorg-for-dependency-versions","title":"Hint::Search clojars.org for dependency versions","text":"

The current version of Compojure or any other Clojure library can be found via Clojars.org.

"},{"location":"projects/leiningen/todo-app/compojure/adding-goodbye-route/","title":"Adding a goodbye route","text":""},{"location":"projects/leiningen/todo-app/compojure/adding-goodbye-route/#noteadd-another-route-to-display-a-goodbye-message","title":"Note::Add another route to display a goodbye message","text":"
(defroutes app\n  (GET \"/\" [] welcome)\n  (GET \"/goodbye\" [] goodbye)\n  (not-found \"Sorry, page not found\"))\n
"},{"location":"projects/leiningen/todo-app/compojure/adding-goodbye-route/#notewrite-the-handler-function-for-the-goodbye-route","title":"Note::Write the handler function for the goodbye route","text":"
(defn goodbye\n  \"A song to wish you goodbye\"\n  [request]\n  {:status 200\n   :headers {}\n   :body \"<h1>Walking back to happiness</h1>\n          <p>Walking back to happiness with you</p>\n          <p>Said, Farewell to loneliness I knew</p>\n          <p>Laid aside foolish pride</p>\n          <p>Learnt the truth from tears I cried</p>\"})\n

Now test your new route.

As we have wrap-reload around app then no restart needed

"},{"location":"projects/leiningen/todo-app/compojure/code-so-far/","title":"Code so far","text":"

The code and configuration we have created so far are in the clojure-todo-list-example repository github repository.

Code for this section is in the branch called 04-compojure

If something is not working or you want to speed up, simply clone the project into a new directory using the command:

git clone https://github.com/practicalli/clojure-todo-list-example\n
Once you have cloned the project, checkout the 04-compojure branch

git checkout 04-compojure\n
"},{"location":"projects/leiningen/todo-app/compojure/defroutes/","title":"Theory: defroutes is a Clojure macro","text":"

The Compojure function defroutes is actually a Clojure macro. The defroutes macro provides a simple syntax for defining routes and associating handler functions.

"},{"location":"projects/leiningen/todo-app/compojure/defroutes/#what-is-a-macro","title":"What is a macro?","text":"

Clojure has a programmatic macro system which allows the Clojure community to extend the language, rather than wait for the language designers. This macro approach also helps keep the language very compact, with a minimum of primitives.

We have already used several macros in our code. In our project.clj configuration we use the defproject macro to make it easy to define our Clojure project. In our code we have used the defn macro to define names (symbols) for functions.

"},{"location":"projects/leiningen/todo-app/compojure/defroutes/#peeking-under-the-covers","title":"Peeking under the covers","text":"

You can always look at what a macro is doing by using the macroexpand or macroexpand-all functions. These functions show you what the code looks like after the macro-reader has processed the macro.

To expand a macro, require the clojure.walk library in your namespace

[clojure.walk :as walk]\n

Then wrap the macro you wish to explore with the macroexpand-all function.

(walk/macroexpand-all\n '(defroutes myapp\n    (GET \"/\" [] \"Show something\")))\n\n\n(def myapp\n  (compojure.core/routes\n   (compojure.core/make-route\n    :get #clout.core.CompiledRoute{:source \"/\", :re #\"/\", :keys [], :absolute? false}\n    (fn* ([request__13075__auto__] (let* [] \"Show something\"))))))\n

You can see that the defroutes function expands to a make-route function that creates the details of the route and associates it with a handler or response map. The routes function join multiple routes together.

For further examples, see http://learnxinyminutes.com/docs/clojure-macros/

"},{"location":"projects/leiningen/todo-app/compojure/lisp-calculator/","title":"Lisp style Calculator","text":"

Lets create a very simple lisp based calculator that works with two numbers as another example of using variable path elements. As its a Lisp calculator, then we will use prefix notation (the 'operator' comes first)

"},{"location":"projects/leiningen/todo-app/compojure/lisp-calculator/#create-a-route-for-the-calculator","title":"Create a route for the calculator","text":"
(defroutes app\n  (GET \"/\" [] greet)\n  (GET \"/goodbye\" [] goodbye)\n  (GET \"/about\" [] about)\n  (GET \"/request-info\" [] handle-dump)\n  (GET \"/hello/:name\" [] hello)\n  (GET \"/calculator/:op/:a/:b\" [] calculator)\n  (not-found \"Sorry, page not found\"))\n
"},{"location":"projects/leiningen/todo-app/compojure/lisp-calculator/#create-a-handler-function-to-add-subtract-divide-or-multiply-two-numbers","title":"Create a handler function to add, subtract, divide or multiply two numbers","text":"
(defn calculator\n  \"A very simple calculator that can add, divide, subtract and multiply.  This is done through the magic of variable path elements.\"\n  [request]\n  (let [a  (Integer. (get-in request [:route-params :a]))\n        b  (Integer. (get-in request [:route-params :b]))\n        op (get-in request [:route-params :op])\n        f  (get operands op)]\n    (if f\n      {:status 200\n       :body (str \"Calculated result: \" (f a b))\n       :headers {}}\n      {:status 404\n       :body \"Sorry, unknown operator.  I only recognise + - * : (: is for division)\"\n       :headers {}})))\n
"},{"location":"projects/leiningen/todo-app/compojure/lisp-calculator/#create-a-dictionary-to-look-up-clojure-function-names","title":"Create a dictionary to look up Clojure function names","text":"

Define a hash-map called operands to look up the names of the mathematical operations (operands) to the actual functions in Clojure

(def operands {\"+\" + \"-\" - \"*\" * \":\" /})\n

Try the calculator out like follows http://localhost:8000/calculator/*/6/7

"},{"location":"projects/leiningen/todo-app/compojure/lisp-calculator/#the-namespace-with-changes-made","title":"The namespace with changes made","text":"

With all the changes from above, the code should look as follows

(def operands {\"+\" + \"-\" - \"*\" * \":\" /})\n\n(defn calculator\n  \"A very simple calculator that can add, divide, subtract and multiply.  This is done through the magic of variable path elements.\"\n  [request]\n  (let [a  (Integer. (get-in request [:route-params :a]))\n        b  (Integer. (get-in request [:route-params :b]))\n        op (get-in request [:route-params :op])\n        f  (get operands op)]\n    (if f\n      {:status 200\n       :body (str (f a b))\n       :headers {}}\n      {:status 404\n       :body \"Sorry, unknown operator.  I only recognise + - * : (: is for division)\"\n       :headers {}})))\n\n(defroutes app\n  (GET \"/\" [] welcome)\n  (GET \"/goodbye\" [] goodbye)\n  (GET \"/about\" [] about)\n  (GET \"/request-info\" [] handle-dump)\n  (GET \"/yo/:name\" [] yo)\n  (GET \"/calculator/:op/:a/:b\" [] calculator)\n  (not-found \"Sorry, page not found\"))\n
"},{"location":"projects/leiningen/todo-app/compojure/show-request-info/","title":"Show request info","text":"

We can see the details of the requests being send to our Clojure webapp by looking at the request object.

"},{"location":"projects/leiningen/todo-app/compojure/show-request-info/#request-info-route","title":"request-info route","text":"

Add a request-info route and handler to view the request information

(defn request-info\n  \"View the information contained in the request, useful for debugging\"\n  [request]\n  {:status 200\n   :body (pr-str request)\n   :headers {}})\n\n(defroutes app\n  (GET \"/\" [] welcome)\n  (GET \"/goodbye\" [] goodbye)\n  (GET \"/about\" [] about)\n  (GET \"/request-info\" [] request-info)\n  (not-found \"<h1>This is not the page you are looking for</h1> <p>Sorry, the page you requested was not found!</p>\"))\n

Visit http://localhost:8000/request-info to see the results.

"},{"location":"projects/leiningen/todo-app/compojure/show-request-info/#using-compojure-request-dump-function","title":"Using Compojure request dump function","text":"

Compojure has a request dump function that gives a much nicer output than our initial request-info function. The dump function also separates the default response keys with any additional keys provided by the URL.

"},{"location":"projects/leiningen/todo-app/compojure/show-request-info/#include-handle-dump-in-the-namespace","title":"Include handle-dump in the namespace","text":"
(ns webdev.core\n  (:require [ring.adapter.jetty :as jetty]\n            [ring.middleware.reload :refer [wrap-reload]]\n            [compojure.core :refer [defroutes GET]]\n            [compojure.route :refer [not-found]]\n            [ring.handler.dump :refer [handle-dump]]))\n
"},{"location":"projects/leiningen/todo-app/compojure/show-request-info/#remove-request-info-function","title":"Remove request-info function","text":"

Delete the request-info function we defined previously and update the /request-info route to use handle-dump as the handler

(defroutes app\n  (GET \"/\" [] welcome)\n  (GET \"/goodbye\" [] goodbye)\n  (GET \"/about\" [] about)\n  (GET \"/request-info\" [] handle-dump)\n  (not-found \"<h1>This is not the page you are looking for</h1> <p>Sorry, the page you requested was not found!</p>\"))\n

Now the output is much nicer http://localhost:8000/request-info

"},{"location":"projects/leiningen/todo-app/compojure/theory-local-name-bindings/","title":"Theory local name bindings","text":""},{"location":"projects/leiningen/todo-app/compojure/theory-local-name-bindings/#theory-local-binding-with-let","title":"Theory: Local binding with let","text":"

The let function binds a name to a value within the scope of the let function. The name is used to represent the value it is bound to, especially useful if the value is complex or the result of an expression.

(let [name value])\n

Binding values to names can be used to remove duplicate code, making the code more efficient.

"},{"location":"projects/leiningen/todo-app/compojure/theory-local-name-bindings/#binding-any-value","title":"Binding any value","text":"

A let expression can bind a name to any Clojure value, from a simple number or string, to a collection or result of an expression.

In our example we are pulling out a value from a map and using the let function to create a name we can use to reference that value. The name is used to in the body of the response map, so when the response map is returned the page is displayed with the name.

(let [name (get-in request [:route-params :name])]\n    {:status 200\n     :body (str \"Hello \" name \".  I got your name from the web URL\")\n     :headers {}})\n

The Ring adaptor creates a Clojure hash-map from the browser request which is called the request map. The request map is passed to handler functions.

"},{"location":"projects/leiningen/todo-app/compojure/theory-local-name-bindings/#a-binding-is-immediately-available","title":"A binding is immediately available","text":"

A name is available for use as soon as it is bound, even within the name/value bindings section of the let expression.

(let [apples  10\n      oranges 15\n      total-fruit (+ apples oranges)]\n  (str \"Total fruit: \" total-fruit))\n
"},{"location":"projects/leiningen/todo-app/compojure/theory-local-name-bindings/#hintuse-meaningful-names-or-avoid-local-names","title":"Hint::Use meaningful names or avoid local names","text":"

Use meaningful names in let expressions to effectively communicate the purpose of the code.

If it is hard to find a meaningful name, either the problem space is not understood enough or local names may not be necessary.

"},{"location":"projects/leiningen/todo-app/compojure/theory-routing/","title":"Theory: routing","text":"

In compojure, each route is a combination offer a HTTP method paired with a URL-matching pattern, an argument list, and a handler. The handler is typically the name of function.

(defroutes myapp\n  (GET     \"/\" [] show-something)\n  (POST    \"/\" [] create-something)\n  (PUT     \"/\" [] replace-something)\n  (PATCH   \"/\" [] modify-something)\n  (DELETE  \"/\" [] annihilate-something)\n  (OPTIONS \"/\" [] appease-something)\n  (HEAD    \"/\" [] preview-something))\n

A handler is a functions which accept request maps and return response maps.

(defn show-something\n  \"A simple handler function\"\n  [request]\n  {:status 200\n   :headers {\"Content-Type\" \"text/html; charset=utf-8}\n   :body \"<h1>I am a simple handler function</h1>\"})\n

These handler functions can be called by passing a Clojure hash-map. The result is another Clojure hash-map that contains values for :status, :headers and :body.

(show-something {:uri \"/\" :request-method :post})\n;; => {:status 200\n;;     :headers {\"Content-Type\" \"text/html; charset=utf-8}\n;;     :body \"<h1>I am a simple handler function</h1>\"}\n

The body may be a function, which must accept the request as a parameter:

(defroutes myapp\n  (GET \"/\" [] (fn [req] \"Do something with req\")))\n

Or, you can just use the request directly:

(defroutes myapp\n  (GET \"/\" req \"Do something with req\"))\n

Route patterns may include named parameters:

(defroutes myapp\n  (GET \"/hello/:name\" [name] (str \"Hello \" name)))\n

You can adjust what each parameter matches by supplying a regex:

(defroutes myapp\n  (GET [\"/file/:name.:ext\" :name #\".*\", :ext #\".*\"] [name ext]\n    (str \"File: \" name ext)))\n
"},{"location":"projects/leiningen/todo-app/compojure/theory-using-hash-maps/","title":"Theory: Accessing hash-maps","text":"

The request is a Clojure hash-map made up of key / value pairs, referred to as the request map. The keys are Clojure keywords. The values are typically strings or Clojure collections (vectors, hash-maps).

Here is an example of a request map

{:request-params {:name \"John\"}}\n

Using the get function to return the value for a particular keyword in the request-map

(get request-map :keyword)\n
"},{"location":"projects/leiningen/todo-app/compojure/theory-using-hash-maps/#using-hash-map-as-a-function","title":"Using hash-map as a function","text":"

A hash-map can be evaluated as a function call to the map with the key as an argument. Any type of key can be used in this expression.

(request-map :keyword)\n(request-map \"key as string\")\n
"},{"location":"projects/leiningen/todo-app/compojure/theory-using-hash-maps/#nested-hash-maps","title":"Nested hash-maps","text":"

Two get expressions could be used to return a particular value when accessing a nested hash-map. The inner get expression returns a hash-map and the outer get expression returns the value.

(get (get outer-map :outer-keyword) :inner-keyword)\n

With many nested maps, the get function can lead to code that is harder to read. Using the get-in function provides a simpler syntax for traversing nested maps

get-in walks through the nested hash-map along the path defined by the vector of keys.

(get-in request-map [:outer-keyword :inner-keyword])\n
"},{"location":"projects/leiningen/todo-app/compojure/theory-using-hash-maps/#using-keywords-and-hash-maps","title":"Using keywords and hash-maps","text":"

Keywords can be evaluated as a function call with a hash-map as an argument and return their associated value in that hash-map.

(def response-map {:name \"john\" :path \"/hello\"}\n

You can get the value from this map using the keyword

(response-map :name)\n\n=> \"john\"\n\n(response-map :path)\n\n=> \"/hello\"\n

Other types of keys do not work as function calls. Either use the map as a function with the key as an argument or use the get and get-in functions as appropriate.

"},{"location":"projects/leiningen/todo-app/compojure/using-compojure/","title":"Using Compojure in the project","text":"

The Compojure defroute function provides a syntax for defining routes and associating handlers.

"},{"location":"projects/leiningen/todo-app/compojure/using-compojure/#add-compojure-to-the-namespace","title":"Add Compojure to the namespace","text":"

Add the defroutes function, GET protocol and notfound route from Compojure to the namespace

(ns todo-list.core\n  (:require [ring.adapter.jetty :as jetty]\n            [ring.middleware.reload :refer [wrap-reload]]\n            [compojure.core :refer [defroutes GET]]\n            [compojure.route :refer [not-found]]))\n
"},{"location":"projects/leiningen/todo-app/compojure/using-compojure/#refactor-the-welcome-function-to-just-say-hello","title":"Refactor the welcome function to just say Hello","text":"

The welcome function should just do one simple thing, return a welcome message.

(defn welcome\n  \"A ring handler to respond with a simple welcome message\"\n  [request]\n  {:status 200\n     :body \"<h1>Hello, Clojure World</h1>\n     <p>Welcome to your first Clojure app, I now update automatically</p>\"\n     <p>I now use defroutes to manage incoming requests</p>\n   :headers {}})\n
"},{"location":"projects/leiningen/todo-app/compojure/using-compojure/#add-a-defroutes-function","title":"Add a defroutes function","text":"

Add a defroutes function called app to manage our routes. Add routes for / and send all other requests to the Compojure not-found function.

(defroutes app\n  (GET \"/\" [] welcome)\n  (not-found \"<h1>This is not the page you are looking for</h1>\n              <p>Sorry, the page you requested was not found!</p>\"))\n
"},{"location":"projects/leiningen/todo-app/compojure/using-compojure/#update-dev-main-and-main-functions","title":"Update -dev-main and -main functions","text":"

Change the -dev-main and -main functions to call the app function, instead of the welcome function

(defn -main\n  \"A very simple web server using Ring & Jetty\"\n  [port-number]\n  (webserver/run-jetty app\n     {:port (Integer. port-number)}))\n\n(defn -dev-main\n  \"A very simple web server using Ring & Jetty that reloads code changes via the development profile of Leiningen\"\n  [port-number]\n  (webserver/run-jetty (wrap-reload #'app)\n     {:port (Integer. port-number)}))\n

As we have changed the -dev-main and -main functions, we need to restart the server again - Ctrl-c then lein run 8000

Now test out your updated web app by visiting http://localhost:8000 and http://localhost:8000/not-there

"},{"location":"projects/leiningen/todo-app/compojure/variable-path-elements/","title":"Variable Path Elements","text":"

A simple way to affect the behaviour of a web app is to add extra text (elements) to the web address (URL). For example, you can add your name to the end of the web address and the returned web page will include your name.

By adding an element to the route path, we can take that element from the URL as it is part of the request. We can then get that value from the request map and use it in our body content.

"},{"location":"projects/leiningen/todo-app/compojure/variable-path-elements/#hello-handler-example","title":"Hello handler example","text":"

Create a simple personalised hello message by adding a route for /hello with /:name as a path element.

Create a hello function as the handler that pulls out the :name element from the request and adds it to the response.

(defn hello\n  \"A simple personalised greeting showing the use of variable path elements\"\n  [request]\n  (let [name (get-in request [:route-params :name])]\n    {:status 200\n     :body (str \"Hello \" name \".  I got your name from the web URL\")\n     :headers {}}))\n\n(defroutes app\n  (GET \"/\" [] greet)\n  (GET \"/goodbye\" [] goodbye)\n  (GET \"/about\" [] about)\n  (GET \"/request-info\" [] handle-dump)\n  (GET \"/hello/:name\" [] hello)\n  (not-found \"Sorry, page not found\"))\n

Now you can test this route out by also including a name to the URL path http://localhost:8000/hello/john

"},{"location":"projects/leiningen/todo-app/connect-to-postgres/","title":"Connecting to Heroku PostgreSQL from Clojure","text":"
  • Add dependencies
  • Define a database connection (Heroku posgres)
  • Migrations (TODO)
"},{"location":"projects/leiningen/todo-app/connect-to-postgres/#using-jdbc-for-relational-databases","title":"Using JDBC for Relational Databases","text":"

Java Database connectivity is a common way to connect to a relational database and has very widespread database support.

next.jdbc is a Clojure library to send SQL statements over jdbc or use a DSL such as HoneySQL) to work with these databases.

"},{"location":"projects/leiningen/todo-app/connect-to-postgres/add-database-dependencies/","title":"Add Dependency","text":"

Our application will use JDBC (Java database connectivity) to connect to the Postgres database. So we need to add the JDBC library along with a a specific JDBC driver for Postgres.

Edit the project configuration file, project.clj and add the following dependencies

[org.clojure/java.jdbc \"0.7.10\"]\n[org.postgresql/postgresql \"42.2.9\"]\n

The project.clj file should now look as follows:

(defproject todo-list \"0.1.0-SNAPSHOT\"\n  :description \"A Todo List server-side webapp using Ring & Compojure\"\n  :url \"https://github.com/practicalli/clojure-todo-list-example\"\n  :license {:name \"Creative Commons Attribution Share-Alike 4.0 International\"\n            :url  \"https://creativecommons.org\"}\n\n  :dependencies [[org.clojure/clojure \"1.10.1\"]\n                 [ring \"1.8.0\"]\n                 [compojure \"1.6.1\"]\n                 [org.clojure/java.jdbc \"0.7.10\"]\n                 [org.postgresql/postgresql \"42.2.9\"]]\n  :min-lein-version \"2.0.0\"\n  :repl-options {:init-ns todo-list.core}\n  :main todo-list.core\n  :profiles {:dev\n             {:main todo-list.core/-dev-main}\n             :uberjar {:aot :all}}\n  :uberjar-name \"todo-list.jar\"\n  :auto-clean false)\n
"},{"location":"projects/leiningen/todo-app/connect-to-postgres/add-database-dependencies/#note-add-dependencies-to-the-project-for-the-heroku-postgres-database","title":"Note:: Add Dependencies to the project for the Heroku Postgres database","text":""},{"location":"projects/leiningen/todo-app/connect-to-postgres/define-db-connection/","title":"Define a Database Connection","text":""},{"location":"projects/leiningen/todo-app/connect-to-postgres/define-db-connection/#hintoutdated-use-nextjdbc-approach","title":"Hint::Outdated: Use next.jdbc approach","text":"

next.jdbc provides a simple way to connect to a range of databases

Heroku provides a way to generate the connection string. The Heroku build process sets an environment variable called JDBC_DATABASE_URL which can be used with next.jdbc.

"},{"location":"projects/leiningen/todo-app/connect-to-postgres/define-db-connection/#outdated-under-review","title":"Outdated - under review","text":"

View the Database_URL configuration variable for the Heroku Database and define a name to represent that in Clojure

Use the Heroku Toolbelt to view the configuration variables

heroku config\n

Edit the file src/todo_list/core.clj file and add the following definition towards the top of the file. Substitute your own database connection values for :subname, user and password.

(def postgres {:subprotocol \"postgresql\"\n               :subname \"//node.domain.com:5432/database-name\"\n               :user \"username\"\n               :password \"password\"\n               :ssl true\n               :sslmode true\n               :sslfactory \"org.postgresql.ssl.NonValidatingFactory\"})\n

Breaking down the Heroku Postgres connection string into a map allows us to easily add options to the connection string whilst keeping it readable.

Also, a JDBC connection string has a slightly different form to the Heroku string. Heroku Posgres creates a configuration variable in the form of postgres://[user]:[password]@[host]:[port]/[database] whereas the JDBC connection string is of the form `jdbc:postgres://[host]:[port]/[database]?user=[user]&password=[pass]

"},{"location":"projects/leiningen/todo-app/connect-to-postgres/define-db-connection/#jdbc-connection-string-for-heroku-postgres","title":"JDBC connection string for Heroku Postgres","text":"

jdbc:postgresql://[host]:[port]/[database]?user=[user]&password=[password]&ssl=true&sslfactory=org.postgresql.ssl.NonValidatingFactory.

Converting the map back to a JDBC connection string

(defn remote-heroku-db-spec [host port database username password]\n  {:connection-uri (str \"jdbc:postgresql://\" host \":\" port \"/\" database \"?user=\" username \"&password=\" password \"&ssl=true&sslfactory=org.postgresql.ssl.NonValidatingFactory\")})\n
"},{"location":"projects/leiningen/todo-app/connect-to-postgres/define-db-connection/#from-heroku","title":"From Heroku","text":"

JDBC_DATABASE_URL environment variable should be used for the Heroku database connection

The DATABASE_URL environment variable from the Heroku Postgres add-on follows this naming convention:

postgres://<username>:<password>@<host>/<dbname>\n

However the Postgres JDBC driver uses the following convention:

jdbc:postgresql://<host>:<port>/<dbname>?user=<username>&password=<password>\n

Notice the additional ql at the end of jdbc:postgresql.

"},{"location":"projects/leiningen/todo-app/create-a-handler-function/","title":"Create a handler function","text":"

So far we have just sent back the same response map. To make our webapp more useful then we should have functions that return different web pages and resources (like JSON for API's).

In Ring terminology, these functions are referred to as a handler. They handler a request and return a response.

When you send a request to the webapp, the ring adaptor converts this request to a map and sends it to the specified handler.

A handler function takes the request map as its argument and returns a response map.

"},{"location":"projects/leiningen/todo-app/create-a-handler-function/#noteadd-separate-handler-function","title":"Note::Add separate handler function","text":"

Refactor the code in the src/todo-list/core.clj file to create a separate welcome handler function that processes all requests

(defn welcome\n  \"A ring handler to process all requests sent to the webapp\"\n  [request]\n  {:status  200\n   :headers {}\n   :body    \"<h1>Hello, Clojure World</h1>\n             <p>Welcome to your first Clojure app.\n             This message is returned regardless of the request, sorry<p>\"})\n

Update the -main function to call the welcome function

(defn -main\n  \"A very simple web server using Ring & Jetty\"\n  [port-number]\n  (webserver/run-jetty\n    welcome\n    {:port  (Integer. port-number)\n     :join? false}))\n
"},{"location":"projects/leiningen/todo-app/create-a-handler-function/#run-the-server-again","title":"Run the server again","text":"

Save code changes and run the web server (use Control-c if you need to stop the server first)

lein run 8000\n

Your webapp should behave exactly as it did before, check by visiting http://localhost:8000.

"},{"location":"projects/leiningen/todo-app/create-a-handler-function/#hintautomatically-reloading","title":"Hint::Automatically reloading","text":"

In the middleware section the wrap-reload ring middleware component is used to automatically reload code changes into the running application, so no need to restart the webserver unless we have to add a dependency.

"},{"location":"projects/leiningen/todo-app/create-a-handler-function/add-not-found/","title":"Add Error message when request not found","text":"

So far our app has responded with the same message, regardless of the web address (route) requested in the browser. The webapp will be more useful if it responds differently to different routes.

(defn welcome\n  \"A ring handler to process all requests for the web server.\n  If a request is for something other than `/` then an error message is returned\"\n  [request]\n  (if (= \"/\" (:uri request))\n    {:status 200\n     :body \"<h1>Hello, Clojure World</h1>\n            <p>Welcome to your first Clojure app.</p>\"\n     :headers {}}\n    {:status 404\n     :body \"<h1>This is not the page you are looking for</h1>\n            <p>Sorry, the page you requested was not found!></p>\"\n     :headers {}}))\n

If the route matches / then a response map with the welcome message is returned. For any other route, a response map containing our error message is returned.

"},{"location":"projects/leiningen/todo-app/create-a-handler-function/add-not-found/#noteadd-error-message-to-handler","title":"Note::Add error message to handler","text":"

Change the code to only respond with content when requesting the default route, that is http://localhost:8000/. Anything else we will return an error.

Edit the welcome function in src/todo-list/core.clj and use an if function to check if the request is valid or not.

"},{"location":"projects/leiningen/todo-app/create-a-handler-function/add-not-found/#run-the-new-version-of-your-code","title":"Run the new version of your code","text":"

If your server is still running, kill it first using Ctrl-c keyboard shortcut. Then run the server again, this time with the new code using the same command as before:

lein run 8000\n

Open http://localhost:8000 in your browser and try out different pages, such at /hello, /goodbye or /complete-indifference.

Only http://localhost:8000 will return the welcome message, everything else should return the error message.

"},{"location":"projects/leiningen/todo-app/create-a-handler-function/code-so-far/","title":"Code so far","text":"

The code and configuration we have created so far are in the clojure-webapps-example github repository.

Code for this section is in the branch called 02-create-a-handler-function

If something is not working or you want to speed up, simply clone the project into a new directory using the command:

git clone https://github.com/practicalli/clojure-webapps-example\n
Once you have cloned the project, checkout the 02-create-a-handler-function branch

git checkout 02-create-a-handler-function\n
"},{"location":"projects/leiningen/todo-app/create-a-handler-function/if-function/","title":"Theory: if function","text":"

Clojure has an if function that evaluates an expresssion. If that expression is true, then the first value is returned, if false then the second argument is returned.

In pseudo-code, the if function in Clojure works as follows

  If (this expression is true ?)\n    then return this value\n    else return this value\n

In the project code an if function checks the web address by returning the value associated with :uri in the request map.

If the :url value is equal to / then the first response map with the hello message is returned.

If the :uri value is not equal to / then the second resource map with an error message is returned.

  (if (= \"/\" (:uri request))\n    {:status 200\n     :body \"<h1>Hello, Clojure World</h1>\n            <p>Welcome to your first Clojure app.</p>\"\n     :headers {}}\n    {:status 404\n     :body \"<h1>This is not the page you are looking for</h1>\n            <p>Sorry, the page you requested was not found!></p>\"\n     :headers {}}))\n
"},{"location":"projects/leiningen/todo-app/create-a-handler-function/if-function/#hintsingle-path-if-function","title":"Hint::Single path if function","text":"

In the case where an if expression is defined with only one value and the expression is false, then the value nil is returned. when function is the idiomatic choice over a single path if function.

"},{"location":"projects/leiningen/todo-app/create-a-handler-function/if-function/#multiple-expression-if-function-with-do","title":"Multiple expression if function with do","text":"

Each of the two possible values the if function returns can come from only evaluating one expression. For example

(if (true)\n  (str \"I am the truth\")\n  (str \"I am the path to darkness\")\n

If you need multiple expressions they can be wrapped in the do function

(if (true)\n  (do (some-function)\n      (another-function))\n  (else-function))\n

The do function calls each function evaluation in turn, returning the result of the last function called.

"},{"location":"projects/leiningen/todo-app/create-a-handler-function/if-function/#hintcompojure-for-managing-routes","title":"Hint::Compojure for managing routes","text":"

The if function is a very simplistic way to define routes in our web application. Compojure is a library for elegantly managing routing for our Clojure server-side applications.

"},{"location":"projects/leiningen/todo-app/create-a-handler-function/maps-and-keywords/","title":"Maps and keywords","text":""},{"location":"projects/leiningen/todo-app/create-a-handler-function/maps-and-keywords/#theory-maps-and-keywords","title":"Theory: maps and keywords","text":"

When a request is received by our application, it is converted from by Jetty to a servlet request. Ring then converts this to a Clojure map called request. All handlers in our application take a request map as an argument.

A map in Clojure contains one or more key / value pairs, you may be familiar with the term hash map. The keys in these maps are often defined using a Clojure keyword. A keyword is a symbol that points to itself and so therefore is unique within a specific scope. A keyword makes it very easy to get a value from a map and acts as a function on the map to return its associated value

So, assume we have defined a map called request. This map contains a key defined with the :uri keyword. We can get the value associated with the key using the keyword as a function

(def request {:uri \"/\"})\n\n(:uri request)\n\n;; As a map can also act as a function to get its elements, you can also use the following form to get the same value\n(request :uri)\n

The function get is functions that helps us get data from maps. The function get-in helps us get data from nested levels of maps.

"},{"location":"projects/leiningen/todo-app/create-a-project/","title":"Create a project","text":"

Create a project called todo-list using Leiningen, the build automation tool for Clojure. This project will run the simplest possible webserver.

On the command line:

lein new todo-list\n

"},{"location":"projects/leiningen/todo-app/create-a-project/#take-a-look-at-the-project-structure","title":"Take a look at the project structure","text":"

Change into the todo-list directory created by the Leiningen command and see the project structure that has been created.

  • project.clj - the project configuration, written in Clojure
  • src for all the source code
  • test for unit test code

Using the tree command is a simple way to see the project structure (alternatively use ls -R or a graphical file browser).

"},{"location":"projects/leiningen/todo-app/create-a-project/#hint-file-names-and-the-java-class-path","title":"Hint:: File names and the Java class path","text":"

The src and test directories both contain a directory named todo_list even though our project is todo-list.

Unfortunately the Java classpath does not like dashes '-' in directory or file names, so Leiningen changes the directory names to src/todo_list & test/todo_list and the initial test to src/todo_list/core_test.clj.

"},{"location":"projects/leiningen/todo-app/create-a-project/code-so-far/","title":"The code so far","text":"

The code and configuration we have created so far are in the clojure-todo-list-example repository github repository.

Code for this section is in the branch called master

If something is not working or you want to speed up, simply clone the project into a new directory using the command:

git clone https://github.com/practicalli/clojure-todo-list-example\n
Once you have cloned the project, checkout the master branch

git checkout master\n
"},{"location":"projects/leiningen/todo-app/create-a-project/update-project-details/","title":"Update project details","text":"

Adding project details to the project.clj file helps every developer that works with the code to have a basic understanding of the projects purpose.

"},{"location":"projects/leiningen/todo-app/create-a-project/update-project-details/#noteupdate-project-details","title":"Note::Update project details","text":"

Edit the project.clj file and make the following changes.

  • Add a description
  • Add the URL of the project, eg. the github repository
  • Update the licence (optional)
  • Update the dependencies to the latest Clojure version

The project.clj file for Practicalli projects is as follows:

(defproject todo-list \"0.1.0-SNAPSHOT\"\n  :description \"A Todo List server-side webapp using Ring & Compojure\"\n  :url \"https://github.com/practicalli/clojure-todo-list-example\"\n  :license {:name \"Creative Commons Attribution Share-Alike 4.0 International\"\n            :url  \"https://creativecommons.org\"}\n  :dependencies [[org.clojure/clojure \"1.10.1\"]]\n  :repl-options {:init-ns todo-list.core})\n
"},{"location":"projects/leiningen/todo-app/create-a-project/update-project-details/#licence-change","title":"Licence change","text":"

All code by Practicalli is under the Creative Commons Attribution Share-alike.

As well as changing the project.clj file :licence declaration, the LICENCE file created by the Leiningen template has been deleted as it refers to another licence.

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/","title":"Use the Ring Library to create a webserver","text":"

The Ring library can start an embedded Java server (eg. Jetty, Tomcat) to listen for requests from a browser. Each browser request received is converted into a request map, a Clojure map with keys and values. This request map is passed to a handler function, which returns a response map.

In the section you will discover how to:

  • Add the Ring library as a dependency
  • Including Ring in the namespace
  • Add a main function to run a Jetty webserver
  • Configure the project's main namespace
  • Run webserver
"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/#related-theory","title":"Related Theory","text":"

We will cover some related theory on Coercing types (also known as casting types) to help us deal with Java interoperability.

We will also cover how to manage the scope of your Clojure code with Namespaces.

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/#hintring-details","title":"Hint::Ring details","text":"

Ring is covered in more detail in the next section, once you have your first webserver up and running.

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/add-a-jetty-webserver/","title":"Run Jetty web server","text":"

The Ring Jetty adaptor is used to run an instance of Jetty. The -main function contains an anonymous function that takes any request and returns a response map.

The -main function takes a port number as an argument which we pass when running the application.

A response map contains the following key / value pairs * :status - the result of the request, eg. 200 OK, 401 Not Found, etc * :body - the content to be returned (web page, json, etc) * :headers - a map of standard headers included in any web browser response

Add a function called -main to the src/todo_list/core.clj file.

(defn -main\n  \"A very simple web server using Ring & Jetty\"\n  [port-number]\n  (webserver/run-jetty\n    (fn [request]\n      {:status  200\n       :headers {}\n       :body    \"<h1>Hello, Clojure World</h1>\n                 <p>Welcome to your first Clojure app.\n                 This message is returned regardless of the request, sorry</p>\"})\n    {:port  (Integer. port-number)\n     :join? false}))\n
"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/add-a-jetty-webserver/#explaining-the-new-function","title":"Explaining the new function","text":"

Using a - at the start of the -main function is a naming convention, helping you see which function is the entry point to your program. Leiningen also looks for this -main function by default when running your application.

The webserver/run-jetty function takes two arguments. In our example, the first argument is an anonymous function that returns a map (the response to the browser request); the second argument is a port number to run the jetty server on expressed as a Java Integer object.

The Integer. function is a call to java.lang.Integer. The . is a special form that tells Clojure to treat this name as a call to Java. See coercing types and java.lang

The :join? false setting enables the REPL prompt to run after the web server starts. By default the join setting is true and the running server would block access to the REPL prompt.

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/add-ring-dependency/","title":"Add Ring Dependency","text":"

Add the ring library as a dependency of the todo-list project.

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/add-ring-dependency/#noteadd-ring-dependency","title":"Note::Add ring dependency","text":"

Edit the project.clj file and add [ring \"1.8.0\"] to the :dependencies section, after the Clojure library dependency.

(defproject todo-list \"0.1.0-SNAPSHOT\"\n  :description \"A Todo List server-side webapp using Ring & Compojure\"\n  :url \"https://github.com/practicalli/clojure-todo-list-example\"\n  :license {:name \"Creative Commons Attribution Share-Alike 4.0 International\"\n            :url  \"https://creativecommons.org\"}\n  :dependencies [[org.clojure/clojure \"1.10.1\"]\n                 [ring \"1.8.0\"]]\n  :repl-options {:init-ns todo-list.core})\n
"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/add-ring-dependency/#hintdependencies-with-leiningen","title":"Hint::Dependencies with Leiningen","text":"

Read the dependencies section of the Leiningen documentation to learn more about adding libraries.

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/add-ring-dependency/#looking-up-libraries-current-versions","title":"Looking up Libraries & current versions","text":"

Libraries created by the Clojure community can be found on Clojars.org, an online repository similar to Maven Central.

Use the Clojars.org website to search for the latest version of Ring.

The dependency notation for Leiningen is documented for each library, making it easy to add the library to your project.

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/code-so-far/","title":"The code so far","text":"

The code created so far is in the clojure-webapps-example github repository, specifically the branch called 01-create-a-webserver

If something is not working or you want to speed up, simply clone the project (if you have not already done so) into a new directory using the command:

git clone https://github.com/practicalli/clojure-webapps-example\n

Checkout the 01-create-a-webserver branch to see the relevant version of the code

git checkout 01-create-a-webserver-with-ring\n
"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/coersing-types-and-java-lang/","title":"Theory: Specifying Types & java.lang","text":"

Clojure has types that are created dynamically when the code is compiled, with everything being represented by Java objects as its compiled to Java byte code.

Clojure simply infers the type of a value, so types do not need to be specified in code.

The built in collections (list, map, vector & set) also support mixed types too.

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/coersing-types-and-java-lang/#calling-java-code","title":"Calling Java code","text":"

The Clojure project uses Jetty, a web application server written in Java. When calling the run-jetty function an Integer type must be passed to the Java object for the port number.

When running the Clojure project, the argument supplied for the port number on the command line is treated as a String object. Therefore we need to explicitly cast the port number from a Java String type to an Java Integer type.

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/coersing-types-and-java-lang/#javalang-library","title":"java.lang library","text":"

The java.lang. library is part of all Clojure projects, so as we are going to create a Java Integer it makes sense to simply use the Integer constructor with a String argument which returns a new Integer object.

(Integer. port-number) calls the java.lang.Integer constructor.

The . is actually a macro in Clojure that provides a simple way to work with Java, allowing you to call Java objects as if they were Clojure functions. In Java you would have to use the form Type instance-name = new Type(argument). In our example you would write this in Java as String port = new String(port-number)

From the Java 8 docs for Integer class: Integer(String s) - constructs a newly allocated Integer object that represents the int value indicated by the String parameter.

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/coersing-types-and-java-lang/#theory-its-java-objects-underneath-strings-numbers","title":"Theory: Its Java Objects underneath strings & numbers","text":"

Strings and numbers are represented by Java objects underneath, so its convenient to use Java Classes to manipulate these simple data structures on the rare occasion you need a specific type.

You can see the underlying Java types in Clojure using the type or class function. In the following example you can see the Java types for strings and numbers

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/configure-main-namespace/","title":"Configure main namespace","text":"

Setting the default namespace will automatically call a function called -main when the Clojure project is run, i.e. via lein run

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/configure-main-namespace/#noteadd-main-namespace","title":"Note::Add main namespace","text":"

Edit the project.clj file and add :main todo-list.core configuration option.

(defproject todo-list \"0.1.0-SNAPSHOT\"\n  :description \"A Todo List server-side webapp using Ring & Compojure\"\n  :url \"https://github.com/practicalli/clojure-todo-list-example\"\n  :license {:name \"Creative Commons Attribution Share-Alike 4.0 International\"\n            :url  \"https://creativecommons.org\"}\n  :dependencies [[org.clojure/clojure \"1.10.1\"]\n                 [ring \"1.8.0\"]]\n  :repl-options {:init-ns todo-list.core}\n  :main todo-list.core)\n
"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/include-ring-library/","title":"Including Ring in the Namespace","text":"

Add the ring-adaptor-jetty namespace from the ring library, so we can use the functions from that library.

The ns expression defines the current namespace as todo-list.core, providing a scope for all the functions and data structures we define within it.

The :require expression makes the ring.adaptor.jetty namespace accessible within the todo-list.core namespace. We can now call any of the public functions in the ring.adaptor.jetty namespace.

In ring.adapter.jetty namespace is bound to the webserver alias, providing a short name to refer to functions from that namespace.

For example, the run-jetty function is called using webserver/run-jetty rather than the fully qualified namespace of ring.adaptor.jetty/run-jetty

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/include-ring-library/#noterequire-the-ring-adaptor","title":"NOTE::Require the ring-adaptor","text":"

Delete all code in src/todo_list/core.clj and replace it with the following code.

(ns todo-list.core\n  (:require [ring.adapter.jetty :as webserver]))\n
"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/include-ring-library/#hintusing-aliases-for-namespaces","title":"Hint::Using aliases for namespaces","text":"

Using :require we can use the :as keyword to specify an alias for a namespace, a short-hand way of referring to a library. You can specify any valid Clojure name for a namespace alias, however please consider the readability of your code and choose a meaningful alias name.

Later in the workshop we will show other options for including functions from other namespaces.

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/namespaces/","title":"Theory: Namespaces","text":"

A namespace in Clojure is used to manage the logical separation of code, usually along features of the application. A namespace limits the scope of functions and names of data structures to a specific namespace.

The names bound to function definitions using the defn function can be used elsewhere in the namespace just by using the name. The same goes for any values bound to a name using the def function.

To use a function outside the namespace, you need to use its namespace and its name, for example clojure.string/reverse

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/namespaces/#hintclojure-order-of-evaluation","title":"Hint::Clojure order of evaluation","text":"

The code in a Clojure namespace is evaluated only once and from top to bottom. To call a named function or data structure, it must have its definition evaluated first.

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/namespaces/#include-another-namespace-in-the-repl","title":"Include another namespace in the REPL","text":"

The require function will provide access functions and names from specific namespaces and an alias for the namespace can also be specified with the :as directive.

A function from that namespace can then be used by prefixing its name with the alias specified in the require expression.

Here is an example of including the clojure.string namespace and calling its reverse function

(require '[clojure.string :as string])\n\n(string/reverse \"RedRum\")\n
"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/namespaces/#including-another-namespace-in-source-code","title":"Including another namespace in source code","text":"

Instead of the require function, add the :require keyword in the namespace definition, ns.

(ns todo-list.core\n (:require '[clojure.string :as string])\n\n(string/reverse \"RedRum\")\n

If a function will be used many times in the namespace, you can :refer a function so you can call it just by name, as if it had been defined in the current namespace.

(ns todo-list.core\n (:require '[clojure.string :refer [reverse]]))\n\n(reverse \"RedRum\")\n
"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/namespaces/#hintdependency-conflicts-avoid-the-use-function","title":"Hint::Dependency conflicts - avoid the use function","text":"

The use function and :use within an ns definition are seen as a bad practice and should be avoided.

The use function includes all the functions as if they had been written in the including a great many unused functions into the namespace. It will also pull in all the other namespace functions that each namespace included.

As Clojure is typically composed of many libraries, its prudent to only include the specific things you need from another namespace. This also helps reduce conflicts when including multiple libraries in your project.

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/namespaces/#namespaces-outside-the-project","title":"Namespaces outside the project","text":"

To use a namespace from a library that is not part of the project, you also need to include it as a dependency. We saw in add ring dependency how to add a library as a :dependency in the Leiningen project.clj file.

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/run-webserver/","title":"Run webserver","text":"

Run the webserver we use Leiningen, the Clojure build automation tool.

"},{"location":"projects/leiningen/todo-app/create-a-webserver-with-ring/run-webserver/#run-the-webserver","title":"Run the webserver","text":"

In a command line terminal, navigate to the root of your project and type the following command

lein run 8000\n

This command will start an embedded Jetty web server that listens on http://localhost:8000.

Open http://localhost:8000 in your browser and try out different pages, such at http://localhost:8000/hello, /goodbye or /makes-no-difference. It should not matter what page you visit, you should get the same response.

To stop the server, press Control-c in the terminal used to run the lein command.

"},{"location":"projects/leiningen/todo-app/database-model/","title":"Creating a database model","text":"

Our tasks are quite simple and so its easy to represent them as a single table

  • id (auto-generated)
  • name of task
  • description of task
  • type of task

Each task will have a unique ID, automatically generated when a new record is created.

The name, description and type of task are all strings.

"},{"location":"projects/leiningen/todo-app/database-model/#hinttodo-complete-the-database-model","title":"Hint::TODO: Complete the database model","text":"

The examples in Database model section are not finished, although hopefully you have learned enough to be able to continue working on this for homework.

Please ask questions and share your approaches in the Practicalli Contact channels

These examples will be updated to use next.jdbc over Winter 2020

"},{"location":"projects/leiningen/todo-app/database-model/#hint-the-type-of-task-could-be-managed-by-a-second-table-that-lists-all-the-tasks-however-this-is-only-meant-to-be-a-simple-app-at-this-stage","title":"Hint:: The type of task could be managed by a second table that lists all the tasks. However, this is only meant to be a simple app at this stage.","text":""},{"location":"projects/leiningen/todo-app/database-model/#namespace-design","title":"Namespace design","text":"

We need to decide what namespace to put our data model in. It seems to make sense to create a new namespace, to help keep our code clean and to separate concerns. So we will create a namespace todo-list.list namespace.

We will need to decide whether to add the items namespace to core or to the handlers... or maybe create another handler namespace for handlers that just access the database

(:require [todo-list.items :as items]\n
"},{"location":"projects/leiningen/todo-app/database-model/alternative-approaches/","title":"Alternative approaches","text":""},{"location":"projects/leiningen/todo-app/database-model/alternative-approaches/#hint-here-is-an-alterative-approach-to-the-code-just-created-for-comparison-purposes-only-there-is-no-need-to-implement-any-of-the-following-code-unless-you-prefer-this-approach","title":"Hint:: Here is an alterative approach to the code just created, for comparison purposes only. There is no need to implement any of the following code (unless you prefer this approach)","text":""},{"location":"projects/leiningen/todo-app/database-model/alternative-approaches/#using-uuid-ossp-postgres-plugin","title":"Using UUID-OSSP Postgres plugin","text":"

The UUID-OSSP extension to our Heroku postgres database to autogenerate universal ID's (UUID). These UUID's are managed by postgres and therefore not resistant to braking from code. The database memory overhead for UUID's is typically less than using text based ID's

(defn create-table [db]\n  (db/execute!\n   db\n   [\"CREATE EXTENSION IF NOT EXISTS \\\"UUID-OSSP\\\"\" ])\n  (db/execute!\n   db\n   [\"CREATE TABLE IF NOT EXISTS items\n      (id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),\n       name TEXT NOT NULL,\n       description BOOLEAN NOT NULL DEFAULT FALSE,\n       date_created TIMESTAMPTZ NOT NULL DEFAULT now()\"]))\n

Fixme What is the clojure.java.jdbc version of the above ?

"},{"location":"projects/leiningen/todo-app/database-model/alternative-approaches/#add-more-database-functions","title":"Add more database functions","text":"
(defn create-item [db name description]\n  (:id (first (db/query\n               db [\"INSERT INTO items (name, description)\n                    VALUES (?, ?)\n                    RETURN id\"\n                   name\n                   description]))))\n\n(defn update-item [db id checked]\n  (= [1] (db/execute!\n          db\n          [\"UPDATE items\n            SET checked = ?\n            WHERE id = ?\"\n           checked\n           id])))\n\n(defn delete-item [db id]\n  (= [1] (db/execute!\n          db\n          [\"DELETE FROM items\n            WHERE id = ?\"\n           id])))\n\n(defn read-items [db]\n  (db/query\n   db\n   [\"SELECT id, name, description, checked, date_created\n     FROM items\n     ORDER BY date_created\"]))\n
"},{"location":"projects/leiningen/todo-app/database-model/create-table/","title":"Create table","text":"

We have our database model for tasks, so lets create write some code that will create a database table in Postgres, assuming that table is not there already.

"},{"location":"projects/leiningen/todo-app/database-model/create-table/#hintrecommend-using-nextjdbc","title":"Hint::Recommend using next.jdbc","text":"

next.jdbc is the next generation of clojure.java.jdbc and is recommended instead. The API is very similar, although with many improvements

"},{"location":"projects/leiningen/todo-app/database-model/create-table/#create-items-namespace","title":"Create items namespace","text":"

Create a new Clojure file src/todo_list/items.clj and add the following code

First add a dependency for Clojure.java.jdbc

[clojure.java.jdbc :as sql]\n

You items.clj should look like

(ns todo-list.items\n  (:require [clojure.java.jdbc :as sql]))\n

We only want to create the database if it does not already exist, so we can check if the table is already part of the schema

(defn db-schema-migrated?\n  \"Check if the schema has been migrated to the database\"\n  []\n  (-> (sql/query postgres\n                 [(str \"select count(*) from information_schema.tables \"\n                       \"where table_name='tasks'\")])\n      first :count pos?))\n

Then add a condition to check if the table exists and if not then create the database table

(defn apply-schema-migration\n  \"Apply the schema to the database\"\n  []\n  (when (not (db-schema-migrated?))\n    (sql/db-do-commands postgres\n                        (sql/create-table-ddl\n                         :tasks\n                         [:id :serial \"PRIMARY KEY\"]\n                         [:body :varchar \"NOT NULL\"]\n                         [:created_at :timestamp\n                          \"NOT NULL\" \"DEFAULT CURRENT_TIMESTAMP\"]))))\n
"},{"location":"projects/leiningen/todo-app/database-model/create-table/#what-heroku-does-when-you-create-a-database","title":"What Heroku does when you create a database","text":"

Heroku Postgres users are granted all non-superuser permissions on their database. These include SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER, CREATE, CONNECT, TEMPORARY, EXECUTE, and USAGE.

Heroku runs the SQL below to create a user and database for you.

You cannot create or modify databases and roles on Heroku Postgres. The SQL below is for reference only.

CREATE ROLE user_name;\nALTER ROLE user_name WITH LOGIN PASSWORD 'password' NOSUPERUSER NOCREATEDB NOCREATEROLE;\nCREATE DATABASE database_name OWNER user_name;\nREVOKE ALL ON DATABASE database_name FROM PUBLIC;\nGRANT CONNECT ON DATABASE database_name TO database_user;\nGRANT ALL ON DATABASE database_name TO database_user;\n
"},{"location":"projects/leiningen/todo-app/database-model/create-task/","title":"Create a task","text":"

Write a function to create tasks in the database

(defn create-task [task-name]\n  (sql/insert! postgres\n               :tasks [:body] [task-name])\n  (println task-name))\n
"},{"location":"projects/leiningen/todo-app/database-model/delete-task/","title":"Delete task","text":""},{"location":"projects/leiningen/todo-app/database-model/show-all-task/","title":"Show all tasks","text":"

Write a function to list all the tasks in the database, limited to the first 128 items

(defn all-tasks []\n  (into [] (sql/query postgres [\"select * from tasks order by id desc limit 128\"])))\n
"},{"location":"projects/leiningen/todo-app/heroku/","title":"Deploying to Heroku","text":"

Heroku is a developer-focused Platform as a Service, using the tools developers know well. You can simply push your projects to Heroku using Git and your application is deployed for you automatically.

  • Create a free Heroku account
  • Download the Heroku Toolbelt
"},{"location":"projects/leiningen/todo-app/heroku/#identify-your-laptop-to-heroku","title":"Identify your laptop to Heroku","text":"

To be able to deploy your app to Heroku, you first need establish a trusted connection between your laptop and Heroku. Run the following command (from the Heroku Toolbelt)

heroku login\n

Enter your username and password for Heroku. Also enter your 2-factor authorisation code if you enabled that on your Heroku account.

Credentials are cached so heroku login should only need to be run once per computer and user account.

"},{"location":"projects/leiningen/todo-app/heroku/code-so-far/","title":"Code so far","text":"

The code and configuration we have created so far are in the clojure-todo-list-example repository github repository,

Code for this section is in the branch called ``

If something is not working or you want to speed up, simply clone the project into a new directory using the command:

git clone https://github.com/practicalli/clojure-todo-list-example\n
Once you have cloned the project, checkout the `` branch

git checkout\n
"},{"location":"projects/leiningen/todo-app/heroku/deploy/","title":"Deploy to Heroku","text":"

First we need to create an Heroku app to deploy our Clojure webapp to. This adds a remote repository we can push our code to using Git.

In a command line terminal, navigate to the root of your project (where your project.clj file is)

heroku create\n

If we have changes in our source code files, then we should first add and then commit them to our local repository.

git add .\ngit commit -m \"meaningful commit message\"\n

Now push the Clojure webapp code to Heroku and wait a few moments for it to deploy

git push heroku master\n
"},{"location":"projects/leiningen/todo-app/heroku/deploy/#note-create-an-heroku-app-using-the-heroku-dashboard-or-using-the-following-heroku-toolbelt-command","title":"Note:: Create an Heroku app using the Heroku dashboard or using the following Heroku toolbelt command","text":""},{"location":"projects/leiningen/todo-app/heroku/procfile/","title":"Add Procfile","text":"

The Procfile is a simple text file that instructs Heroku how to build and run an application.

Using the web: directive, we tell Heroku that our application will listen for web traffic (https). Heroku sets a value for the port our application can listen to using the PORT configuration variable (ports are dynamically assigned).

Create a new file called Procfile with the following text

web: java $JVM_OPTS -cp target/todo-list.jar clojure.main -m todo-list.core $PORT\n
"},{"location":"projects/leiningen/todo-app/heroku/procfile/#hintget-webserver-port-from-heroku","title":"Hint::Get webserver PORT from Heroku","text":"

Heroku dynamically assigns a port number for each web: application deployed. The Heroku port is set in the PORT environment variable within Heroku each time the application is deployed.

\"$PORT\" should be an argument to any service that runs a web server (Jetty, HTTPkit server) or the value should be obtained from the Heroku environment from the Clojure code.

"},{"location":"projects/leiningen/todo-app/heroku/procfile/#theory-running-clojure-as-a-java-application","title":"Theory: Running Clojure as a Java application","text":"

When you run a Clojure project with Leiningen, two Java virtual machines (JVM's) are started. One JVM is to run Leiningen and the second JVM is to run your application. By using Leiningen to run your application in production you are using use extra resources and also risk pulling in unnecessary development libraries & configuration only needed during development.

When you run your application in production you can save resources by only running a JVM for your application. This is done by running a Clojure application just like a Java application, using the java command in the Heroku Procfile.

"},{"location":"projects/leiningen/todo-app/heroku/procfile/#theory-building-clojure","title":"Theory: Building Clojure","text":"

Building a Clojure project with Leiningen generates a Java jar file, a packaged version of your application. A jar file generated from Java can be run using java -jar jar-file-name.jar. However to run your Clojure jar file as a Java application you also need to include the the Clojure core library.

Leiningen can also generate an Uberjar. The uberjar is a jar file that also includes the Clojure library as well as your application and libraries. As the uberjar contains Clojure, you can run an uberjar in any Java environment.

By adding an :uberjar entry to the project.clj then the Leiningen command lein uberjar is run during the build and an uberjar is created.

When deploying on Heroku your application is built from your codebase using Leiningen, pulling in all the libraries your application depends on. When your application is run it is done so as a Java application using only the uberjar, starting just the one JVM.

Further reading: https://devcenter.heroku.com/articles/clojure-support

"},{"location":"projects/leiningen/todo-app/heroku/procfile/#theory-identifying-an-entry-point-for-your-application","title":"Theory: Identifying an entry point for your application","text":"

If your main namespace doesn\u2019t have a :gen-class then you can use clojure.main as your entry point and indicate your app\u2019s main namespace using the -m argument in your Procfile:

web: java $JVM_OPTS -cp target/project-standalone.jar clojure.main -m myproject.web $PORT\n
"},{"location":"projects/leiningen/todo-app/heroku/update-project/","title":"Update the project","text":"

Specify how Leiningen builds the project in more detail and tell Heroku how to run the application.

"},{"location":"projects/leiningen/todo-app/heroku/update-project/#configure-the-leiningen-build","title":"Configure the Leiningen Build","text":"

Update the Clojure project file with a minimum version number for Leiningen and a name for the jar file that Leiningen will build

Edit the project.clj file and add the following lines, usually after the dependencies declarations

:min-lein-version \"2.0.0\"\n:uberjar-name \"todo-list.jar\"\n

The update project file should look as follows

(defproject todo-list \"0.1.0-SNAPSHOT\"\n  :description \"A simple webapp using Ring\"\n  :url \"http://example.com/FIXME\"\n  :license {:name \"Eclipse Public License\"\n            :url \"http://www.eclipse.org/legal/epl-v10.html\"}\n  :dependencies [[org.clojure/clojure \"1.6.0\"]\n                 [ring \"1.4.0-beta2\"]\n                 [compojure \"1.3.4\"]]\n  :main todo-list.core\n  :min-lein-version \"2.0.0\"\n  :uberjar-name \"todo-list.jar\"\n  :profiles {:dev\n              {:main todo-list.core/-dev-main}})\n
"},{"location":"projects/leiningen/todo-app/hiccup/","title":"Hiccup - HTML Library","text":"

Hiccup is a library for generating HTML from Clojure, keeping your code consistent and easier to write and maintain.

Using HTML, a heading would be written as:

<h1 class='heading'>I am a heading</h1>\n

Hiccup uses vectors to define HTML tags and maps to represent styles and other attributes. So the same heading in Hiccup would be written as:

[:h1 {:class \"heading\"} \"I am a heading\"]\n
"},{"location":"projects/leiningen/todo-app/hiccup/#using-hiccup","title":"Using Hiccup","text":"

Add the hiccup dependency to your project.clj file

[hiccup \"1.0.5\"]\n

In the REPL, require the hiccup.core library

user=> (require '[hiccup.core :as markup])\n;; => nil\n

Or add hiccup to the namespace definition in your Clojure code file.

(ns my-namespace.core\n  (require '[hiccup.core :as markup]))\n
"},{"location":"projects/leiningen/todo-app/hiccup/#writing-hiccup","title":"Writing Hiccup","text":"

Here is a basic example of Hiccup syntax:

(markup/html [:span {:class \"foo\"} \"bar\"])\n;; => \"<span class=\\\"foo\\\">bar</span>\"\n

The first element of the vector is used as the element name. The second attribute can optionally be a map, in which case it is used to supply the element's attributes. Every other element is considered part of the tag's body.

Hiccup is intelligent enough to render different HTML elements in different ways, in order to accommodate browser quirks:

(markup/html [:script])\n;; => \"<script></script>\"\n\n(html [:p])\n;; =>  \"<p />\"\n

And provides a CSS-like shortcut for denoting id and class attributes:

(markup/html [:div#foo.bar.baz \"bang\"])\n;; =>  \"<div id=\\\"foo\\\" class=\\\"bar baz\\\">bang</div>\"\n

When writing multiple lines of hiccup markup, wrap them in either a [:div ] or a (list ) expression.

[:div\n  [:h1 \"My Picture Album\"]\n  [:img {:src seaside.png} \"A sunny seaside view\"]\n  [:img {:src pier.png} \"A walk along the pier\"]]\n

If the body of the element is a seq, its contents will be expanded out into the element body. This makes working with forms like map and for more convenient:

(html [:ul\n  (for [x (range 1 4)]\n    [:li x])])\n;; => \"<ul><li>1</li><li>2</li><li>3</li></ul>\"\n
(html\n  [:ul\n  (for [x (range 1 4)]\n    [:li x])])\n;; => \"<ul><li>1</li><li>2</li><li>3</li></ul>\"\n

The parent tag will still be rendered in the above example, so

"},{"location":"projects/leiningen/todo-app/hiccup/#hint-hiccup-reference-and-guides","title":"Hint:: Hiccup reference and guides","text":"
  • Hiccup API
  • Hiccup Tips - Lisp Cast
"},{"location":"projects/leiningen/todo-app/hiccup/code-so-far/","title":"Code so far","text":""},{"location":"projects/leiningen/todo-app/hiccup/create-new-handler/","title":"Create a new handler","text":"
(defn trying-hiccup\n  [request]\n  (html5 {:lang \"en\"}\n         [:head (include-js \"myscript.js\") (include-css \"mystyle.css\")]\n         [:body\n           [:div [:h1 {:class \"info\"} \"This is Hiccup\"]]\n           [:div [:p \"Take a look at the HTML generated in this page, compared to the about page\"]]\n           [:div [:p \"Style-wise there is no difference between the pages as we haven't added anything in the stylesheet, however the hiccup page generates a more complete page in terms of HTML\"]]]))\n
"},{"location":"projects/leiningen/todo-app/hiccup/create-new-handler/#hintnamed-content-sections","title":"Hint::Named content sections","text":"

As content grows, refactor it into def expressions to give content sections names. Pages can use names in the handler code that represent the content, simplifying the handler code.

As the project grows, break code into a view namespace with layouts and specific views defined in their own namespace.

"},{"location":"projects/leiningen/todo-app/hiccup/updating-handlers-with-hiccup/","title":"Updating handlers with hiccup","text":"

Instead of including fiddly html code (and having to make sure you close tags), we will write our markup in Clojure syntax using hiccup.

"},{"location":"projects/leiningen/todo-app/hiccup/updating-handlers-with-hiccup/#add-dependencies","title":"Add dependencies","text":"
[hiccup \"1.0.5\"]\n
"},{"location":"projects/leiningen/todo-app/hiccup/updating-handlers-with-hiccup/#note-add-hiccup-dependencies","title":"Note:: Add hiccup dependencies","text":""},{"location":"projects/leiningen/todo-app/hiccup/updating-handlers-with-hiccup/#require-hiccup","title":"Require hiccup","text":"
[hiccup.core :refer :all]\n[hiccup.page :refer :all]\n

Your core.clj file should look like this

(ns todo-list.core\n  (:require [ring.adapter.jetty :as jetty]\n            [ring.middleware.reload :refer [wrap-reload]]\n            [compojure.core :refer [defroutes GET]]\n            [compojure.route :refer [not-found]]\n            [ring.handler.dump :refer [handle-dump]]\n            [hiccup.core :refer :all]\n            [hiccup.page :refer :all]))\n
"},{"location":"projects/leiningen/todo-app/hiccup/updating-handlers-with-hiccup/#note-add-hiccup-to-your-namespace","title":"Note:: Add hiccup to your namespace","text":""},{"location":"projects/leiningen/todo-app/hiccup/updating-handlers-with-hiccup/#update-the-welcome-handler","title":"Update the welcome handler","text":"
(defn welcome\n  \"A ring handler to respond with a simple welcome message\"\n  [request]\n  (html [:h1 \"Hello, Clojure World\"]\n        [:p \"Welcome to your first Clojure app, I now update automatically\"]))\n

The html function create html code based on the keywords used. However, the html function does not create a full html web page.

"},{"location":"projects/leiningen/todo-app/hiccup/updating-handlers-with-hiccup/#note-change-the-welcome-handler-to-use-hiccup-rather-than-html-code","title":"Note:: Change the welcome handler to use hiccup rather than html code","text":""},{"location":"projects/leiningen/todo-app/hiccup/updating-handlers-with-hiccup/#update-the-goodbye-handler","title":"Update the goodbye handler","text":"
(defn goodbye\n  \"A song to wish you goodbye\"\n  [request]\n    (html5 {:lang \"en\"}\n           [:head (include-js \"myscript.js\") (include-css \"mystyle.css\")]\n           [:body\n            [:div [:h1 {:class \"info\"} \"Walking back to happiness\"]]\n            [:div [:p \"Walking back to happiness with you\"]]\n            [:div [:p \"Said, Farewell to loneliness I knew\"]]\n            [:div [:p \"Laid aside foolish pride\"]]\n            [:div [:p \"Learnt the truth from tears I cried\"]]]))\n

Using the html5 function a complete html page is created, with a header and body section.

See the hiccup.page API documentation.

"},{"location":"projects/leiningen/todo-app/hiccup/updating-handlers-with-hiccup/#note-change-the-goodbye-handler-to-use-hiccup-rather-than-html","title":"Note:: Change the goodbye handler to use hiccup rather than html","text":""},{"location":"projects/leiningen/todo-app/introducing-ring/","title":"Introducing Ring","text":"

Web applications typically run on a web or application server, such as Tomcat or Jetty that provide a Java Servlet Container.

The Ring library provides a way to use these servers without being tied to any specific implementation. Ring provides a common way to

  • Write your application using Clojure functions and maps
  • Run your application in an auto-reloading development server (wrap-reload)
  • Compile your application into a Java Servlet application
  • Package your application into a Java war file
  • Use a selection of middleware functions
  • Deploy your application in cloud environments like Heroku

In essence, Ring converts the requests that come from the browser into a Clojure map, the request map. The request map may be passed through one or more middleware functions before being converted to a response map by a handler. The response map may be processed by one or more middleware functions before being converted by Ring to a web server response.

In the following sections you will get a better understanding of

  • how to represent a handler
  • what do requests and responses look like
  • how to separate query parameters from the design of your web app

Ring is the current de facto standard library used to write web applications in Clojure. Higher level frameworks such as Compojure use Ring as a common basis.

"},{"location":"projects/leiningen/todo-app/postgres/","title":"Postgres Database","text":"

Postgres is a modern and powerful relational database that also supports storing of json, xml and object relationships. Postgres has a strong open source community behind it and is actively maintained. Postgres is also highly scalable database with drivers for all the major programming languages.

In this workshop we are going to use Heroku Postgres, a database on demand service that requires no local installation.

"},{"location":"projects/leiningen/todo-app/postgres/#todocontent-may-be-a-little-dated-sorry","title":"TODO::Content may be a little dated, sorry","text":"

Content update during the Winter of 2020

"},{"location":"projects/leiningen/todo-app/postgres/#hint-alternatively-you-can-use-a-local-instance-of-postgres-if-you-are-happy-to-run-it-on-your-laptop","title":"Hint:: Alternatively, you can use a local instance of Postgres if you are happy to run it on your laptop.","text":""},{"location":"projects/leiningen/todo-app/postgres/#nextjdbc-a-modern-approach-to-relational-databases-with-clojure","title":"next.jdbc - a modern approach to relational databases with Clojure","text":"

See the next.jdbc getting started guide for lots of useful information on writing SQL queries in Clojure.

"},{"location":"projects/leiningen/todo-app/postgres/#postgresql-resources","title":"PostgreSQL Resources","text":"
  • Heroku Postgres
  • Amazon Relational Database Service (RDS) - Amazon Aurora, PostgreSQL, MySQL, MariaDB, Oracle Database, and SQL Server
  • IBM Cloud: Postgres services
  • What is PostgreSQL - postgresqltutorial.com
"},{"location":"projects/leiningen/todo-app/postgres/connect-to-heroku-postgres-from-clients/","title":"Connecting to Heroku Postgres from Postgres Clients","text":"
  • Command Line
  • GUI tools
  • Operations tools

Heroku Postgres databases are accessible from anywhere via a secure http connection, so you can connect your favourite Postgres client.

"},{"location":"projects/leiningen/todo-app/postgres/connect-to-heroku-postgres-from-clients/#view-provisioned-postgres-data-stores","title":"View Provisioned Postgres Data stores","text":"

Login to https://data.heroku.com/ to see all the provisioned Heroku Postgres data stores (postgres, redis, etc.) and data clips.

"},{"location":"projects/leiningen/todo-app/postgres/connect-to-heroku-postgres-from-clients/#view-datastore-add-on","title":"View Datastore add-on","text":"

Login to https://heroku.com/ to see the dashboard of Heroku Applications created for that account. Select a specific Application to see what Datastore add-on is attached

"},{"location":"projects/leiningen/todo-app/postgres/connect-to-heroku-postgres-from-clients/#database_url-configuration-variable","title":"DATABASE_URL Configuration Variable","text":"

Provisioning an Heroku Postgres add-on automatically adds a DATABASE_URL environment variable to the Heroku app. Use this value to connect consistently throughout the life of your database from the Heroku application

To see the value of the DATABSAE_URL use the Heroku Toolbelt command heroku config, specifying the app name if you created multiple Heroku apps for the current project

heroku config\n\nheroku config --app my-app-name\n

"},{"location":"projects/leiningen/todo-app/postgres/connect-to-heroku-postgres-from-clients/#hintdatabase-clients-and-other-services","title":"Hint::Database Clients and other Services","text":"

The value of the DATABASE_URL can be used to connect remote database clients (DBeaver, PGAdmin) as well as other services that require a data store.

"},{"location":"projects/leiningen/todo-app/postgres/dataclips/","title":"Heroku Dataclips","text":"

Its very easy to create quick reports on your Heroku Postgres database using Dataclips and share the results with your company.

Heroku Dataclips allow you to write SQL queries that run on Heroku Postgres. You can then share these queries along with the results via a web address.

You can give people the ability to create their own query based on yours and share their version of the query and results. Its kind of Github or Gists for databases.

"},{"location":"projects/leiningen/todo-app/postgres/dataclips/#finding-tables-in-heroku-postgres","title":"Finding tables in Heroku Postgres","text":"
SELECT * FROM pg_catalog.pg_tables WHERE schema-name != 'pg_catalog' AND schema-name != 'information_schema'\n
"},{"location":"projects/leiningen/todo-app/postgres/environment-variables/","title":"Environment Variables","text":"

Add an Heroku Postgres database to the Heroku app creates a DATABASE_URL configuration variable, an environment variable manged by Heroku. This configuration variable can be used to avoid including database connection details in the code repository.

"},{"location":"projects/leiningen/todo-app/postgres/environment-variables/#note-check-your-heroku-app-has-a-database_url-configuration-variable","title":"Note:: Check your Heroku app has a DATABASE_URL configuration variable","text":"

List all the configuration variables for your app using the command:

heroku config\n

The DATABASE_URL contains your username and password for the database, as well as the hostname and database name in the form of:

\"postgres://username:password@hostname/database-name\"\n
"},{"location":"projects/leiningen/todo-app/postgres/environment-variables/#using-postgresql-client-applications","title":"Using PostgreSQL client applications","text":"

The Heroku Postgres database is available via an secure connection from anywhere on the Internet, so you can use these details with your favourite postgres client. The postgres client must connect over SSL, or the connection will be rejected by Heroku postgres.

"},{"location":"projects/leiningen/todo-app/postgres/install/","title":"Postgres install","text":"

Using infrastructure or software as a service databases are provisioned, usually by issuing a simple command or using a web based dashboard for that service.

Using the Heroku app created previously, a Posgres database will be provisioned.

In the root of your Clojure project, run the following Heroku toolbelt command to add a Postgres database to your existing Heroku app

heroku addons:create heroku-postgresql\n
"},{"location":"projects/leiningen/todo-app/postgres/install/#local-postgresql-install","title":"Local PostgreSQL Install","text":"
  • Install Postgresql locally - postgresql.org
  • Ubuntu documentation: PostgreSQL
  • Install and use postgresql on Ubuntu 20.04 - digitalocean
  • Ubuntu Linux PostgreSQL downloads - postgresql.org
"},{"location":"projects/leiningen/todo-app/postgres/jira-ticket/","title":"Jira ticket","text":"

get-connection requires that provided URIs be structured like so:

dbtype://user:password@host:port/database

This is often sufficient, but many PostgreSQL URIs require the use of URI parameters to further configure connections. For example, postgresql.heroku.com provides JDBC URIs like this:

jdbc:postgresql://ec2-22-11-231-117.compute-1.amazonaws.com:5432/d1kuttup5cbafl6?user=pcgoxvmssqabye&password=PFZXtxaLFhIX-nCA0Vi4UbJ6lH&ssl=true

...which, when used outside of Heroku's network, require a further sslfactory=org.postgresql.ssl.NonValidatingFactory parameter.

The PostgreSQL JDBC driver supports a number of different URI parameters, and recommends putting credentials into parameters rather than using the user:password@ convention. Peeking over at the Oracle thin JDBC driver's docs, it appears that it expects credentials using its own idiosyncratic convention, user/password@.

This all leads me to think that get-connection should pass URIs along to DriverManager without modification, and leave URI format conventions up to the drivers involved. For now, my workaround is to do essentially that, using a map like this as input to with-connection et al.:

{:factory #(DriverManager/getConnection (:url %)) :url \"jdbc:postgresql://ec2-22-11-231-117.compute-1.amazonaws.com:5432/d1kuttup5cbafl6?user=pcgoxvmssqabye&password=PFZXtxaLFhIX-nCA0Vi4UbJ6lH&ssl=true\"}

That certainly works, but I presume that such a workaround won't occur to many users, despite the docs/source.

I don't think I've used java.jdbc enough (or RDMBS' enough of late) to comfortably provide a patch (or feel particularly confident in the suggestion above). Hopefully the report is helpful in any case. Activity

All\nComments\nHistory\nActivity\n

Sean Corfield added a comment - 14/Jun/12 1:36 AM - edited

How about an option that takes a map like:

{:connection-uri \"jdbc:postgresql://ec2-22-11-231-117.compute-1.amazonaws.com:5432/d1kuttup5cbafl6?user=pcgoxvmssqabye&password=PFZXtxaLFhIX-nCA0Vi4UbJ6lH&ssl=true\"}

Essentially as a shorthand for the workaround you've come up with? Sean Corfield added a comment - 15/Jun/12 10:20 PM

Try 0.2.3-SNAPSHOT which has support for :connection-uri and let me know if that is a reasonable solution for you? Chas Emerick added a comment - 18/Jun/12 3:35 PM

Yup, 0.2.3-SNAPSHOT's :connection-uri works fine. I've since moved on to using a pooled datasource, but this will hopefully be a more obvious path to newcomers than having to learn about :factory and DriverManager. Sean Corfield added a comment - 18/Jun/12 3:52 PM

Resolved by adding :connection-uri option. Carlos Cunha added a comment - 28/Jul/12 8:09 PM

accessing an heroku database outside heroku, \"sslfactory=org.postgresql.ssl.NonValidatingFactory\" doesn't work. i get \"ERROR: syntax error at or near \"user\" Position: 13 - (class org.postgresql.util.PSQLException\". this happens whether adding it to :subname or :connection-uri Strings

another minor issue - why the documentation of \"with-connection\" (0.2.3) refers the following format for the connection string URI: \"subprotocol://user:password@host:post/subname An optional prefix of jdbc: is allowed.\" but the URI which can actually be parsed successfully is like the one above: jdbc:postgresql://ec2-22-11-231-117.compute-1.amazonaws.com:5432/d1kuttup5cbafl6?user=pcgoxvmssqabye&password=PFZXtxaLFhIX-nCA0Vi4UbJ6lH&ssl=true \"subprotocol://user:password@host:post/subname\" (format like the DATABASE environment variables on heroku) will not be parsed correctly. why the format for the URI that is used on heroku is not supported by the parser?

maybe i'm doing something wrong here

thanks in advance Sean Corfield added a comment - 29/Jul/12 4:57 PM

Carlos, the :connection-uri passes the string directly to the driver with no parsing. The exception you're seeing is coming from inside the PostgreSQL driver so you'll have to consult the documentation for the driver.

The three \"URI\" styles accepted by java.jdbc are:

:connection-uri - passed directly to the driver with no parsing or other logic in java.jdbc,\n:uri - a pre-parsed Java URI object,\na string literal - any optional \"jdbc:\" prefix is ignored, then the string is parsed by logic in java.jdbc, based on the pattern shown (subprotocol://user:password@host:port/subname).\n

If you're using :connection-uri (which is used on its own), you're dealing with the JDBC driver directly.

If you're using :uri or a bare string literal, you're dealing with java.jdbc's parsing (implemented by Phil Hagelberg - of Heroku).

Hope that clarifies? Carlos Cunha added a comment - 29/Jul/12 8:36 PM

Sean, thank you for such comprehensive explanation.

Still, it didn't work with any of the options. I used before a postgres JDBC driver to export to the same database (in an SQL modeller - SQLEditor for the MAC) and it worked (though it would connect some times, but others not). The connection String used was like \"jdbc:postgresql://host:port/database?user=xxx&password=yyy&ssl=true&sslfactory=org.postgresql.ssl.NonValidatingFactory\". The driver name was \"org.postgresql.Driver\" (JDBC4). Anyway, time to give up. I will just use a local database.

Thank you! Carlos Cunha added a comment - 31/Jul/12 7:20 PM

Sean, JDBC combinations were working after. i was neglecting an insert operation in a table with a reserved sql keyword \"user\", so i was getting a \"ERROR: syntax error at or near \"user\" Position: 13\", and therefore the connection was already established at the time.

i'm sorry for all the trouble answering the question (_

thank you Sean Corfield added a comment - 31/Jul/12 7:46 PM

Glad you got to the bottom of it and confirmed that it wasn't a problem in java.jdbc!

"},{"location":"projects/leiningen/todo-app/postgres/lobo-table-creation/","title":"How to use Lobos with Heroku","text":"

http://pupeno.com/2011/08/20/how-to-use-lobos-with-heroku/

Lobos is a Clojure library to create and alter tables which also supports migrations similar to what Rails can do. I like where Lobos is going but it\u2019s a work in progress, so the information here might be out of date soon, beware!

Let\u2019s imagine a project called px (for Project X of course) with the usual Leiningen structure. In the src directory you you need to create a lobos directory and inside there let\u2019s get started with config.clj which contains the credentials and other database information:

(ns lobos.config)\n\n(def db\n  {:classname \"org.postgresql.Driver\"\n   :subprotocol \"postgresql\"\n   :subname \"//localhost:5432/px\"})\n

then we create a simple migration in lobos/migrations.clj that creates the users table:

(ns lobos.migrations\n  (:refer-clojure :exclude [alter defonce drop bigint boolean char double float time])\n  (:use (lobos [migration :only [defmigration]] core schema) lobos.config))\n\n(defmigration create-users\n  (up [] (create (table :users\n                   (integer :id :primary-key)\n                   (varchar :email 256 :unique))))\n  (down [] (drop (table :users))))\n

You run a REPL, load the migrations and run them (using the joyful Clojure example code convention):

(require 'lobos.migrations)\n;=> nil\n(lobos.core/run)\n;=> java.lang.Exception: No such global connection currently open: :default-connection, only got [] (NO_SOURCE_FILE:0)\n

and you get an error because you didn\u2019t open the connection yet, so, let\u2019s do that:

(require 'lobos.connectivity)\n;=> nil\n(lobos.connectivity/open-global lobos.config/db)\n;=> {:default-connection {:connection #<Jdbc4Connection org.postgresql.jdbc4.Jdbc4Connection@2ab600af>, :db-spec {:classname \"org.postgresql.Driver\", :subprotocol \"postgresql\", :subname \"//localhost:5432/px\"}}}\n

and now it works:

(lobos.core/run)\n; create-users\n;=> nil\n

and you can also rollback:

(lobos.core/rollback)\n; create-users\n;=> nil\n

You might be tempted to open the global connection in your config.clj and that might be fine for some, but I found it problematic that the second time I load the file, I get an error: \u201cjava.lang.Exception: A global connection by that name already exists (:default-connection) (NO_SOURCE_FILE:0)\u201d.

My solution was to write a function called open-global-when-necessary that will open a global connection only when there\u2019s none or when the database specification changed, and will close the previous connection in that case, leaving a config.clj that looks like:

(ns lobos.config\n  (:require lobos.connectivity))\n\n(defn open-global-when-necessary\n  \"Open a global connection only when necessary, that is, when no previous\n  connection exist or when db-spec is different to the current global\n  connection.\"\n  [db-spec]\n  ;; If the connection credentials has changed, close the connection.\n  (when (and (@lobos.connectivity/global-connections :default-connection)\n             (not= (:db-spec (@lobos.connectivity/global-connections :default-connection)) db-spec))\n    (lobos.connectivity/close-global))\n  ;; Open a new connection or return the existing one.\n  (if (nil? (@lobos.connectivity/global-connections :default-connection))\n    ((lobos.connectivity/open-global db-spec) :default-connection)\n    (@lobos.connectivity/global-connections :default-connection)))\n\n(def db\n  {:classname \"org.postgresql.Driver\"\n   :subprotocol \"postgresql\"\n   :subname \"//localhost:5432/px\"})\n\n(open-global-when-necessary db)\n

That works fine locally, so let\u2019s move to Heroku. To get started with Clojure on Heroku I recommend you read:

Getting Started With Clojure on Heroku/Cedar\nBuilding a Database-Backed Clojure Web Application\n

I took the code used to extract the database specification from DATABASE_URL but I modified it so I don\u2019t depend on that environment variable existing on my local computer and I ended up with the following config.clj:

(ns lobos.config\n  (:require [clojure.string :as str] lobos.connectivity)\n  (:import (java.net URI)))\n\n(defn heroku-db\n  \"Generate the db map according to Heroku environment when available.\"\n  []\n  (when (System/getenv \"DATABASE_URL\")\n    (let [url (URI. (System/getenv \"DATABASE_URL\"))\n          host (.getHost url)\n          port (if (pos? (.getPort url)) (.getPort url) 5432)\n          path (.getPath url)]\n      (merge\n       {:subname (str \"//\" host \":\" port path)}\n       (when-let [user-info (.getUserInfo url)]\n         {:user (first (str/split user-info #\":\"))\n          :password (second (str/split user-info #\":\"))})))))\n\n(defn open-global-when-necessary\n  \"Open a global connection only when necessary, that is, when no previous\n  connection exist or when db-spec is different to the current global\n  connection.\"\n  [db-spec]\n  ;; If the connection credentials has changed, close the connection.\n  (when (and (@lobos.connectivity/global-connections :default-connection)\n             (not= (:db-spec (@lobos.connectivity/global-connections :default-connection)) db-spec))\n    (lobos.connectivity/close-global))\n  ;; Open a new connection or return the existing one.\n  (if (nil? (@lobos.connectivity/global-connections :default-connection))\n    ((lobos.connectivity/open-global db-spec) :default-connection)\n    (@lobos.connectivity/global-connections :default-connection)))\n\n(def db\n  (merge {:classname \"org.postgresql.Driver\"\n          :subprotocol \"postgresql\"\n          :subname \"//localhost:5432/px\"}\n         (heroku-db)))\n\n(open-global-when-necessary db)\n

After you push to Heroku, you can run heroku run lein repl, load lobos.config and run the migrations just as if they were local.

"},{"location":"projects/leiningen/todo-app/postgres/pg-admin/","title":"pgAdmin","text":""},{"location":"projects/leiningen/todo-app/postgres/postgres-cli/","title":"Postgres CLI","text":"

Heroku toolbelt has many commands for viewing information and querying the Heroku Postgres database. Here is a breakdown of the most commonly used commands.

Postgres Command Line Client required

Heroku Toolbelt pg commands require a working postgres command line client to be installed and available on your operating system path.

Ubuntu documentation: PostgreSQL has details on installing postgresql clients.

"},{"location":"projects/leiningen/todo-app/postgres/postgres-cli/#postgres-information","title":"Postgres Information","text":"

To see all PostgreSQL databases provisioned by your application and the identifying characteristics of each (db size, status, number of tables, PG version, creation date etc\u2026) use the heroku pg:info command.

$ heroku pg:info\n=== HEROKU_POSTGRESQL_BROWN_URL (DATABASE_URL)\nPlan:        Hobby-dev\nStatus:      available\nConnections: 0\nPG Version:  9.3.3\nCreated:     2014-03-20 23:33 UTC\nData Size:   6.5 MB\nTables:      1\nRows:        4/10000 (In compliance)\nFork/Follow: Unsupported\nRollback:    Unsupported\n

To continuously monitor the status of your database, pass pg:info through the unix watch command:

watch heroku pg:info\n

"},{"location":"projects/leiningen/todo-app/postgres/postgres-cli/#running-queries-on-postgres","title":"Running Queries on Postgres","text":"

psql is the native PostgreSQL interactive terminal and is used to execute queries and issue commands to the connected database. To establish a psql session with your remote database use heroku pg:psql. You must have PostgreSQL installed on your system to use heroku pg:psql.

$ heroku pg:psql\n---> Connecting to HEROKU_POSTGRESQL_BROWN_URL (DATABASE_URL)\npsql (9.2.6, server 9.3.3)\nWARNING: psql version 9.2, server version 9.3.\n         Some psql features might not work.\nSSL connection (cipher: DHE-RSA-AES256-SHA, bits: 256)\nType \"help\" for help.\n\nheroku-app-name::BROWN=> \\dt\n               List of relations\n Schema |     Name     | Type  |     Owner\n--------|--------------|-------|----------------\n public | pl0_programs | table | moiwgreelvvujc\n(1 row)\n\nheroku-app-name::BROWN=>\nheroku-app-name::BROWN=> SELECT * FROM pl0_programs;\n  name  |           source\n--------|-----------------------------\n 3m2m1  |                     3-2-1\\r+\n        |\n ap1tb  | a+1*b\\r                    +\n        |\n test   |                     a+1*b\\r+\n        |           \\r               +\n        |\n lolwut |                     3-2-1\\r+\n        |\n(4 rows)\n

If you have more than one database, specify the database to connect to as the first argument to the command (the database located at DATABASE_URL is used by default).

$ heroku pg:psql HEROKU_POSTGRESQL_GRAY\nConnecting to HEROKU_POSTGRESQL_GRAY... done\n
"},{"location":"projects/leiningen/todo-app/postgres/postgres-cli/#reset-your-database","title":"Reset your database","text":"

To drop and recreate your database use heroku pg:reset

$ heroku pg:reset DATABASE\n\n !    WARNING: Destructive Action\n !    This command will affect the app: heroku-app-name\n !    To proceed, type \"pegjspl0\" or re-run this command with --confirm heroku-app-name\n\n> heroku-app-name\nResetting HEROKU_POSTGRESQL_BROWN_URL (DATABASE_URL)... done\n

Then restart the server

$ heroku ps:restart\nRestarting dynos... done\n

There are many more Heroku toolbelt commands you can use for postgres. [TODO: Link to postgres command]

"},{"location":"projects/leiningen/todo-app/postgres/postgres-cli/#resources","title":"Resources","text":"
  • Accessing a Database - postgresql.org
  • Ubuntu documentation: PostgreSQL - client and server install
"},{"location":"projects/leiningen/todo-app/postgres/postgres-commands/","title":"pg:info","text":"

To see all PostgreSQL databases provisioned by your application and the identifying characteristics of each (db size, status, number of tables, PG version, creation date etc\u2026) use the heroku pg:info command.

heroku pg:info\n=== HEROKU_POSTGRESQL_BROWN_URL (DATABASE_URL)\nPlan:        Hobby-dev\nStatus:      available\nConnections: 0\nPG Version:  9.3.3\nCreated:     2014-03-20 23:33 UTC\nData Size:   6.5 MB\nTables:      1\nRows:        4/10000 (In compliance)\nFork/Follow: Unsupported\nRollback:    Unsupported\n\nTo continuously monitor the status of your database, pass pg:info through the unix watch command:\n\nwatch heroku pg:info\n-bash: watch: no se encontr\u00f3 la orden\nbrew install watch\nwatch heroku pg:info\n...\n
"},{"location":"projects/leiningen/todo-app/postgres/postgres-commands/#pgpsql","title":"pg:psql","text":"

psql is the native PostgreSQL interactive terminal and is used to execute queries and issue commands to the connected database.

To establish a psql session with your remote database use heroku pg:psql. You must have PostgreSQL installed on your system to use heroku pg:psql.

heroku pg:psql\n---> Connecting to HEROKU_POSTGRESQL_BROWN_URL (DATABASE_URL)\npsql (9.2.6, server 9.3.3)\nWARNING: psql version 9.2, server version 9.3.\n         Some psql features might not work.\nSSL connection (cipher: DHE-RSA-AES256-SHA, bits: 256)\nType \"help\" for help.\n\npegjspl0::BROWN=> \\dt\n               List of relations\n Schema |     Name     | Type  |     Owner\n--------|--------------|-------|----------------\n public | pl0_programs | table | moiwgreelvvujc\n(1 row)\n\npegjspl0::BROWN=>\npegjspl0::BROWN=> SELECT * FROM pl0_programs;\n  name  |           source\n--------|-----------------------------\n 3m2m1  |                     3-2-1\\r+\n        |\n ap1tb  | a+1*b\\r                    +\n        |\n test   |                     a+1*b\\r+\n        |           \\r               +\n        |\n lolwut |                     3-2-1\\r+\n        |\n(4 rows)\n

If you have more than one database, specify the database to connect to as the first argument to the command (the database located at DATABASE_URL is used by default).

heroku pg:psql HEROKU_POSTGRESQL_GRAY\nConnecting to HEROKU_POSTGRESQL_GRAY... done\n...\n
"},{"location":"projects/leiningen/todo-app/postgres/postgres-commands/#pgreset","title":"pg:reset","text":"

To drop and recreate your database use pg:reset:

heroku pg:reset DATABASE\n\n !    WARNING: Destructive Action\n !    This command will affect the app: pegjspl0\n !    To proceed, type \"pegjspl0\" or re-run this command with --confirm pegjspl0\n\n> pegjspl0\nResetting HEROKU_POSTGRESQL_BROWN_URL (DATABASE_URL)... done\n\nheroku ps:restart\nRestarting dynos... done\n
"},{"location":"projects/leiningen/todo-app/postgres/postgres-commands/#pgpull","title":"pg:pull","text":"

pg:pull can be used to pull remote data from a Heroku Postgres database to a database on your local machine. The command looks like this:

pg_ctl -D /usr/local/var/postgres -l /usr/local/var/postgres/server.log start\nserver starting\n
"},{"location":"projects/leiningen/todo-app/postgres/postgres-commands/#create-local-database","title":"Create local database","text":"
heroku pg:pull HEROKU_POSTGRESQL_MAGENTA mylocaldb --app sushi\n

Create a new local database named mylocaldb, then pull data from database at DATABASE_URL from the app sushi.

In order to prevent accidental data overwrites and loss, the local database must not exist. You will be prompted to drop an already existing local database before proceeding.

"},{"location":"projects/leiningen/todo-app/postgres/postgres-commands/#pgpush","title":"pg:push","text":"

Like pull but in reverse, pg:push will push data from a local database into a remote Heroku Postgres database. The command looks like this:

heroku pg:push mylocaldb HEROKU_POSTGRESQL_MAGENTA --app sushi\n

This command will take the local database mylocaldb and push it to the database at DATABASE_URL on the app sushi. In order to prevent accidental data overwrites and loss, the remote database must be empty. You will be prompted to pg:reset an already a remote database that is not empty.

"},{"location":"projects/leiningen/todo-app/postgres/postgres-commands/#backups","title":"Backups","text":"

Heroku Postgres Backups service automates backup of the database pointed to by the DATABASE_URL environment variable in the Heroku app. Ensure the database is promoted

heroku pg:promote HEROKU_POSTGRESQL_VIOLET --app your-app\n
"},{"location":"projects/leiningen/todo-app/postgres/postgres-commands/#monitoring-database-provisioning","title":"Monitoring database provisioning","text":"

When provisioning larger databases, they may take several minutes to become available. Using the heroku pg:wait command you can see when the database provisioning is complete.

You may also want to use heroku pg:wait when putting your application into maintenenace mod [TODO: expand on this]

heroku help pg:wait\n\n\nUsage: heroku pg:wait [DATABASE]\n\n monitor database creation, exit when complete\n\n defaults to all databases if no DATABASE is specified\n\n --wait-interval SECONDS      # how frequently to poll (to avoid rate-limiting)\n
"},{"location":"projects/leiningen/todo-app/postgres/postgres-commands/#setting-a-name-for-a-new-database","title":"Setting a name for a new database","text":"

Once Heroku Postgres has been added a HEROKU_POSTGRESQL_COLOR_URL setting will be available in the app configuration and will contain the URL used to access the newly provisioned Heroku Postgres service. This can be confirmed using the heroku config command.

heroku config -s | grep HEROKU_POSTGRESQL\nHEROKU_POSTGRESQL_RED_URL=postgres://username:password@hostname.domain.com:1234/database-name\n

You can choose the alias that the add-on uses on the application using the --as flag. This will affect the name of the variable the add-on adds to the application:

heroku addons:create heroku-postgresql:hobby-dev --as USERS_DB\nAdding heroku-postgresql:hobby-dev to sushi... done, v69 (free)\nAttached as USERS_DB\nDatabase has been created and is available\n\n heroku config -s | grep USERS_DB\nUSERS_DB_URL=postgres://postgres://username:password@hostname.domain.com:1234/database-name\n
"},{"location":"projects/leiningen/todo-app/postgres/postgres-performance-analytics/","title":"Performance Analytics","text":"

Performance Analytics is the visibility suite for Heroku Postgres. It enables you to monitor the performance of your database and to diagnose potential problems. It consists of several components:

"},{"location":"projects/leiningen/todo-app/postgres/postgres-performance-analytics/#expensive-queries","title":"Expensive Queries","text":"

The leading cause of poor database performance are queries that are not optimised . Expensive Queries reports, available through the Heroku dashboard helps to identify and understand the queries that take the most time in your database.

"},{"location":"projects/leiningen/todo-app/postgres/postgres-performance-analytics/#logging","title":"Logging","text":"

If your service emits logs on database access, you will be able to retrieve them through Heroku\u2019s log-stream:

 heroku logs -t\n

To see logs from the database service itself you can also use heroku logs but with the -p postgres flag indicating that you only wish to see the logs from PostgreSQL.

 heroku logs -p postgres -t\n

In order to have minimal impact on database performance, logs are delivered on a best-effort basis.

Read more about Heroku Postgres log statements here.

"},{"location":"projects/leiningen/todo-app/postgres/postgres-performance-analytics/#pgdiagnose","title":"pg:diagnose","text":"

pg:diagnose performs a number of useful health and diagnostic checks that help analyst and optimize the performance of a database. The report that can be shared with others on your team or with Heroku Support.

Before taking any action based on a report, be sure to carefully consider the impact to your database and application.

 heroku pg:diagnose --app sushi\nReport 1234abc\u2026 for sushi::HEROKU_POSTGRESQL_MAROON_URL\navailable for one month after creation on 2014-07-03 21:29:40.868968+00\n\nGREEN: Connection Count\nGREEN: Long Queries\nGREEN: Idle in Transaction\nGREEN: Indexes\nGREEN: Bloat\nGREEN: Hit Rate\nGREEN: Blocking Queries\nGREEN: Load\n
"},{"location":"projects/leiningen/todo-app/postgres/postgres-performance-analytics/#check-connection-count","title":"Check: Connection Count","text":"

Each Postgres connection requires memory. And database plans have a limit on the number of connections they can accept. If you are using too many connections you may want to consider using a connection pool such as PgBouncer or migrating to a larger plan with more RAM.

"},{"location":"projects/leiningen/todo-app/postgres/postgres-performance-analytics/#long-running-queries-idle-in-transaction","title":"Long Running Queries, Idle in Transaction","text":"

Long-running queries and transactions can cause problems with bloat that prevents auto vacuuming and causes followers to lag behind. They also create locks on your data which can prevent other transactions from running. You may want to consider killing the long running query with pg:kill.

"},{"location":"projects/leiningen/todo-app/postgres/postgres-performance-analytics/#check-indexes","title":"Check: Indexes","text":"

The Indexes check includes three classes of indexes.

Never Used Indexes have not been used (since the last manual database statistics refresh). These indexes are typically safe to drop, unless they are in use on a follower.

Low Scans, High Writes indexes are used, but infrequently relative to their write volume. Indexes are updated on every write, so are especially costly on a high write table. Consider the cost of slower writes against the performance improvements that these indexes provide.

Seldom used Large Indexes are not used often and take up a significant space both on disk and in cache (RAM). These indexes may still be important to your application for example if they are used by periodic jobs or infrequent traffic patterns.

Index usage is only tracked on the database receiving the query. If you use followers for reads, this check will not account for usage made against the follower and is likely inaccurate.

"},{"location":"projects/leiningen/todo-app/postgres/postgres-performance-analytics/#check-bloat","title":"Check: Bloat","text":"

Because Postgres uses MVCC, old versions of updated or deleted rows are simply made invisible rather than modified in place.

Under normal operation an auto vacuum process goes through and asynchronously cleans these up. However sometimes it cannot work fast enough or otherwise cannot prevent some tables from becoming bloated.

High bloat can slow down queries, waste space, and even increase load as the database spends more time looking through dead rows.

You can manually vacuum a table with the VACUUM (VERBOSE, ANALYZE); command in psql. If this occurs frequently you may want to make auto-vacuum more aggressive.

"},{"location":"projects/leiningen/todo-app/postgres/postgres-performance-analytics/#check-hit-rate","title":"Check: Hit Rate","text":"

This checks the overall index hit rate, the overall cache hit rate, and the individual index hit rate per table. It is very important to keep hit rates in the 99+% range. Databases with lower hit rates perform significantly worse as they have to hit disk instead of reading from memory. Consider migrating to a larger plan for low cache hit rates, and adding appropriate indexes for low index hit rates. Check: Blocking Queries

Some queries can take locks that block other queries from running. Normally these locks are acquired and released very quickly and do not cause any issues. In pathological situations however some queries can take locks that cause significant problems if held too long. You may want to consider killing the query with pg:kill.

"},{"location":"projects/leiningen/todo-app/postgres/postgres-performance-analytics/#check-load","title":"Check: Load","text":"

There are many, many reasons that load can be high on a database: bloat, CPU intensive queries, index building, and simply too much activity on the database. Review your access patterns, and consider migrating to a larger plan which would have a more powerful processor.

"},{"location":"projects/leiningen/todo-app/postgres/postgres-toolbelt-commands/","title":"Postgres toolbelt commands","text":""},{"location":"projects/leiningen/todo-app/postgres/postgres-toolbelt-commands/#heroku-toolbelt-for-postgres","title":"Heroku Toolbelt for Postgres","text":"

Heroku Postgres is integrated directly into the Heroku toolbelt and offers several commands that automate many common tasks associated with managing a database-backed application.

psql required for some commands

Some commands require a postgres client to be installed on your computer to work

"},{"location":"projects/leiningen/todo-app/postgres/postgres-toolbelt-commands/#pginfo","title":"pg:info","text":"

To see all PostgreSQL databases provisioned by your application and the identifying characteristics of each (db size, status, number of tables, PG version, creation date etc\u2026) use the heroku pg:info command.

 heroku pg:info\n=== HEROKU_POSTGRESQL_RED\nPlan         Standard 0\nStatus       available\nData Size    82.8 GB\nTables       13\nPG Version   9.1.3\nCreated      2012-02-15 09:58 PDT\n=== HEROKU_POSTGRESQL_GRAY\nPlan         Standard 2\nStatus       available\nData Size    82.8 GB\n

To continuously monitor the status of your database, pass pg:info through the unix watch command:

 watch heroku pg:info\n
"},{"location":"projects/leiningen/todo-app/postgres/postgres-toolbelt-commands/#pgpsql","title":"pg:psql","text":"

psql is the native PostgreSQL interactive terminal and is used to execute queries and issue commands to the connected database.

To establish a psql session with your remote database use heroku pg:psql.

You must have PostgreSQL installed on your system to use heroku pg:psql.

 heroku pg:psql\nConnecting to HEROKU_POSTGRESQL_RED... done\npsql (9.1.3, server 9.1.3)\nSSL connection (cipher: DHE-RSA-AES256-SHA, bits: 256)\nType \"help\" for help.\n\nrd2lk8ev3jt5j50=> SELECT * FROM users;\n\nIf you have more than one database, specify the database to connect to (just the color works as a shorthand) as the first argument to the command (the database located at DATABASE_URL is used by default).\n\n heroku pg:psql gray\nConnecting to HEROKU_POSTGRESQL_GRAY... done\n
"},{"location":"projects/leiningen/todo-app/postgres/postgres-toolbelt-commands/#pgpush-and-pgpull","title":"pg:push and pg:pull","text":"

pg:pull can be used to pull remote data from a Heroku Postgres database to a database on your local machine. The command looks like this:

 heroku pg:pull HEROKU_POSTGRESQL_MAGENTA mylocaldb --app sushi\n

This command will create a new local database named \u201cmylocaldb\u201d and then pull data from database at DATABASE_URL from the app \u201csushi\u201d. In order to prevent accidental data overwrites and loss, the local database must not exist. You will be prompted to drop an already existing local database before proceeding.

If providing a Postgres user or password for your local DB is necessary, use the appropriate environment variables like so:

PGUSER=postgres PGPASSWORD=password heroku pg:pull HEROKU_POSTGRESQL_MAGENTA mylocaldb --app sushi

Note: like all pg:* commands you can use the shorthand identifiers here, so to pull data from HEROKU_POSTGRESQL_RED on the app \u201csushi\u201d you could do heroku pg:pull sushi::RED mylocaldb. pg:push

Like pull but in reverse, pg:push will push data from a local database into a remote Heroku Postgres database. The command looks like this:

 heroku pg:push mylocaldb HEROKU_POSTGRESQL_MAGENTA --app sushi\n

This command will take the local database \u201cmylocaldb\u201d and push it to the database at DATABASE_URL on the app \u201csushi\u201d. In order to prevent accidental data overwrites and loss, the remote database must be empty. You will be prompted to pg:reset an already a remote database that is not empty.

Usage of the PGUSER and PGPASSWORD for your local database is also supported for pg:push, just like for the pg:pull commands. Troubleshooting

These commands rely on the pg_dump and pg_restore binaries that are included in a Postgres installation. It is somewhat common, however, for the wrong binaries to be loaded in $PATH. Errors such as

!    createdb: could not connect to database postgres: could not connect to server: No such file or directory\n!      Is the server running locally and accepting\n!      connections on Unix domain socket \"/var/pgsql_socket/.s.PGSQL.5432\"?\n!\n!    Unable to create new local database. Ensure your local Postgres is working and try again.\n
and

pg_dump: server version: 9.3.1; pg_dump version: 9.1.5\npg_dump: aborting because of server version mismatch\npg_dump: *** aborted because of error\npg_restore: [archiver] input file is too short (read 0, expected 5)\n

are both often a result of this incorrect $PATH problem. This problem is especially common with Postgres.app users, as the post-install step of adding /Applications/Postgres.app/Contents/MacOS/bin to $PATH is easy to forget.

"},{"location":"projects/leiningen/todo-app/postgres/postgres-toolbelt-commands/#pgps-pgkill-pgkillall","title":"pg:ps pg:kill pg:killall","text":"

These commands give you view and control over currently running queries.

The pg:ps command queries the pg_stat_statements table in postgres to give a concise view into currently running queries.

 heroku pg:ps\n procpid |         source            |   running_for   | waiting |         query\n---------|---------------------------|-----------------|---------|-----------------------\n   31776 | psql                      | 00:19:08.017088 | f       | <IDLE> in transaction\n   31912 | psql                      | 00:18:56.12178  | t       | select * from hello;\n   32670 | Heroku Postgres Data Clip | 00:00:25.625609 | f       | BEGIN READ ONLY; select 'hi'\n(3 rows)\n

The procpid column can then be used to cancel or terminate those queries with pg:kill. Without any arguments pg_cancel_backend is called on the query which will attempt to cancel the query. In some situations that can fail, in which case the --force option can be used to issue pg_terminate_backend which drops the entire connection for that query.

 heroku pg:kill 31912\n pg_cancel_backend\n-------------------\n t\n(1 row)\n\n heroku pg:kill --force 32670\n pg_terminate_backend\n----------------------\n t\n(1 row)\n

pg:killall is similar to pg:kill except it will cancel or terminate every query on your database.

"},{"location":"projects/leiningen/todo-app/postgres/postgres-toolbelt-commands/#pgpromote","title":"pg:promote","text":"

In setups where more than one database is provisioned (common use-cases include a master/slave high-availability setup or as part of the database upgrade process) it is often necessary to promote an auxiliary database to the primary role. This is accomplished with the heroku pg:promote command.

 heroku pg:promote HEROKU_POSTGRESQL_GRAY_URL\nPromoting HEROKU_POSTGRESQL_GRAY_URL to DATABASE_URL... done\n

pg:promote works by setting the value of the DATABASE_URL config var (which your application uses to connect to the primary database) to the newly promoted database\u2019s URL and restarting your app. The old primary database location is still accessible via its HEROKU_POSTGRESQL_COLOR_URL setting.

After a promotion, the demoted database is still provisioned and incurring charges. If it\u2019s no longer need you can remove it with

heroku addons:destroy HEROKU_POSTGRESQL_COLOR.\n
"},{"location":"projects/leiningen/todo-app/postgres/postgres-toolbelt-commands/#pgcredentials","title":"pg:credentials","text":"

Heroku Postgres provides convenient access to the credentials and location of your database should you want to use a GUI to access your instance.

The database name argument must be provided with pg:credentials command. Use DATABASE for your primary database.

 heroku pg:credentials DATABASE\nConnection info string:\n   \"dbname=dee932clc3mg8h host=ec2-123-73-145-214.compute-1.amazonaws.com port=6212 user=user3121 password=98kd8a9 sslmode=require\"\n

It is a good security practice to rotate the credentials for important services on a regular basis. On Heroku Postgres this can be done with heroku pg:credentials --reset.

 heroku pg:credentials HEROKU_POSTGRESQL_GRAY_URL --reset\n

New credentials are created for the database and the related config vars on your Heroku application are updated.

On Standard, Premium, and Enterprise tier databases the old credentials are not removed immediately.

All of the open connections remain open until the currently running tasks complete, then those credentials are updated. This is to make sure that any background jobs or other workers running on your production environment aren\u2019t abruptly terminated, potentially leaving the system in an inconsistent state.

"},{"location":"projects/leiningen/todo-app/postgres/postgres-toolbelt-commands/#pgreset","title":"pg:reset","text":"

The PostgreSQL user your database is assigned doesn\u2019t have permission to create or drop databases. To drop and recreate your database use pg:reset.

 heroku pg:reset DATABASE\n
"},{"location":"projects/leiningen/todo-app/refactor-namespace/","title":"Refactor Core Namespace","text":"

The todo-list.core namespace is getting quite full and only going to get more code, unless we refactor the design and create some additional namespaces.

A namespace is a way to group behaviour (functions) and data (data structures / defs) in one scope. Functions can be defined as private to that scope, so only other functions in the same namespace can call them.

"},{"location":"projects/leiningen/todo-app/refactor-namespace/base-routes/","title":"Base routes","text":"

handlers.clj should look as follows:

(ns todo-list.handlers\n   (:use\n     [hiccup.core]\n     [hiccup.page]))\n\n\n(defn welcome\n  \"A ring handler to respond with a simple welcome message\"\n  [request]\n  (html [:h1 \"Hello, Clojure World\"]\n        [:p \"Welcome to your first Clojure app, I now update automatically\"]))\n\n(defn goodbye\n  \"A song to wish you goodbye\"\n  [request]\n    (html5 {:lang \"en\"}\n           [:head (include-js \"myscript.js\") (include-css \"mystyle.css\")]\n           [:body\n            [:div [:h1 {:class \"info\"} \"Walking back to happiness\"]]\n            [:div [:p \"Walking back to happiness with you\"]]\n            [:div [:p \"Said, Farewell to loneliness I knew\"]]\n            [:div [:p \"Laid aside foolish pride\"]]\n            [:div [:p \"Learnt the truth from tears I cried\"]]]))\n\n(defn about\n  \"Information about the website developer\"\n  [request]\n  (html [:h1 \"About the Website\"]\n        [:p \"I am an awesome Clojure developer, well getting there... trying some Hiccup now\"]))\n\n(defn hello\n  \"A simple personalised greeting showing the use of variable path elements\"\n  [request]\n  (let [name (get-in request [:route-params :name])]\n    {:status 200\n     :body (str \"Hello \" name \".  I got your name from the web URL\")\n     :headers {}}))\n\n(def operands {\"+\" + \"-\" - \"*\" * \":\" /})\n\n(defn calculator\n  \"A very simple calculator that can add, divide, subtract and multiply.  This is done through the magic of variable path elements.\"\n  [request]\n  (let [a  (Integer. (get-in request [:route-params :a]))\n        b  (Integer. (get-in request [:route-params :b]))\n        op (get-in request [:route-params :op])\n        f  (get operands op)]\n    (if f\n      {:status 200\n       :body (str \"<h1>Result = \" (f a b) \"</h1>\")\n       :headers {}}\n      {:status 404\n       :body \"Sorry, unknown operator.  I only recognise + - * : (: is for division)\"\n       :headers {}})))\n\n(defn trying-hiccup\n  [request]\n  (html5 {:lang \"en\"}\n         [:head (include-js \"myscript.js\") (include-css \"mystyle.css\")]\n         [:body\n           [:div [:h1 {:class \"info\"} \"This is Hiccup\"]]\n           [:div [:p \"Take a look at the HTML generated in this page, compared to the about page\"]]\n           [:div [:p \"Style-wise there is no difference between the pages as we haven't added anything in the stylesheet, however the hiccup page generates a more complete page in terms of HTML\"]]]))\n
"},{"location":"projects/leiningen/todo-app/refactor-namespace/base-routes/#note-create-a-new-file-called-srctodo_listhandlersbase-routesclj-and-move-all-the-handler-code-into-this-file-from-srctodo_listcore-make-sure-you-also-move-the-hiccup-libraries-into-the-new-handlers-namespace","title":"Note:: Create a new file called src/todo_list/handlers/base-routes.clj and move all the handler code into this file from src/todo_list/core. Make sure you also move the hiccup libraries into the new handlers namespace.","text":""},{"location":"projects/leiningen/todo-app/refactor-namespace/code-so-far/","title":"Code so far","text":""},{"location":"projects/leiningen/todo-app/refactor-namespace/core/","title":"Refactored Core","text":"

Refactor the core namespace to contain the code that starts up our server and join up all the route handlers.

Edit the src/todo_list/core.clj file and update the namespace definition to include the new handlers namespace, including the whole namespace in core.

(ns todo-list.core\n  (:require [compojure.core          :refer [routes]]\n            [todo-list.handlers.play :refer [play-routes]]\n            [todo-list.handlers.task :refer [task-routes]]\n            [todo-list.handlers.base :refer [base-routes]]\n            [ring.middleware.reload  :refer [wrap-reload]]\n            [ring.adapter.jetty      :as    jetty]))\n

Change app from a defroutes to a def and use the route function to merge all the defroutes into one

(def app\n  (routes #'play-routes #'base-routes #'task-routes))\n

The routes function takes the names of all the other defroutes and merges into one list of handlers.

app is now just a name we give to reference the handlers for all routes. We can continue to add more defroutes to app as our application grows, along with any middleware we wish to apply to our handlers.

core should be much smaller, containing only the route definition and the main app (plus the middleware around the app)

(ns todo-list.core\n  (:require [compojure.core          :refer [routes]]\n            [todo-list.handlers.play :refer [play-routes]]\n            [todo-list.handlers.task :refer [task-routes]]\n            [todo-list.handlers.base :refer [base-routes]]\n            [ring.middleware.reload  :refer [wrap-reload]]\n            [ring.adapter.jetty      :as    jetty]))\n\n(def app\n  (routes #'play-routes #'base-routes #'task-routes))\n\n(defn -main\n  \"A very simple web server using Ring & Jetty\"\n  [port-number]\n  (jetty/run-jetty app\n    {:port (Integer. port-number)}))\n\n(defn -dev-main\n  \"A very simple web server using Ring & Jetty that reloads code changes via the development profile of Leiningen\"\n  [port-number]\n  (jetty/run-jetty (wrap-reload #'app)\n                   {:port (Integer. port-number)}))\n
"},{"location":"projects/leiningen/todo-app/refactor-namespace/play-routes/","title":"Play routes","text":""},{"location":"projects/leiningen/todo-app/refactor-namespace/task-routes/","title":"Task routes","text":""},{"location":"projects/leiningen/todo-app/reloading-the-application/","title":"Automatic reloading with wrap-reload middleware","text":"

wrap-reload is a ring middleware function that will push all our code changes to the application each time we save.

"},{"location":"projects/leiningen/todo-app/reloading-the-application/#note-include-wrap-reload-in-the-namespace-of-our-project","title":"Note:: Include wrap-reload in the namespace of our project","text":"

Require the wrap-reload directly into the namespace

(ns todo-list.core\n  (:require [ring.adapter.jetty :as jetty]\n            [ring.middleware.reload :refer [wrap-reload]]))\n
"},{"location":"projects/leiningen/todo-app/reloading-the-application/#define-a-function-to-use-wrap-reload","title":"Define a function to use wrap-reload","text":"

A function called -dev-main will run the reloading web server when we are developing, ensuring we only use the wrap-reload function during development.

"},{"location":"projects/leiningen/todo-app/reloading-the-application/#note-create-a-dev-main-function","title":"Note:: Create a -dev-main function","text":"

The -dev-main function is the same as -main, except we use the wrap-reload middleware around the welcome function. Each time you change the welcome function definition it will be reloaded.

Using the quote reader macro, #' in front of the welcome function name tells Clojure to skip evaluation of the function and reference the name of the function instead. This allows the wrap-reload middleware to decide when to evaluate the welcome function.

(defn -dev-main\n  \"A very simple web server using Ring & Jetty,\n  called via the development profile of Leiningen\n  which reloads code changes using ring middleware wrap-reload\"\n  [port-number]\n  (webserver/run-jetty\n    (wrap-reload #'welcome)\n    {:port  (Integer. port-number)\n     :join? false}))\n
"},{"location":"projects/leiningen/todo-app/reloading-the-application/#notetweak-the-main-function-for-production","title":"Note::Tweak the -main function for production","text":"

The -main function is typically called on the command line when run in production, so we want to be connected to the output of the webserver.

Remove the :join? false option for the embedded Jetty server, so the output of the server is displayed

(defn -main\n  \"A very simple web server using Ring & Jetty\n  Production mode operation, no reloading.\"\n  [port-number]\n  (webserver/run-jetty\n    welcome\n    {:port (Integer. port-number)}))\n
"},{"location":"projects/leiningen/todo-app/reloading-the-application/#configure-the-dev-profile-in-your-project","title":"Configure the dev profile in your project","text":"

When you start your Clojure webapp with lein run it looks for main class to run in the :dev profile first. So we need to create a :dev profile.

:dev profile that sets -dev-main to be the starting point of our application. This

:profiles {:dev\n            {:main todo-list.core/-dev-main}}\n

The project.clj file should look like the following:

(defproject todo-list \"0.1.0-SNAPSHOT\"\n\n  :description \"A Todo List server-side webapp using Ring & Compojure\"\n  :url \"https://github.com/practicalli/clojure-todo-list-example\"\n\n  :license {:name \"Creative Commons Attribution Share-Alike 4.0 International\"\n            :url  \"https://creativecommons.org\"}\n\n  :dependencies [[org.clojure/clojure \"1.10.1\"]\n                 [ring \"1.8.0\"]]\n\n  :repl-options {:init-ns todo-list.core}\n\n  :main todo-list.core\n\n  :profiles {:dev\n             {:main todo-list.core/-dev-main}})\n
"},{"location":"projects/leiningen/todo-app/reloading-the-application/#noteadd-profile-to-project-configuration","title":"Note::Add profile to project configuration","text":"

Edit the project.clj and create a :dev profile to define the initial function to call when starting our webapp.

"},{"location":"projects/leiningen/todo-app/reloading-the-application/code-so-far/","title":"The code so far","text":"

The code and configuration we have created so far are in the clojure-todo-list-example repository github repository.

Code for this section is in the branch called 03-reloading-the-application

If something is not working or you want to speed up, simply clone the project into a new directory using the command:

git clone https://github.com/practicalli/clojure-todo-list-example\n
Once you have cloned the project, checkout the 03-reloading-the-application branch

git checkout 03-reloading-the-application\n
"},{"location":"projects/leiningen/todo-app/reloading-the-application/middleware/","title":"Middleware in Ring","text":"

Middleware in ring is a way to modify the incoming requests or outgoing responses.

Middleware can also wrap handlers or other middleware, affecting their behaviour. For example the wrap-reload middleware enables live reloading by detecting file changes and reloading affected functions into their namespace, before the request is passed to the relevant handler function

Middleware in ring/ring-core

  • wrap-cookies (ring.middleware.cookies)
  • wrap-file (ring.middleware.file)
  • wrap-file-info (ring.middleware.file-info)
  • wrap-flash (ring.middleware.flash)
  • wrap-keyword-params (ring.middleware.keyword-params)
  • wrap-multipart-params (ring.middleware.multipart-params
  • wrap-nested-params (ring.middleware.nested-params
  • wrap-params (ring.middleware.params)
  • wrap-session (ring.middleware.session)

Middleware in ring/ring-devel

  • wrap-lint (ring.middleware.lint)
  • wrap-reload (ring.middleware.reload)
  • wrap-stacktrace (ring.middleware.stacktrace)
"},{"location":"projects/leiningen/todo-app/reloading-the-application/test-your-code-reloads/","title":"Test the wrap-reload middleware","text":"

Make a change to the welcome function code and check that it automatically reloads.

Change the default response text in the welcome function

Open the webapp in the browser http://localhost:8000.

Make a change to the code in the welcome function, altering the text of the :body in the default request.

(defn welcome\n  \"A ring handler to process all requests for the web server.\n  If a request is for something other than `/` then an error message is returned\"\n  [request]\n  (if (= \"/\" (:uri request))\n    {:status  200\n     :headers {}\n     :body    \"<h1>Hello, Clojure World</h1>\n               <p>Welcome to your first Clojure app.</p>\n               <p>I now reload changes automatically</p> \"}\n    {:status  404\n     :headers {}\n     :body    \"<h1>This is not the page you are looking for</h1>\n               <p>Sorry, the page you requested was not found!</p>\"}))\n

Save the code change and refresh your browser page, you should now see the updated message.

"},{"location":"projects/leiningen/todo-app/task-handlers/","title":"Task handlers","text":"

Now we have functions that create the database and add / remove tasks, we can provide handlers to call these functions and therefore enable the user to add and remove tasks.

In the following pages we will create handlers and use hiccup for the html markup.

"},{"location":"projects/leiningen/todo-app/task-handlers/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"projects/leiningen/todo-app/task-handlers/add-a-task/","title":"Add a task","text":"

which namespace are these going in

(ns todo-list.handlers.tasks :requires models....)

"},{"location":"projects/leiningen/todo-app/task-handlers/add-a-task/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"projects/leiningen/todo-app/task-handlers/delete-a-task/","title":"Delete a task","text":""},{"location":"projects/leiningen/todo-app/task-handlers/delete-a-task/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"projects/leiningen/todo-app/task-handlers/show-task/","title":"Show tasks","text":""},{"location":"projects/leiningen/todo-app/task-handlers/show-task/#todowork-in-progress-sorry","title":"TODO::work in progress, sorry","text":""},{"location":"projects/leiningen/todo-app/unit-test-handler-function/","title":"Unit Test handler functions","text":"

Handler functions can be tested with unit tests as they are just pure functions. All handlers take a request hash-map and return a response hash-map. So its easy to give each handler a hash-map as an argument and test that we get the expected response hash-map in return.

There is no need to mock the framework until we do integration level testing, where we are testing the full lifecycle of request-response.

It is useful to have separate unit and integration tests to quickly narrow down the root cause of issues.

"},{"location":"projects/leiningen/todo-app/unit-test-handler-function/#unit-test-branch","title":"Unit test branch","text":"

The unit tests are placed under test/full_namespace_path/ and reside in files with the same names as the source code filenames, with -test postfixed to the end.

src/practicalli/simple_webapp/handlers.clj\ntest/practicalli/simple_webapp/handlers-test.clj\n
"},{"location":"projects/leiningen/todo-app/unit-test-handler-function/#writing-unit-tests","title":"Writing unit tests","text":"

clojure.test is used to write unit tests for handlers, as we are just treating them as functions.

"},{"location":"projects/leiningen/working-example/","title":"A working example","text":"

As we don't have time to build a full web app in this workshop, here is one that was built early.

"},{"location":"projects/leiningen/working-example/#note-checkout-the-shouter-code-from-gitub","title":"Note:: Checkout the Shouter code from Gitub","text":""},{"location":"projects/slack-app/","title":"Clojure powered Slack Application","text":"

Slack applications provide a way to extend the functionality of Slack and provide integration with other services, e.g. GitHub, Atlassian Jira & Confluence, etc.

"},{"location":"projects/slack-app/#development-process-overview","title":"Development process Overview","text":"
  • create a slack account and slack space (to test the application)
  • create a slack app
  • deploy the slack app in the slack test workspace created previously
  • Create a clojure project and interact with the Slack API
  • Setup ngrok domain for Slack app to access a locally running Clojure web service (to respond to interaction with the app - assuming the slack isnt just push driven)
  • alternatively, webservices connection can be configured between the locally running Clojure app and slack (desktop app required? - how does this work...)

  • deploy clojure application to public cloud (AWS, Render.com, etc)

"},{"location":"projects/slack-app/create-slack-app/","title":"Create Slack App","text":"

Create a Slack account and follow the prompts to create a new workspace. The workspace will be used to deploy the Slack App for testing purposes.

Use an existing Slack workspace if sufficient administration privelleges are available (and you wont affect other peoples use of Slack)

Slack Quickstart describes how to create a Slack app.

Create a new Slack app with the Slack UI

Select From Scratch

Enter App Name and select the Development Workspace to experiment and build the app.

Regardless of development workspace, the app can be distributed to any other workspaces.

The Slack Web UI displays the basic information for the newly created Slack app.

"},{"location":"projects/slack-app/create-slack-app/#configure-scopes","title":"Configure scopes","text":"

Add a scope to post messages to a channel

Sidebar > OAuth & Permissions > Scopes > Add an OAuth Scope

Add the following Bot Token Scopes

  • chat:write scope to the Bot Token to allow the app to post messages
  • channels:read scope too so your app can gain knowledge about public Slack channels

Reisntall the app if changing scopes and other features

"},{"location":"projects/slack-app/create-slack-app/#update-display-information","title":"Update Display information","text":"

This feels like it should have been done before installing the app

Provide a short & long description of the app and set background colour. Optionally add an app icon (between 512 and 2000 px in size).

  • Short description: A random function from the Clojure Standard Library
  • Long description: A random function is selected from the Clojure Standard Library and displayed along with the documentation (doc string) to explain what the function does. A bonus feature will be to provide examples of function use.
"},{"location":"projects/slack-app/create-slack-app/#install-app-in-workspace","title":"Install app in workspace","text":"

Install your app to your Slack workspace to test it and generate the tokens you need to interact with the Slack API. You will be asked to authorize this app after clicking an install option.

Sidebar > Settings > Basic Information > Install your app

"},{"location":"projects/slack-app/create-slack-app/#authorization-token","title":"Authorization token","text":"

The Authorization token for the workspace is in the app management web page for the specific app

Sidebar > OAuth & Permissions > OAuth Tokens for Your Workspace

Create an environment variable to hold the authorization token with the value of the Bot User OAuth Token

export SLACK_AUTHENTICATION_TOKEN=xxxx-123412341234-12345123451245-...   \n

Environment variables used with Clojure must be set before running the REPL, so the variables are available to the Java Virtual Machine process.

Add the environment variables to .bashrc for Bash or .zshenv for Zsh

Access tokens represent the permissions delegated to the app by the installing user.

Avoid checking access tokens into version control

"},{"location":"projects/slack-app/create-slack-app/#test-app-save-for-later","title":"Test app - save for later...","text":"

Add the app to a public channel and test its (as yet unconfigured slash command)

Confirm the Slack App should be added to the selected workspace

"},{"location":"projects/slack-app/create-slack-app/#local-development","title":"Local Development","text":"

Use Socket Mode to route the app interactions and events over a WebSockets connection instead sending payloads to Request URLs, the public HTTP endpoints.

Socket mode is intended for internal apps that are in development or need to be deployed behind a firewall. It is not intended for widely distributed apps.

Alternatively, use ngrock to redirect requests to the local app.

"},{"location":"projects/slack-app/create-slack-app/#install-app-into-workspace","title":"Install app into workspace","text":"

Install Slack app into a workspace (not the development workspace)

Sidebar > Install App > Install App To Workspace > Slack OAuth UI

"},{"location":"projects/slack-app/slack-api-methods/","title":"Slack API Methods","text":"

An access token allows the calling of the methods described by the scopes requested during a Slack App installation, e.g., the chat:write scope allows an app to post messages.

The app isn't a member of any channels when installed. Choose a channel suitable for testing purposes and /invite the Slack app to the channel.

You can find the corresponding id for the channel that your app just joined by looking through the results of the conversations.list method:

curl https://slack.com/api/conversations.list -H \"Authorization: Bearer xoxb-1234...\"\n

You'll receive a list of conversation objects.

Now, post a message to the same channel your app just joined with the chat.postMessage method:

curl -X POST -F channel=C1234 -F text=\"Reminder: we've got a softball game tonight!\" https://slack.com/api/chat.postMessage -H \"Authorization: Bearer xoxb-1234...\"\n

Voila! We're already well on our way to putting a full-fledged Slack app on the table.

Web API Guide

API Methods list

Interactive Workflows

"},{"location":"projects/slack-app/slack-scopes/","title":"Slack Scopes Overview","text":"

Scopes give the app permission to carry out actions, e.g. post messages, in the development workspace.

Open the development workspace, either in a web page or in the Slack desktop app.

Sidebar > OAuth & Permissions > Scopes > Add an OAuth Scope

  • chat:write scope to the Bot Token to allow the app to post messages
  • channels:read scope too so your app can gain knowledge about public Slack channels
  • commands scope to build a Slash command.
  • incoming-webhook scope to use Incoming Webhooks.
  • chat:write.public scope to gain the ability to post in all public channels, without joining. Otherwise, you'll need to use conversations.join, or have your app invited by a user into a channel, before you can post.
  • chat:write.customize scope to adjust the app's message authorship to make use of the username, icon_url, and icon_emoji parameters in chat.postMessage.

Add scopes to the Bot Token.

Only add scopes to the User Token when the app needs to act as a specific user (e.g. post message as user, set user status, etc.)

If the scopes applied to a Slack App are changed, the Slack App must be redeployed to the workspace (and Slack App Directory) for the changes to take effect.

"},{"location":"projects/status-monitor-deps/","title":"Status Monitor project with Clojure tools","text":"

A status monitor dashboard to show operational status of a range of services.

A server-side web application using - ring and compojure for webapp request management - bulma CSS library for styling - hiccup for writing html in Clojure syntax - SVG graphics for status graphics - clojure.spec for validating functions and svg definitions in Clojure

"},{"location":"projects/status-monitor-deps/#creating-a-project","title":"Creating a project","text":"deps-newManual

Create a project using the app template and called practicalli/status-monitor. The :project/create alias from Practicalli Clojure CLI Config uses the deps-new project to create Clojure projects

clojure -T:project/create :template app :name practicalli/status-monitor\n
Some minor tweaks are made to the project before starting the application development

  • describe the project and how it can be used in the README
  • delete the LICENSE file and use a Creative Commons in the README
  • format the deps.edn file for readability

Create a project in a directory called status-monitor, with a deps.end file in the root of that directory deps.edn

{:paths [\"src\"]\n :deps {org.clojure/clojure {:mvn/version \"1.11.3\"}}}\n
Create a src/practicalli/status_monitor.clj file src/practicalli/status_monitor.clj
(ns practicalli.status-monitor)\n
Create a test/practicalli/status_monitor_test.clj file src/practicalli/status_monitor.clj
(ns practicalli.status-monitor-test\n  (:require [clojure.test :refer [deftest is testing]]))\n

practicalli/status-monitor

The code for this project can be found at practicalli/status-monitor

"},{"location":"projects/status-monitor-deps/application-server/","title":"Application Server","text":""},{"location":"projects/status-monitor-deps/application-server/#add-an-embedded-web-application-server","title":"Add an embedded web application server","text":"

The status monitor service runs on top of an application server which handles the infrastructure of messaging over https and other Internet protocols.

There are several libraries to provide this, ring and http-kit being the most common.

"},{"location":"projects/status-monitor-deps/application-server/#add-dependencies-for-application-server-and-routing","title":"Add dependencies for application server and routing","text":"

Edit the deps.edn file for the project.

Add the http-kit library library for the application server

Add the compojure library for routing of requests

 :deps\n {org.clojure/clojure {:mvn/version \"1.10.1\"}\n  http-kit            {:mvn/version \"2.3.0\"}\n  compojure           {:mvn/version \"1.6.1\"}}\n
"},{"location":"projects/status-monitor-deps/application-server/#add-code-to-start-the-application-server","title":"Add code to start the application server","text":"

Edit the src/practicalli/status_monitor_service.clj file

Include the http-kit server namespace and the compojure core namespace as requires in the ns definition.

(ns practicalli.status-monitor-service\n  (:gen-class)\n  (:require [org.httpkit.server :as app-server]\n            [compojure.core :refer [defroutes GET]]))\n

Add an atom to hold a reference to the running application server. When the server is not running, the atom contains nil.

(defonce app-server-instance (atom nil))\n

Update the -main function to start the application server using http-kit run-server, optionally setting the port number the server should run on.

(defn -main\n  \"Start the application server and run the application\"\n  [port]\n  (println \"INFO: Starting server on port: \" port)\n\n  (reset! app-server-instance\n          (app-server/run-server #'status-monitor {:port (Integer/parseInt port)})))\n
"},{"location":"projects/status-monitor-deps/application-server/#define-a-default-route-and-handler","title":"Define a default route and handler","text":"

Using the compojure defroutes function, define the default route and response when the status-monitor app received a request on the main URL, (eg. http://localhost:8888/)

(defroutes status-monitor\n  (GET \"/\" [] {:status 200 :body \"Status Monitor Dashboard\"}))\n

The route returns a hash-map that is the form of a response map. http-kit server transforms all response maps into https responses that are sent back to the requesting web browser.

"},{"location":"projects/status-monitor-deps/application-server/#stop-and-restart-server-from-repl","title":"Stop and restart server from REPL","text":"

Add functions to stop and restart the server, so change to the application code can be loaded in without having to stop the Clojure REPL.

Use the value in the app-server-instance atom to determine if the app-server is already running. If so, then send the instance the :timeout key with a value of time to shut itself down.

(defn stop-app-server\n  \"Gracefully shutdown the server, waiting 100ms\"\n  []\n  (when-not (nil? @app-server-instance)\n    (@app-server-instance :timeout 100)\n    (reset! app-server-instance nil)\n    (println \"INFO: Application server stopped\")))\n

With a REPL running the project, the server is started calling (-main) and stopped by calling (stop-app-server). A restart function is simply calling the stop and start functions.

(defn restart-app-server\n  \"Convenience function to stop and start the application server\"\n  []\n  (stop-app-server)\n  (-main))\n

Component lifecycle service

This approach is the essence of component lifecycle services such as mount, component and integrant.

Use the mount library if you are starting with component lifecycle services or require a clean and simple approach. Try integrant to take a data centric approach to such a service.

"},{"location":"projects/status-monitor-deps/application-server/#repl-experiment-section","title":"REPL experiment section","text":"

To help use the code during development a comment body has been included with calls to start, stop and restart the application.

(comment\n\n  ;; start application\n  (-main)\n\n  ;; stop application\n  (stop-app-server)\n\n  ;; restart application\n  (restart-app-server)\n\n  )\n
"},{"location":"projects/status-monitor-deps/continuous-integration/","title":"Continuous Integration","text":"

To assist in the development of the application, CircleCI, a continuous integration service will be used.

Initially this will run all the unit tests for the application and report on the results. In another chapter, CircleCI will be used to package the application and deploy the application.

"},{"location":"projects/status-monitor-deps/continuous-integration/#alias-for-test-runner","title":"Alias for test runner","text":"

Edit deps.edn file and add an alias called :test/run that calls kaocha test runner on the code

  :test/run\n  {:extra-paths [\"test\"]\n   :extra-deps {lambdaisland/kaocha {:mvn/version \"1.71.1119\"}}\n   :main-opts   [\"-m\" \"kaocha.runner\"]\n   :exec-fn kaocha.runner/exec-fn\n   :exec-args {:randomize? false\n               :fail-fast? true}}\n

Check the test runner is working by running the clojure command with the :test/run alias in a terminal at the root of the Clojure project

clojure -X:test/run\n
"},{"location":"projects/status-monitor-deps/continuous-integration/#add-circleci-configuration","title":"Add CircleCI configuration","text":"

Edit .circleci/config.yml and add a configuration to build and test the Clojure application. The cimg/clojure:1.10.0 image contains OpenJDK 17 and the latest version of Clojure CLI, Leiningen and Babashka.

Run the clojure commands in the root of the project before adding the configuration, to ensure the commands work locally first.

version: 2.0\njobs:\n  build:\n    working_directory: ~/build\n    docker:\n      - image: cimg/clojure:1.10\n    environment:\n      JVM_OPTS: -Xmx3200m\n    steps:\n      - checkout\n      - restore_cache:\n          key: status-monitor-service-{{ checksum \"deps.edn\" }}\n      - cache-dependencies\n      - run: clojure -P\n      - save_cache:\n          paths:\n            - ~/.m2\n            - ~/.gitlibs\n          key: status-monitor-service-{{ checksum \"deps.edn\" }}\n      - Unit-testing\n      - run: clojure -X:test/run\n
"},{"location":"projects/status-monitor-deps/continuous-integration/#add-project-on-circleci","title":"Add project on CircleCI","text":"

Visit the CircleCI dashboard and select Add Projects. Find the status-monitor-service repository and select Set Up Project button.

Choose the Add Manual install and Start Building

"},{"location":"projects/status-monitor-deps/debugging-requests/","title":"Debug ring requests","text":"

Requests are Clojure hash-maps so are easy to extract data from in a meaningful way.

If getting unexpected results, checking the details received in the request is a fast way to diagnose issues by seeing the data. The ring/ring-devel library contains a handle-dump function which displays the request parameters in a web page.

"},{"location":"projects/status-monitor-deps/debugging-requests/#add-ring-development-library","title":"Add ring development library","text":"

Add the :env/dev alias to include the ring/ring-devel library as a dependency. The ring-devel library includes functions for developing and debugging ring applications.

 :deps\n {org.clojure/clojure {:mvn/version \"1.10.3\"}\n  http-kit/http-kit   {:mvn/version \"2.5.3\"}\n  ring/ring-core      {:mvn/version \"1.9.5\"}\n  compojure/compojure {:mvn/version \"1.6.2\"}}\n\n :aliases\n {:env/dev\n  {:extra-deps {ring/ring-devel {:mvn/version \"1.8.1\"}}}}\n
"},{"location":"projects/status-monitor-deps/debugging-requests/#restart-repl","title":"Restart REPL","text":"

Dependencies are only added to the classpath when the REPL process starts, unless using the unofficial dependency hotload approach

Quit the REPL if it is already running

Start the REPL including the alias :env/dev. For example, run a rich terminal UI using rebel readline which also starts an nREPL sever:

clojure -M:env/dev:repl/rebel\n

:repl/rebel is defined in the user level configuration practicalli/clojure-deps-edn

Hotload libraries into a running REPL

Clojure CLI Hotload Libraries can add libraries to the class path without having to restart the REPL

"},{"location":"projects/status-monitor-deps/debugging-requests/#require-the-ringhandlerdump-namespace","title":"Require the ring.handler.dump namespace","text":"

Require the ring.handler.dump namespace in the ns form of practicalli.status-monitor-server namespace and refer the specific handle-dump function.

(ns practicalli.status-monitor-service\n  (:gen-class)\n  (:require\n   [org.httpkit.server       :as    app-server]\n   [compojure.core           :refer [defroutes GET]]\n   [compojure.route          :refer [not-found]]\n   [ring.handler.dump        :refer [handle-dump]]\n   [ring.util.response       :refer [response]]\n   [practicalli.helpers-http :refer [http-status-code]]))\n
"},{"location":"projects/status-monitor-deps/debugging-requests/#add-a-route-to-show-the-request-map","title":"Add a route to show the request map","text":"

Add a route that shows the request information using the handle-dump function.

(defroutes status-monitor\n  (GET \"/\" [] {:status (:OK http-status-code) :body \"Status Monitor Dashboard\"})\n  (GET \"/request-dump\" [] handle-dump))\n

(re)start the application server and visit the URL http://localhost:8080/request-dump

The request map details are also printed to the REPL buffer

{:remote-addr \"127.0.0.1\",\n :params {},\n :route-params {},\n :headers\n {\"accept\"\n  \"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\",\n  \"accept-encoding\" \"gzip, deflate\",\n  \"accept-language\" \"en-US,en;q=0.5\",\n  \"connection\" \"keep-alive\",\n  \"cookie\"\n  \"_ga=GA1.1.1141619352.1582159249; ring-session=0b3dc210-278e-4011-bc03-d8c2292b2c17; JSESSIONID=5RNxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxB; _gid=GA1.1.111111111.3333333333\",\n  \"host\" \"localhost:8080\",\n  \"upgrade-insecure-requests\" \"1\",\n  \"user-agent\"\n  \"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0\"},\n :async-channel\n #object[org.httpkit.server.AsyncChannel 0x359a37c8 \"/127.0.0.1:8080<->/127.0.0.1:38190\"],\n :server-port 8080,\n :content-length 0,\n :compojure/route [:get \"/request-dump\"],\n :websocket? false,\n :content-type nil,\n :character-encoding \"utf8\",\n :uri \"/request-dump\",\n :server-name \"localhost\",\n :query-string nil,\n :body nil,\n :scheme :http,\n :request-method :get}\n
"},{"location":"projects/status-monitor-deps/deployment-via-ci/","title":"Deployment Via Continuous Integration","text":"

Building on the CircleCI build pipeline created so far, the application will be deployed on Heroku if all the tests pass.

A workflow is added to the CircleCI configuration that deploys the application on Heroku from the source code. Heroku packages the application into an uberjar and then runs the application from that uberjar.

When commits in the Clojure project code are pushed to GitHub they are detected by CircleCI and the tests run. If the tests pass then the Heroku deployment stage starts.

TODO: Convert to tools.build approach

The depstar project has been retired (although still works) in favour of the official tools.build approach

"},{"location":"projects/status-monitor-deps/deployment-via-ci/#add-depstar-to-build-an-uberjar","title":"Add depstar to build an uberjar","text":"

Use the depstar tool to create a Java archive (jar) package of the application. The deps.edn configuration in the root of the project already contains an uberjar alias for this tool.

:project/uberjar\n{:replace-deps {com.github.seancorfield/depstar {:mvn/version \"2.1.303\"}}\n :exec-fn      hf.depstar/uberjar\n :exec-args    {:jar \"status-monitor-service.jar\"\n                :aot true}}\n

To try this on the command line:

clojure -X:project/uberjar\n

This will be the same command used in the build script

"},{"location":"projects/status-monitor-deps/deployment-via-ci/#create-a-custom-build-behaviour","title":"Create a custom build behaviour","text":"

Heroku build scripts use Leiningen by default. Configure Heroku to build with Clojure Tools, create a custom build file which will run instead of Leiningen.

Create a file called bin/build script in the root of the project

#!/usr/bin/env bash\nclojure -X:project/uberjar\n

Create an empty project.clj file so that Heroku recognized the project as Clojure.

"},{"location":"projects/status-monitor-deps/deployment-via-ci/#define-how-to-run-the-application","title":"Define how to run the application","text":"

Create a Procfile file in the root of the project directory containing the command to run the application.

Use the $PORT as an argument to the command. Heroku automatically assigns a port number for an application to listen upon when creating a contain in which the application will run. This port number is set using the PORT environment variable and is available to the application on startup. Using the PORT environment variable ensures the Clojure application will receive requests.

web: java -jar status-monitor-service.jar $PORT\n
"},{"location":"projects/status-monitor-deps/deployment-via-ci/#specifying-a-java-version","title":"Specifying a Java version","text":"

Create a system.properties and specify the Java version to use for the application. Java 1.8 is the default version use on Heroku, however, our development environment is Java 17, so add a property to set the Java runtime to version 17.

java.runtime.version=17\n
"},{"location":"projects/status-monitor-deps/deployment-via-ci/#heroku-configuration","title":"Heroku configuration","text":"

Login to the Heroku dashboard and create a new application.

In the Heroku dashboard, open the application Settings and add a Config Vars using the name CLOJURE_CLI_VERSION with a value of 1.10.1.727

"},{"location":"projects/status-monitor-deps/deployment-via-ci/#circleci-configuration-with-heroku-orb","title":"CircleCI configuration with Heroku Orb","text":"

Edit the .circleci/config.yml file and add the heroku orb and a workflow to call the orb task. The workflow has a dependency on the build job, so that will take place first.

The Heroku workflow will build the application from source code using the heroku/deploy-via-git. Only changes pushed to the live branch of the GitHub repository will be used in the Heroku deploy workflow.

Feature branches can be deployed on Heroku by creating an additional Heroku application and push the branch to it. Or use Heroku pipelines

version: 2.1\n\norbs:\n  heroku: circleci/heroku@1.2.6 # Invoke the Heroku orb\n\nworkflows:\n  heroku_deploy:\n    jobs:\n      - build\n      - heroku/deploy-via-git: # Use the pre-configured job, deploy-via-git\n          requires:\n            - build\n          filters:\n            branches:\n              only: live\n\njobs:\n  build:\n    working_directory: ~/build\n    docker:\n      - image: cimg/clojure:1.10\n    environment:\n      JVM_OPTS: -Xmx3200m\n    steps:\n      - checkout\n      - restore_cache:\n          key: status-monitor-service-{{ checksum \"deps.edn\" }}\n      - run: clojure -P\n      - save_cache:\n          paths:\n            - ~/.m2\n            - ~/.gitlibs\n          key: status-monitor-service-{{ checksum \"deps.edn\" }}\n      - run: clojure -X:test/run\n
"},{"location":"projects/status-monitor-deps/deployment-via-ci/#circleci-environment-variables","title":"CircleCI Environment Variables","text":"

Open the CircleCI and select project settings > Environment Variables

Add environment variables to define where the Heroku application can be found and a token to provide access.

Environment Variable Value HEROKU_API_KEY name of the application created on Heroku HEROKU_APP_NAME API key found in Account Settings > API Key"},{"location":"projects/status-monitor-deps/deployment-via-ci/#push-changes-to-trigger-build","title":"Push changes to trigger build","text":"

Commit the changed and push them to the GitHub repository. This triggers a build by CircleCI. The build downloads the dependencies and runs the unit tests. If the tests pass, then the Heroku deploy workflow starts.

The two stages can be seen in the dashboard as the pipeline runs.

Now visit the deployed Heroku application to see it in action.

"},{"location":"projects/status-monitor-deps/deployment-via-ci/#troubleshooting","title":"Troubleshooting","text":"

If there are issues, then use the Heroku toolbelt to look at the logs. In a command line terminal, issue the login command which opens a web browser to login to Heroku. Once logged in, run the heroku logs command to view the latest logs

heroku login\n\nheroku logs --app status-monitor-service\n

The logs can also be viewed live, as the application is being deployed by including the --tail option when running the heroku logs command in a terminal

heroku logs --app status-monitor-service --tail\n

The example Heroku logs show that the status-monitor-service is using the default port number if non is supplied as an argument, rather than Heroku assigned port. Heroku therefore considers the application as unresponsive and sets it status to crashed, tearing down the container the application is running in.

These logs were generated before adding the $PORT to the command in the Procfile.

"},{"location":"projects/status-monitor-deps/deployment-via-ci/#no-forced-pushes","title":"No forced pushes","text":"

Heroku doesn't like force Git pushes coming via CircleCI.

To get around this, either don't do force pushes to GitHub, or add the Heroku repository for the project as a remote to local git repository.

Heroku repository details in heroku dashboard Settings under App Information

Changes can now be pushed, ideally using force-with-lease to Heroku repository.

git push heroku live:master\n

Heroku only builds from a branch called master or main, so the above command pushes the local live branch to the remote master branch on Heroku.

"},{"location":"projects/status-monitor-deps/deployment-via-ci/#stopping-the-application","title":"Stopping the application","text":"

An application can be run for free on Heroku with the monthly free credits provided. However, to make the most out of these free credits then applications not in use should be shut down

Run the following command in the root of the Clojure project.

heroku ps:stop status-monitor-service\n
"},{"location":"projects/status-monitor-deps/refactor-handlers-and-tests/","title":"Refactor handlers and unit tests","text":"

Refactor the unit tests to use the ring-mock library to test handler functions. Create separate handler functions for the routes in defroute (there is only one custom handler at present).

(deftest dashboard-test\n  (testing \"Testing elements on the dashboard\"\n    (is (= (SUT/dashboard (mock/request :get \"/\"))\n           {:status  200\n            :body    \"Status Monitor Dashboard\"\n            :headers {}}))))\n

Create a handler for the GET \"/\" route

(defn dashboard\n  [request]\n  {:status (:OK http-status-code) :body \"Status Monitor Dashboard\" :headers {}})\n

Use the ring.util.response/response function to create a well formed response map which has a status code of 200. This removes the need to type in the response map structure explicitly which potentially can introduce bugs.

In the current namespace ns form, this function is required as an explicit refer, [ring.util.response :refer [response]], so its available to use by its unqualified name, response.

(defn dashboard\n  [request]\n  (response \"Status Monitor Dashboard\"))\n

Update the defroutes definition to call this handler rather than hard coding the response map.

(defroutes status-monitor\n  (GET \"/\" [] dashboard)\n  (GET \"/request-dump\" [] handle-dump)\n  )\n

Run the Cognitect test runner to check the unit tests are still passing after the code refactor.

clojure -M:env/dev:test:runner\n
"},{"location":"projects/status-monitor-deps/refactor-handlers-and-tests/#helper-functions","title":"Helper functions","text":"

The ring util library contains several other helper functions, bad-request, not-found and redirect:

(ring.util.response/bad-request \"Hello\")\n;;=> {:status 400, :headers {}, :body \"Hello\"}\n\n(ring.util.response/created \"/post/clojure-is-awesome\")\n;;=> {:status 201, :headers {\"Location\" \"/post/clojure-is-awesome\"}, :body nil}\n\n(ring.util.response/redirect \"https://clojure.org/getting-started/\")\n;;=> {:status 302, :headers {\"Location\" \"https://clojure.org/getting-started/\"}, :body \"\"}\n

The status function converts an existing response to use a given status code (which can be anything). Use this with care and document what the status code means otherwise confusion will abound.

(ring.util.response/status (ring.util.response/response \"Time for Cake!\") 555)\n;;=> {:status 555, :headers {}, :body \"Time for Cake!\"}\n

Ring utilities has functions for setting the header data for responses, content-type, header or set-cookie.

(ring.util.response/content-type (ring.util.response/response \"Hello\") \"text/plain\")\n;;=>  {:status 200, :headers {\"Content-Type\" \"text/plain\"}, :body \"Hello\"}\n\n(ring.util.response/header (ring.util.response/response \"Hello\") \"X-Tutorial-For\" \"Practicalli\")\n;;=>  {:status 200, :headers {\"X-Tutorial-For\" \"Practicalli\"}, :body \"Hello\"}\n\n(ring.util.response/set-cookie (ring.util.response/response \"Hello\") \"User\" \"123\")\n;;=>  {:status 200, :headers {}, :body \"Hello\", :cookies {\"User\" {:value \"123\"}}}\n

wrap-cookies middleware required

The set-cookie function adds a new entry to the response map and requires the wrap-cookies middleware to process correctly.

"},{"location":"projects/status-monitor-deps/refactor-handlers-and-tests/#handler-functions","title":"Handler functions","text":"

A handler to return the incoming IP Address

(defn check-ip-handler [request]\n    (ring.util.response/content-type\n        (ring.util.response/response (:remote-addr request))\n        \"text/plain\"))\n
"},{"location":"projects/status-monitor-deps/unit-test-mocking-handlers/","title":"Mocking in Unit Tests","text":"

The main focus of unit tests in a web application are the handler functions, passing requests to those functions and checking the responses.

All handler functions are passed a request object by default when using compojure defroutes function for routing.

If handler functions do not use arguments then you can test those handlers by simply passing an empty hash-map, {}.

For all other handler functions you can pass a request object or just specific parts of a request in a hash-map.

"},{"location":"projects/status-monitor-deps/unit-test-mocking-handlers/#ring-mock-library","title":"Ring mock library","text":"

ring-mock is a small library creating Ring request maps (Clojure hash-maps) to support unit testing. Generated hash-maps are examples of a ring request and used as arguments when calling the handler functions in tests.

"},{"location":"projects/status-monitor-deps/unit-test-mocking-handlers/#add-dev-dependency","title":"Add dev dependency","text":"

As ring-mock is a development only library, it should be added to an alias not included in the packaging of the project for production.

Edit the project deps.edn file in the project and add ring-mock to an alias called :env/dev, creating the alias if required.

  :env/dev\n  {:extra-deps {ring/ring-mock {:mvn/version \"0.4.0\"}}}\n
"},{"location":"projects/status-monitor-deps/unit-test-mocking-handlers/#add-namespace","title":"Add namespace","text":"

Add the ring-mock.request namespace to any of the test namespaces mocking of requests will be useful.

Edit the test/practicalli/status-monitor-service.clj and add ring-mock as a required namespace in files ns form.

(ns practicalli.status-monitor-service-test\n  (:require [clojure.test :refer [deftest is testing]]\n            [ring.mock.request :as  mock]\n            [practicalli.status-monitor-service :as status-monitor]))\n

Add unit tests to check the handlers (which are going to be added next - TDD style)

(deftest test-app\n  (testing \"main route\"\n    (let [response ((status-monitor/app) (request :get \"/\"))]\n      (is (= 200 (:status response)))))\n\n  (testing \"not-found route\"\n    (let [response ((status-monitor/app) (request :get \"/invalid\"))]\n      (is (= 404 (:status response))))))\n
"},{"location":"projects/status-monitor-deps/unit-test-mocking-handlers/#examples","title":"Examples","text":"
  • API: ring-mock
(deftest your-handler-test\n  (is (= (your-handler (mock/request :get \"/doc/10\"))\n         {:status  200\n          :headers {\"content-type\" \"text/plain\"}\n          :body    \"Your expected result\"})))\n\n(deftest your-json-handler-test\n  (is (= (your-handler (-> (mock/request :post \"/api/endpoint\")\n                           (mock/json-body {:foo \"bar\"})))\n         {:status  201\n          :headers {\"content-type\" \"application/json\"}\n          :body    {:key \"your expected result\"}})))\n
"},{"location":"reference/","title":"reference","text":""},{"location":"reference/continuous-integration/heroku/","title":"CI: Heroku","text":"

Heroku is a now only a commercial service without a developer environment.

Practicalli is looking into other services as that are more developer friendly.

"},{"location":"reference/continuous-integration/heroku/#heroku-pipelines","title":"Heroku pipelines","text":"

Using Heroku Pipelines the staging environment is promoted to production rather than being rebuilt

The Heroku dashboard can be used to promote the application into production, once the staging application is signed off.

"},{"location":"reference/continuous-integration/heroku/#heroku-build-process","title":"Heroku Build process","text":"

The build process starts when commits are pushed to Heroku, either directly or via a continuous integration service (eg. CircleCI).

"},{"location":"reference/ring/","title":"Ring specification","text":"

Information to complement the ring projects

Ring provides a defacto web standard that the majority of server-side web appliications use

  • request (Clojure hash-map)
  • response (Clojure hash-map)
  • handler (Clojure function)
  • middleware (Clojure function)
  • adaptor (Clojure function / wrapper)

Routing requests to handlers is typially managed by functions or libraries used in conjunction with ring, e.g reitit or compojure

"},{"location":"reference/ring/#ring-request-map","title":"Ring request map","text":"

Ring represents HTTP requests as simple Clojure maps, whose keys are drawn from the Java Servlet API and the official documentation RFC2616 \u2013 Hypertext Transfer Protocol - HTTP/ 1.1 ( http:// www.w3. org/ Protocols/ rfc2616/ rfc2616. html ).

A request map contains the following keys:

  • :server-port the port the HTTP server was listening for the request
  • :server-name the resolved name or IP address of the server handling the request
  • :remote-addr IP address of the client that made the request
  • :uri the path to the requested resource (the part of the URL address after the domain name)
  • :query-string the HTTP query string if included in the request. e.g. http://practical.li/blog?topic=clojure has a request map that includes :query-string \"topic=clojure\".
  • :scheme protocol used to make the request as a keyword, i.e. :http for HTTP request and :https for Secure HTTP
  • :request-method HTTP method used to make the request as keyword, one of :get, :post, :put, :delete, :head or :options
  • :headers hash-map of header names and values, e.g: {:headers {\"content-type\" \"text/html\" \"content-length\" \"500\" \"pragma\" \"no-cache\"}}
  • :body a string of the request body (e.g. contents of an HTTP POST request)

Request maps are not restricted to these top level keys. Middleware is commonly used to mutate the request map by adding keys.

See the reference page for a Clojure request map

"},{"location":"reference/ring/#response-maps","title":"Response maps","text":"

Ring represents an HTTP response as a simple Clojure map.

The response map contains only three keys:

  • :status HTTP status code of the response as an integer, such as 200 or 403. A full list of HTTP status codes is made available as part of the RFC2616, and can be viewed at http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html.
  • :headers contains a map of header names (string) to header values, similar to the request map.
  • :body the body of the response as one of the following four types, and the behavior will change for each:
  • String - the body is sent directly to client
  • ISeq - each element of the sequence is sent to client as a String
  • File - contents of the file sent to client
  • InputStream - contents of the stream sent to the client, after which the stream is closed

An example of a simple Hello World! response map can look like this:

{:status 200 \n :headers {\" Content-Type\" \"text/ html\"} \n :body \"<html><body><h1>Hello World!</h1></body></html>\"}\n
"},{"location":"reference/ring/#handlers","title":"Handlers","text":"

A handler is a Clojure function that accepts a request map and returns a response map.

an example handler function:

(defn hello-world \n  \"Returns an Hello World Response Map\" \n  [request] \n  {:status 200 \n   :headers {\"Content-Type\" \"text/html\"} \n   :body \"<html><body><h1>Hello World</h1></body></html>\"}) \n

Handlers are the core of the application.

Typically our URLs will map one-to-one with a handler.

create a handler and configure a route to use that handler too generate a response.

Open the src/practicalli/routes/home.clj file.

Above the call to defroutes, add the following handler:

(defn hello-world \n  [] \n  {:status 200 \n   :headers {\"Content-Type\" \"text/html\"} \n   :body \"<html><body><h1>Hello World</h1></body></html>\"})\n

Define a get request for an /about URI route that calls the hello-world handler:

(GET \"/about\" [] (foo-response)) \n

Navigate to http://localhost:3000/about in a browser and see a simple Hello World! page.

"},{"location":"reference/ring/#middleware","title":"Middleware","text":"

Middleware are functions that sit between the adapter and the handler and can be assigned to one or more routes.

A middleware function accepts a handler function and returns a new handler function.

Middleware functions can update the request or response map (adding keys, changingg values, coercing types or logging request and response maps) before passing it on to the handler function.

An example: a middleware function which adds a :friday? key to the request map which is used in the hello-world handler :

Edit the src/practicalli/middleware.clj file.

Add the following middleware function, which takes a handler and returns a new handler function (which in turn adds a new key to the request map and calls the next handler in the chain):

(defn friday? \n  [handler] \n  (fn [request] \n  (let [request (assoc request :friday? true)] \n    (handler request)))) \n

In the middleware definition add the friday? middleware function call:

(def middleware [go-bowling? wrap-error-page wrap-exceptions])\n

In the src/practicalli/routes/home.clj adjust the :body value in our hello-world handler to include a message based on the :friday? key value on the request map.

Adjust the handler function parameters to accept the request map:

(defn hello-world \n  [request] \n  {:status 200 \n   :headers {\" Content-Type\" \"text/html\"} \n   :body (str \"<html><body> <dt>Is it Friday?</dt>\"\"<dd >\"(:friday? request)\"</dd></body></html>\")}) \n

Change the /about route to make use of the request map:

(GET \"/ about\" request (hello-world request))\n

Refresh the browser page at http://localhost:3000/about and see the middleware in action!

Ring Wiki: Middleware concepts has further details on middleware and it's use in Ring

"},{"location":"reference/ring/#adapters","title":"Adapters","text":"

Adapters translate between the HTTP protocol and Clojure data, greatly simplifying all Clojure web applications.

An adapter converts an incoming HTTP request into a Clojure request hash-map and passes the request to the Clojure web application. The adaptor converts the Clojure response hash-map into the appropriate servlet HTTP response, sending that HTTP respose back to the client.

The Ring library comes with a Jetty adapter ([ring/ring-jetty-adapter \"1.3.0\"]) which sits between a Jetty servlet container and the rest of the application stack.

Http-kit also provides a ring compatible adaptor for its HTTP server.

"},{"location":"reference/ring/request-map/","title":"Ring - request map","text":"
  • What is a request
Key Always Present? Type Description :async-supported? Y boolean True if this request supports asynchronous operation :body Y ServletInputStream The body of the request. May be a zero-length stream. :content-type N String Present if sent by client. Content type of the request body. :content-length N Long Present if sent by client. Content length of the request body. :character-encoding N String Present if sent by client. Character encoding applicable to request body. :edn-params N Any :form-params N Map of keyword -> String :headers Y Map of String -> String Request headers sent by the client. Header names are all converted to lower case. :json-params N Map of String -> String :params N Map of keyword or String -> String Query params. See :query-params. :path-info Y String Request path, below the context path. Always at least \"/\", never an empty string. :path-params N Map of keyword -> String Present if the router found any path parameters. :protocol Y String Name and version of the protocol with which the request was sent :query-params N Map of keyword -> String :query-string Y String The part of the request's URL after the '?' character. :remote-addr Y String IP Address of the client (or the last proxy to forward the request) :request-method Y Keyword The HTTP verb used to make this request, lowercase and in keyword form. For example, :get or :post. :put and :delete request methods via a query parameter :_method :server-name Y String Host name of the server to which the request was sent :server-port Y int Port number to which the request was sent :scheme Y String The name of the scheme used to make this request, for example, http, https, or ftp. :ssl-client-cert N java.security.cert.X509Certificate[] Present if sent by client. Array of certificates that identify the client. :transit-params N Any data structure :uri Y String The part of this request's URL from the protocol name up to the query string in the first line of the HTTP request"},{"location":"relational-databases-and-sql/","title":"SQL and Relational Databases","text":"

seancorfield/next.jdbc is the defacto Clojure wrapper for SQL queries and managing connections to relational databases.

next.jdbc supports a wide range of databases and automatically pulls in the relevant database drivers. Abstractions are provided for insert, query, update and delete actions, which define data in a hash map allow the use of Clojure specifications (clojure.spec or Malli) for validation and generative testing.

"},{"location":"relational-databases-and-sql/#relational-databases","title":"Relational Databases","text":"

This guide will use the following relational databases

  • H2 database - lightweight in-process database that writes to disk, easily added for a fast and simple dev environment.
  • Postgresql - open source, feature rich and production grade database (defacto production choice)

Other persistent storage approach include

  • Amazon RDS - a postgres-like storage as an AWS service (should work just like postgres)
  • CockroachDB an elastic, indestructible SQL database for developers building modern applications
  • yugabyteDB open source, cloud native relational DB for powering global, internet-scale apps.
"},{"location":"relational-databases-and-sql/#key-value-stores","title":"Key Value stores","text":"
  • Redis
  • RocksDB is a high performance embedded persistent key-value store with fast storage writes (fork of Google's LevelDB)
  • AWS Dynamo - 400k limit per stored value
"},{"location":"relational-databases-and-sql/#clojure-databases","title":"Clojure databases","text":"
  • Crux - open database with temporal graph query
  • Datomic - transactional database with a flexible data model, elastic scaling, and rich queries Interesting databases in the Clojure spaces include Datomic and Crux.
"},{"location":"relational-databases-and-sql/#database-drivers","title":"Database drivers","text":"

Database drivers for commonly used database

  • Apache Derby
  • H2
  • HSQLDB
  • Microsoft SQL Server jTDS
  • Microsoft SQL Server -- Official MS Version
  • MySQL
  • PostgreSQL
  • SQLite

Database drivers may require a minimum version of Java, so consider Java 8 as the minimum version and Java 11 as the recommended version (until a new long term support version of Java is release).

see database support at http://clojure-doc.org/articles/ecosystem/java_jdbc/home.html (is this up to date?)

Feedback welcome

"},{"location":"relational-databases-and-sql/h2-database/","title":"H2 Lightweight Relational Database","text":""},{"location":"relational-databases-and-sql/managing-connections/","title":"Managing database connections","text":"

A Clojure application / REPL can connect to a database source and request a new connection. This connection can be used to send SQL statements to the database and receive the results.

In a single threaded application, then only one connection to each database would be created. In a multi-threaded application then multiple connections to the database may be created. If multiple instances of an application are deployed, then multiple database connections will be created.

As the number of simultaneous connections to the database grows, the more need for a connection pool service to maximize the performance of the database.

"},{"location":"relational-databases-and-sql/managing-connections/#postgresql","title":"PostgreSQL","text":"

When a client connects to PostgreSQL database the parent process spawns a worker process which listens to the newly created connection. Spawning a work process each time can support a small number of database connections. As the number of simultaneous connections increases, CPU and memory resources also increase.

Without a connection pool a new database connection is created for each client.

"},{"location":"relational-databases-and-sql/managing-connections/#hintlimits-via-postgresql-configuration","title":"Hint::Limits via PostgreSQL configuration","text":"

The max_connections configuration for PostgreSQL limits the number of client connections allowed, so additional connections are refused or dropped.

"},{"location":"relational-databases-and-sql/managing-connections/#connection-pool-with-postgresql","title":"Connection pool with PostgreSQL","text":"

A connection pool shares a fixed set of recyclable connections which can manage a large numbers of simultaneous connections due to the reduced CPU and memory usage.

The fixed set of connections is called the pool size and it is recommended to test the size of the pool used during integration tests.

A connection pool can efficiently deal with idle or stagnant client connections, as well as queue up client requests during traffic spikes instead of rejecting them.

"},{"location":"relational-databases-and-sql/managing-connections/#connection-pool-implementations","title":"Connection pool implementations","text":"

A connection pool can be implemented either on the application side or as middleware between the database and your application.

  • pgBouncer - a lightweight, open-source middleware connection pool for PostgreSQL.
  • HikariCP - Fast, simple, reliable. HikariCP is a \"zero-overhead\" production ready JDBC connection pool. At roughly 130Kb, the library is very light.
  • c3p0 - an easy-to-use library for making traditional JDBC drivers \"enterprise-ready\" by augmenting them with functionality defined by the jdbc3 spec and the optional extensions to jdbc
  • Heroku pgBouncer build-pack, Heroku Postgres connection limit guidance and [Heroku Postgres plans] with connection limits.
  • Tuning your PostgreSQL Server - wiki.postgresql.org
  • PostgreSQL 12.4 Documentation
"},{"location":"relational-databases-and-sql/postgresql-database/","title":"Postgresql database","text":"

PostgreSQL is a powerful, open source object-relational database system with over 30 years of active development that has earned it a strong reputation for reliability, feature robustness, and performance.

PostgreSQL is a common choice for Clojure WebApps that require a persistent store for data, especially where that data is relational in nature. PostgreSQL is also a great choice for JSON and other formats.

"},{"location":"relational-databases-and-sql/postgresql-database/#heroku-postgres","title":"Heroku Postgres","text":"

Heroku provides a Posgresql service (free 10,000 rows limit per database) which provisions PostgreSQL databases on demand, so is a simple way to start development.

"},{"location":"relational-databases-and-sql/h2-database/","title":"H2 Relational Database","text":"

H2 is a database distributed as library, making it a ideal for a self-contained development environment for a Clojure application with a relational database. Data is persisted to mv.db files as SQL queries are executed in Clojure.

H2 database main features include * Very fast, open source, JDBC API * Embedded and server modes; in-memory databases * Browser based Console application * Small footprint: around 2 MB jar file size

Whilst H2 could be used for very small production web applications, it is recommended only as a development time database.

"},{"location":"relational-databases-and-sql/h2-database/#using-h2-in-the-repl","title":"Using h2 in the REPL","text":"

H2 works with next.jdbc, the defacto relational database library for Clojure.

"},{"location":"relational-databases-and-sql/h2-database/#including-h2-in-clojure-projects","title":"Including H2 in Clojure projects","text":"

next.jdbc is highly recommended library for SQL queries in Clojure

{% tabs deps=\"deps.edn projects\", lein=\"Leiningen projects\" %}

{% content \"deps\" %} To use H2 database as only a development database, add an :extra-deps entry to include the H2 library in a :dev alias in the project deps.edn file.

{:deps\n {org.clojure/clojure    {:mvn/version \"1.10.1\"}\n org.seancorfield/next.jdbc {:mvn/version \"1.1.569\"}}}\n\n{:aliases\n  {:dev\n   {:extra-deps {com.h2database/h2 {:mvn/version \"1.4.200\"}}}}}\n

Alternative, if using practicalli/clojure-deps-edn configuration, use the :database-h2 alias when starting the REPL to include the H2 library on the class path.

{% content \"lein\" %}

Edit the project.clj configuration file and add the H2 library to the :dev-dependencies section to run H2 as the development only database.

(defproject project-name \"1.0-SNAPSHOT\"\n  :description \"Database application using next.jdbc with H2 as development database\"\n  :url \"http://practicalli.github.io/clojure/\"\n  :dependencies [[org.clojure/clojure \"1.10.1\"]\n                 [seancorfield/next.jdbc \"1.1.582\"]]\n\n  :dev-dependencies [[com.h2database/h2 \"1.4.200\"]])\n

{% endtabs %}

"},{"location":"relational-databases-and-sql/h2-database/#auto-increment-values-in-h2-database","title":"Auto-increment values in H2 database","text":"

The IDENTITY type is used for automatically generating an incrementing 64-bit long integer in H2 database.

CREATE TABLE public.account (\n  id IDENTITY NOT NULL PRIMARY KEY ,\n  name VARCHAR NOT NULL,\n  number VARCHAR NOT NULL,\n  sortcode VARCHAR NOT NULL,\n  created TIMESTAMP WITH TIME ZONE NOT NULL);\n

No need to pass a value for our primary key column value as it is being automatically generated by H2.

INSERT INTO public.account ( id, name, number, sortcode, created)\nVALUES ( ? , ? , ? , ? );\n
"},{"location":"relational-databases-and-sql/h2-database/#resources","title":"Resources","text":"
  • next.jdbc documentation and next.jdbc db-types list
  • H2 Database website
  • SQL Constraints - W3Schools.com
  • Purpose of constraint naming - Stack Overflow
  • seancorfield/honeysql - SQL as data structures
  • stack overflow - auto increment id in h2 database
"},{"location":"relational-databases-and-sql/h2-database/database-tools/","title":"Database tools","text":"

DBeaver is a free database tool that supports the H2 database and many other databases.

"},{"location":"relational-databases-and-sql/h2-database/database-tools/#create-a-new-connection","title":"Create a new connection","text":"

Create a New Connection and select Embedded > H2 database

Select a *.mv.db file as the path

If the H2 driver is not installed in DBeaver, a will prompt will display to download it.

Expand the connection to see the schema details

"},{"location":"relational-databases-and-sql/h2-database/database-tools/#h2-database-single-connection","title":"H2 database single connection","text":"

When connecting to the H2 database using a database management tool such ad DBeaver, the database is locked and prevents code from running in the REPL.

Close the connection in the database management tool to continue using the REPL.

"},{"location":"relational-databases-and-sql/h2-database/schema-design/","title":"H2 Schema design","text":"

Key concepts and syntax for designing database schema for the H2 database

"},{"location":"relational-databases-and-sql/h2-database/schema-design/#auto-increment-values-in-h2-database","title":"Auto-increment values in H2 database","text":"

The IDENTITY type is used for automatically generating an incrementing 64-bit long integer in H2 database.

CREATE TABLE public.account (\n  id IDENTITY NOT NULL PRIMARY KEY ,\n  name VARCHAR NOT NULL,\n  number VARCHAR NOT NULL,\n  sortcode VARCHAR NOT NULL,\n  created TIMESTAMP WITH TIME ZONE NOT NULL);\n

The value for id is automatically generated by H2, so no need to provide a value for id in the SQL statement

INSERT INTO public.account ( id, name, number, sortcode, created)\nVALUES ( ? , ? , ? , ? );\n
"},{"location":"relational-databases-and-sql/h2-database/schema-design/#resources","title":"Resources","text":"
  • next.jdbc documentation and next.jdbc db-types list
  • H2 Database website
  • SQL Constraints - W3Schools.com
  • Purpose of constraint naming - Stack Overflow
  • seancorfield/honeysql - SQL as data structures
  • stack overflow - auto increment id in h2 database
"},{"location":"relational-databases-and-sql/next-jdbc-library/","title":"SQL queries in Clojure with next.jdbc library","text":"

Using next.jdbc to connect to a database and run queries only a few steps

  • add seancorfield/next.jdbc as a project dependency
  • require the seancorfield/next.jdbc in the relevant project namespace definitions
  • define a database specification (hash-map of database details or JDBC string)
  • create a connection (optionally using a connection pool)
  • execute SQL statements (individual, batch, transaction)

"},{"location":"relational-databases-and-sql/next-jdbc-library/#hintnextjdbc-supersedes-clojurejavajdbc","title":"Hint::next.jdbc supersedes clojure.java.jdbc","text":"

seancorfield/next.jdbc supersedes clojure.java.jdbc which used to be the defacto library for database backed projects. next.jdbc is faster and exposes a more modern API design (according to the author of clojure.java.jdbc). Migration from clojure.java.jdbc is documented on the next.jdbc repository

"},{"location":"relational-databases-and-sql/next-jdbc-library/#live-coding-example","title":"Live Coding example","text":""},{"location":"relational-databases-and-sql/next-jdbc-library/#summary-of-using-nextjdbc","title":"Summary of using next.jdbc","text":"

Include next.jdbc as a dependency in the project

{:deps\n {org.clojure/clojure        {:mvn/version \"1.10.1\"}\n  org.seancorfield/next.jdbc {:mvn/version \"1.1.569\"}}}\n

Require next.jdbc into the project namespace

(ns practicalli.database-access\n  (:require [next.jdbc :as jdbc]))\n
"},{"location":"relational-databases-and-sql/next-jdbc-library/#specify-the-database-connection","title":"Specify the database connection","text":"

Define a data source connection using a next.jdbc hash map or a JDBC URL

An example next.jdbc specification for H2 database

{:dbtype \"h2\" :dbname \"banking-on-clojure\"}\n

An example JDBC connection string for postgres database

\"jdbc:postgresql://<hostname>:port/<database-name>?user=<username>&password=<password>&sslmode=require\"\n
"},{"location":"relational-databases-and-sql/next-jdbc-library/#running-sql-queries","title":"Running SQL queries","text":"

execute! runs an SQL statement and returns the results as a vector of hash maps. The hash maps use table and column name to create qualified keywords in the results.

A Clojure string contains the SQL statement.

(jdbc/execute!\n      connection\n      [(str \"insert into account_holders(\n               account_holder_id,first_name,last_name,email_address,residential_address,social_security_number)\n             values(\n               '\" account-holder-id \"', 'Jenny', 'Jetpack', 'jen@jetpack.org', '42 Meaning Lane, Altar IV', 'AB101112C' )\")])\n
"},{"location":"relational-databases-and-sql/next-jdbc-library/#hintdatafy-results","title":"Hint::Datafy results","text":"

Hash maps returned by execute! use Datafy and are therefore navigable using Clojure data browsers

"},{"location":"relational-databases-and-sql/next-jdbc-library/#using-connections-and-queries-effectively","title":"Using connections and queries effectively","text":"

Define a name for the database connection using the form (jdbc/get-datasource {:dbtype \"...\" :dbname \"...\" ...})

(def db-spec (jdbc/get-datasource {:dbtype \"h2\" :dbname \"banking-on-clojure\"}))\n

Use the with-open Clojure core function to automatically close connections after running SQL expressions

    (with-open [connection (jdbc/get-connection db-spec)]\n      (jdbc/execute! connection [...]))\n

Defining a generic function provides a simple way to run any SQL query for a specified data base connection.

(defn query-database\n  [db-spec sql-statement]\n  (with-open [connection (jdbc/get-connection db-spec)]\n    (jdbc/execute! connection sql-statement)))\n
"},{"location":"relational-databases-and-sql/next-jdbc-library/#using-nextjdbc-friendly-functions","title":"Using next.jdbc friendly functions","text":"

next.jdbc provides higher level abstractions over execute! function. These friendly functions take a database connection, a table name as a Clojure keyword and a hash map that contains the values in the query. As the query is a hash map it can also be represented by a Clojure specification (clojure.spec or Malli).

Function names ending with a bang, !, change the contents of the database

  • insert! - insert new rows
  • query - read data
  • update! - update existing rows
  • delete! - remove rows
"},{"location":"relational-databases-and-sql/next-jdbc-library/#generic-insert-function-with-nextjdbcsql","title":"Generic insert function with next.jdbc.sql","text":"

To save repetition, define a generic function that uses insert and takes a database table name, data to insert and the database connection.

(defn insert-data\n  [db-spec table record-data]\n  (with-open [connection (jdbc/get-connection db-spec)]\n    (jdbc-sql/insert! connection table record-data)))\n

Call the generic insert function with the database connection, table name and query specification

(insert-data\n  db-spec\n  :public.account_holders\n  {:account_holder_id      (java.util.UUID/randomUUID)\n   :first_name             \"Rachel\"\n   :last_name              \"Rocketpack\"\n   :email_address          \"rach@rocketpack.org\"\n   :residential_address    \"1 Ultimate Question Lane, Altar IV\"\n   :social_security_number \"BB104312D\"} )\n
"},{"location":"relational-databases-and-sql/next-jdbc-library/#hintnextjdbc-getting-started-guide","title":"HINT::next.jdbc getting started guide","text":"

next.jdbc getting started guide is very detailed.

"},{"location":"relational-databases-and-sql/next-jdbc-library/add-to-project/","title":"Add next.jdbc to a project","text":"

Create a new Clojure project using clj-new tool (see Clojure install for details)

clojure -T:project/new :template app :name practicalli/simple-database\n
"},{"location":"relational-databases-and-sql/next-jdbc-library/add-to-project/#main-library-dependency","title":"Main library dependency","text":"

Edit the deps.edn file in the root of the project directory.

In the :deps hash-map, add next.jdbc libraries as a dependency

{:deps {org.clojure/clojure    {:mvn/version \"1.10.1\"}\n        org.seancorfield/next.jdbc {:mvn/version \"1.1.569\"}}}\n

An alias should be used to include the H2 database library as a development dependency, to avoid including it in the packaged project.

{% tabs practicalli=\"practicalli/clojure-deps-edn\", manual=\"Manually add Alias\" %}

{% content \"practicalli\" %}

practicalli/clojure-deps-edn provides user level aliases that can be used with any project

:database/h2 adds the library dependency for H2 database

{% content \"manual\" %}

Edit the project deps.edn file in the root of the project (or add an alias to the user level deps.edn to use with any project).

Include an :extra-deps section for the H2 library

{:deps {org.clojure/clojure    {:mvn/version \"1.10.1\"}\n        org.seancorfield/next.jdbc {:mvn/version \"1.1.569\"}}}\n\n :aliases\n {:database/h2\n  {:extra-deps {com.h2database/h2 {:mvn/version \"2.1.210\"}}}}\n

{% endtabs %}

"},{"location":"relational-databases-and-sql/next-jdbc-library/add-to-project/#starting-a-repl-for-development","title":"Starting a REPL for development","text":"

Include the :database/h2 alias when starting a REPL

clojure -M:database/h2:repl/rebel\n
"},{"location":"relational-databases-and-sql/next-jdbc-library/add-to-project/#staging-and-production-dependencies","title":"Staging and Production dependencies","text":"

Assuming PostgreSQL is used as the staging and production database, the postgres library should be added to the main dependencies of the project.

In the :deps hash-map, add the PostgreSQL JDBC driver library as dependencies along-side next.jdbc.

{:deps\n  {org.clojure/clojure {:mvn/version \"1.10.1\"}\n\n  ;; Database\n  org.seancorfield/next.jdbc    {:mvn/version \"1.1.582\"}\n  org.postgresql/postgresql {:mvn/version \"42.2.16\"}}}\n
"},{"location":"relational-databases-and-sql/next-jdbc-library/add-to-project/#hintcheck-for-latest-library-versions","title":"Hint::Check for latest library versions","text":"

Check clojars.org for latest version org.seancorfield/next.jdbc and Maven Central for latest version of H2 database

jdbc.postgres.org shows the latest release, or look at the Postgresql page on Maven Central

"},{"location":"relational-databases-and-sql/next-jdbc-library/connection-pool-lifecycle/","title":"Using next.jdbc with a Connection pool","text":"

As the scale of database use increases it becomes more efficient to continually re-use existing connections to the database, rather than create a new connection to execute each SQL statement.

A connection pool is a set of open connections that are used over and over again, enhancing the performance of the database and allowing the database to scale more efficiently.

Databases may provide their own connection pool (postgres has ..., h2 has ...). Hikari and c3p0 are commonly used database connection pool libraries

"},{"location":"relational-databases-and-sql/next-jdbc-library/connection-pool-lifecycle/#configure-nextjdbc-with-a-connection-pool","title":"Configure next.jdbc with a connection pool","text":"
  • Add connection pool library (question: if using a db connection pool, is this just the driver?)
  • Require next.jdbc and next.jdbc.connection in the Clojure namespace where the connection pool will be used

{% tabs hikari=\"hikari\", c3p0=\"C3P0\", h2=\"H2 database\", postgresql=\"PostgreSQL database\" %}

{% content \"hikari\" %}

(ns my.main\n  (:require\n    [next.jdbc :as jdbc]\n    [next.jdbc.connection :as connection])\n\n  (:import\n    (com.zaxxer.hikari HikariDataSource)))\n

Create a database specification

HikariCP requires :username instead of :user in the db-spec

(def ^:private db-spec {:dbtype \"...\" :dbname \"...\" :username \"...\" :password \"...\"})\n

When using a JDBC URL with a connection pool, use :jdbcUrl in the database spec instead of :dbtype, :dbname, etc)

{% content \"c3p0\" %}

(ns my.main\n  (:require\n    [next.jdbc :as jdbc]\n    [next.jdbc.connection :as connection])\n\n  (:import\n    (com.mchange.v2.c3p0 ComboPooledDataSource PooledDataSource)))\n

{% content \"h2\" %}

{% endtabs %}

"},{"location":"relational-databases-and-sql/next-jdbc-library/connection-pool-lifecycle/#execute-with-a-connection-pools","title":"Execute with a connection pools","text":"

next.jdbc.connection/->pool takes a connection pool (Java Class) and a database specification.

(with-open [^HikariDataSource ds (connection/->pool HikariDataSource db-spec)]\n    (jdbc/execute! ds ...)\n    (jdbc/execute! ds ...)\n    (do-other-stuff ds args)\n    (into [] (map :column) (jdbc/plan ds ...)))\n
"},{"location":"relational-databases-and-sql/next-jdbc-library/connection-pool-lifecycle/#configure-nextjdbc-with-lifecycle-management-libraries","title":"Configure next.jdbc with lifecycle management libraries","text":"

A connection pool has a start/stop lifecycle, so fits easily into lifecycle management libraries such as mount, component and integrant.

Start the database server connection pool

Assumes database is a separate service already running. Add a check for the status of the database before starting the components?

{% tabs mount=\"Mount\", component=\"Component\", integrant=\"Integrant\" %}

{% content \"mount\" %}

{% content \"component\" %} next.jdbc.connection/component supports Component directly by creating a Component-compatible entity.

Example code from next.jdbc.connection/component

(component/start (connection/component HikariDataSource db-spec))\n
(ns practicalli.application\n  (:require\n    [com.stuartsierra.component :as component]\n    [next.jdbc :as jdbc]\n    [next.jdbc.connection :as connection])\n\n  (:import\n    (com.zaxxer.hikari HikariDataSource)))\n\n(def ^:private db-spec {:dbtype \"...\" :dbname \"...\" :username \"...\" :password \"...\"})\n\n(defn -main [& args]\n  ;; connection/component takes the same arguments as connection/->pool:\n  (let [ds (component/start (connection/component HikariDataSource db-spec))]\n    (try\n      ;; \"invoke\" the data source component to get the javax.sql.DataSource:\n      (jdbc/execute! (ds) ...)\n      (jdbc/execute! (ds) ...)\n      ;; can pass the data source component around other code:\n      (do-other-stuff ds args)\n      (into [] (map :column) (jdbc/plan (ds) ...))\n      (finally\n        ;; stopping the component will close the connection pool:\n        (component/stop ds)))))\n

{% content \"integrant\" %}

{% endtabs %}

"},{"location":"relational-databases-and-sql/next-jdbc-library/database-specifications/","title":"Database Specifications","text":"

Call the next.jdbc/get-datasource function with a database specification or a JDBC URL string

A database specification is a hash map describing the database you wish to connect to. TODO: examples of database specifications

A \"database spec\" is a Clojure map that specifies how to access the data source. Specify the database type, the database name, and the username and password.

(def db-spec\n  {:dbtype \"mysql\"\n   :dbname \"db-name\"\n   :user \"user-account\"\n   :password \"secret\"})\n

use aero to use a different database specification based on the environment being run (dev, test, prod, etc.)

next.jdbc also works with connection pooling libraries which can be used to construct a datasource from. Examples include HikariCP or c3p0

"},{"location":"relational-databases-and-sql/next-jdbc-library/next-jdbc-and-resultsets/","title":"next.jdbc and result sets","text":"

We are using the db-query-with-resultset to apply a result-set-fn on the result-set lazily (in the db sense) but the fetch-size doesn't seem to be respected. If I do a (count result-set) it returns the size of the all the rows expected from the query instead of the fetch size, this is how our function looks like. clojure.java.jdbc version is \"0.3.5\"

(defn do-lazy-read [db-spec sql-params size result-set-fn]\n  (jdbc/db-query-with-resultset\n    db-spec\n    (into [] (cons {:fetch-size size} sql-params))\n    (fn [result-set]\n      (prn (count result-set))\n      (-> result-set\n          (jdbc/result-set-seq :identifiers qstr/underscores->hyphens)\n          result-set-fn))))\n

That is expected :fetch-size is not a limit, it's just a hint for each \"chunk\" of the overall result set during database access.

But we are facing memory issues and we think this not being lazy is the cause, number of rows are in the order of a few 100,000 rows to a million

You need reducible-query

Haven\u2019t used it before, but it seems it will close the connection after reducing the result-set, how would I go about maintaining the cursor? (edited)

I am going through the documentation, will explore reducible query. But the question is if lets say the fetch size 1000 is just a hint, why is the hint not considered? Why would it always return all the rows, that too rows close to a million?

Reading this answer of yours https://stackoverflow.com/questions/39765943/clojure-java-jdbc-lazy-query/39775018#39775018 and the linked docs and the other SO question on why jdbc ignores setFetchSize (edited)

  1. fetch size tells the JDBC driver to try to only fetch that many rows at a time but it is not a limit on how many rows come back in the result set 17:43
  2. the result set is built lazily -- so result-set is a lazy sequence and if you call count you will realize the entire sequence, which will be you 1M rows 17:44
  3. even trying to process result set lazily and using fetch, you are at the usual mercy of Clojure's treatment of very large lazy sequences -- and you must completely process the result set before c.j.j. closes the connection (otherwise you'll get errors when you try to realize the next piece of the lazy result set -- because it relies on the connection staying open). 17:45
  4. since all of that is very tricky (as you're discovering), reducible-query was added so you can process the result set in a single pass reduction without needing to worry about laziness 17:46 FWIW, next.jdbc is built on that concept as a primary API: next.jdbc/plan is explicitly a reducible that is also \"foldable\" (in the clojure.core.reducers/fold sense so you can achieve some level of concurrency as well). 17:47 The reducible-query function in c.j.j. is the predecessor to next.jdbc/plan -- but the latter is better designed for performance (as is the whole of next.jdbc). 17:49 As another part of #3 above: holding onto the head is definitely a possibility -- as with processing any very large lazy sequence, but you're dealing with a Clojure problem there, not a JDBC problem. 17:49
"},{"location":"relational-databases-and-sql/next-jdbc-library/simple-example/","title":"Simple database example","text":"

Create a project called simple database

clojure -T:project/new :template app :name practicalli/simple-database\n

Edit the deps.edn file in the root of the project directory.

In the :deps hash-map, add next.jdbc library as dependency and add a :dev alias could include an :extra-deps section for the H2 driver

{:deps {org.clojure/clojure        {:mvn/version \"1.10.1\"}\n        org.seancorfield/next.jdbc {:mvn/version \"1.1.569\"}}}\n\n{:dev\n  {:extra-deps {com.h2database/h2 {:mvn/version \"1.4.200\"}}}}\n

{% tabs repl=\"In the REPL\", project=\"In a Clojure Project\" %}

{% content \"repl\" %}

"},{"location":"relational-databases-and-sql/next-jdbc-library/simple-example/#using-nextjdbc-in-a-repl-session","title":"Using next.jdbc in a REPL session","text":"

In a terminal window, change to the root directory of the simple-database project.

Start a Rebel REPL from the root of the new project

cd simple-project\n\nclojure -M:repl/rebel\n
The first time libraries are used they are downloaded and cached locally (~/.m2/repository)

Require the next.jdbc namespace using an alias called jdbc

(require '[next.jdbc :as jdbc])\n

Define a database specification containing the details of the H2 database to be used

(def db-specification {:dbtype \"h2\" :dbname \"address-book\"})\n

Define a data source that is a connection to the database

(def data-source (jdbc/get-datasource db-specification))\n

Create a table in the database using a standard SQL statement

(jdbc/execute!\n  data-source\n  [\"create table contacts (\n     id int auto_increment primary key,\n     name varchar(32),\n     email varchar(255))\"])\n

An address-book.mv.db file is created in the root of the project

Insert an entry into the database by executing an SQL insert query

(jdbc/execute!\n  data-source\n  [\"insert into contacts(name,email)\n    values('Jenny Jetpack','jenny@jetpack.org')\"])\n

View all the records added to the database (there should be only one)

(jdbc/execute!\n  data-source\n  [\"select * from address\"])\n

To delete all the records in the database, drop the contacts table in the database

(jdbc/execute!\n  data-source\n  [\"drop table contacts\"])\n

{% content \"project\" %}

Edit the file src/practicalli/simple-database.clj from the simple-database project.

Update the practicalli.simple-database namespace definition with a require statement for next.jdbc

(ns practicalli.simple-database\n  (:gen-class)\n  (:require [next.jdbc :as jdbc]))\n

Define a data source for the H2 database

(def db-specification {:dbtype \"h2\" :dbname \"address-book\"})\n

Define a data source that is a connection to the database

(def data-source (jdbc/get-datasource db-specification))\n

Create a table in the database using a standard SQL statement

(jdbc/execute!\n  data-source\n  [\"create table contacts (\n     id int auto_increment primary key,\n     name varchar(32),\n     email varchar(255))\"])\n

An address-book.mv.db file is created in the root of the project

Insert an entry into the database by executing an SQL insert query

(jdbc/execute!\n  data-source\n  [\"insert into contacts(name,email)\n    values('Jenny Jetpack','jenny@jetpack.org')\"])\n

View all the records added to the database (there should be only one)

(jdbc/execute!\n  data-source\n  [\"select * from address\"])\n

To delete all the records in the database, drop the contacts table in the database

(jdbc/execute!\n  data-source\n  [\"drop table contacts\"])\n

{% endtabs %}

"},{"location":"relational-databases-and-sql/next-jdbc-library/simple-example/#hintdatabase-driver-lookup","title":"Hint::Database driver lookup","text":"

The :dbtype (:classname) is used to find the correct database driver

"},{"location":"service-repl-workflow/","title":"Clojure Service REPL workflow","text":"

Practicalli Service REPL workflow extends the reloading concept and tools used in Practicalli REPL Reloaded workflow to Clojure Services.

Services are composed of components such as HTTP server, database connection, log publisher, message queue, request router, etc. These components can be updated by evaluating code changes as they are made, although some changes require the component or whole system to be restarted.

Lifecycle tools (Donut, Integrant, Mount, etc.) manage the components in a system, e.g. (start), (restart), (stop). The state of a running system can be inspected, (system), as it is represented by a Clojure hash-map.

System Configuration as Data

Practicalli recommends expressing a system configuration as data to provide a readily understandable system.

A component may have a dependancy on one or more other components, so starting and stoping the sytem should manage components in the correct order, e.g.:

  • a request router component is dependent on an http server component
  • a request router is dependant on a database connection to an external data source

Using a data configuration, each component is a top-level key associated with hash-map containing the component configuration, optionally using an aero profile to support multiple environments in which the components run (e.g. development, testing, staging, production)

"},{"location":"service-repl-workflow/#aero-configuration","title":"Aero Configuration","text":"

Aero provides tag litterals to use with an EDN configuration file to inject values based on a #profile or #env environment variables.

Multiple configurations can be defined within the same file, i.e. resources/config.edn, with each profile providing values for a specific environment, e.g. dev, test, stage and prod environments.

"},{"location":"service-repl-workflow/#system-component-libraries","title":"System component libraries","text":"
  • Mount - shared data approach using custom defstate atom
  • donut.system - configuration as data, functions to manage components
  • Integrant & Integrant REPL - configuration as data, runtime polymorphism (defmethod) to manage components, repl reloading tools
  • Component - provides lifecycle protocol approach using defrecord
  • Component.repl - development tools for Component
  • JUXT/clip
"},{"location":"service-repl-workflow/#references","title":"References","text":"

Enter Integrant - James Reeves

Practicalli REPL Reloaded Workflow

Clojure Reloaded workflow

"},{"location":"service-repl-workflow/aero/","title":"Aero System configuration","text":"

Aero library is an EDN reader that provides reader tags (tag literals) to support the declarative definition of system components, especially across different deployment environments (e.g. dev, stage, production).

A specific profile value (e.g. :dev :stage :prod) is given to aero/read-config which parses the Integrant configuration, returning an updated Integrant configuration containing values specific to the given profile.

Quote

Configuration should be explicit obvious, but not clever. It should be easy to understand what the config is, and where it is declared. - JUXT.

"},{"location":"service-repl-workflow/aero/#aero-reader","title":"Aero reader","text":"

Require the aero.core library and use the read-config function to process an EDN configuration file saved in resources/config.end.

ProjectREPL

Project with aero and supporting requires

(ns practicalli.gameboard.environment\n  (:require\n   [aero.core       :as aero]\n   [clojure.java.io :as io]))\n\n(defn aero-config\n  \"Profile specific configuration for all services\"\n  [profile]\n  ;; (mulog/log ::aero-parse-config :profile profile :local-time (java.time.LocalDateTime/now)\n  (aero/read-config (io/resource \"config.edn\") {:profile profile}))\n

Project with aero and supporting requires

(require '[aero.core :as aero])\n(require '[clojure.java.io :as io])\n(aero/read-config (io/resource \"config.edn\"))\n

resources/config.edn commonly used with aero

System EDN configuration loaded at runtime are created within the resources directory which is typically defined as part of the class path and therefore available even when the service is packaged in a jar file.

clojure.java.io/resource reads a file from the resources directory, the resulting file can be passed to the aero.core/reader fuction with an optional profile value.

"},{"location":"service-repl-workflow/aero/#aero-tag-literals","title":"Aero Tag Literals","text":"

Aero uses tag literals as placeholders for specific values

  • #profile - replace with the value from the given profile name
  • #env - replace with the value of the matching operating system environment variable
  • #hostname - replace with the value from the given computer hostname
  • #or - a vector of possible values, returning the first \"truthy\" value
  • #long - cast a String value to a Clojure Long type (e.g. for PORT values)
  • #ref - refer to another part of the system configuration rather than duplicate it
  • #ig/ref - integrant version of #ref to reference another part of the system
Aero tag literal definitions

aero/core.cljc contains the definitions for the Aero tag literals

Define Custom Tags

Custom tag literals can be added to extend Aero, e.g. adding Integrant Reference tag literal

"},{"location":"service-repl-workflow/aero/#profiles","title":"Profiles","text":"

Aero #profile tag literal supports multiple environment within one EDN configuration

Pass a profile and configuration to the aero reader and only the matching profile values in the configuration are returned each key. So a profile is a type of filter on the system configuration.

Aero reader with profile

Aero reader with profile
(defn aero-config\n  \"Apply profile to service configuration: :dev :stage :prod\"\n  [profile]\n  (aero/read-config (io/resource \"config.edn\") {:profile profile}))\n

As each part of the system can be defined using profiles, the same resources/config.edn configuration can be used for both Integrant and Integrant REPL

Mulog Event Publish Configuration with aero

The areo profiles (:dev :docker :prod) determine which type of publisher is use for mulog events

 ;; Event logging service - mulog\n :practicalli.gameboard.service/log-publish\n {\n  :mulog #profile {:dev  {:type :console-json :pretty? true}\n\n                   ;; Multiple publishers using Open Zipkin service (started via docker-compose)\n                   :docker  {:type :multi\n                             :publishers\n                             [{:type :console-json :pretty? false}\n                              {:type :zipkin :url \"http://localhost:9411/\"}]}\n\n                   :prod {:type :console-json :pretty? false}}}\n

As an alternative, a separate dev/resources/config.edn file could be defined if the development environment significantly deviates from stage and production. Using separate files does require additional maintenance to ensure the deployed environments are consistent.

"},{"location":"service-repl-workflow/aero/#set-default-values","title":"Set default values","text":"

#or defines a vector of possible values, returning the first \"truthy\" value.

Use #or to define a default value to avoid nil appearing in the configuration, especially where #env is used to get operating system environment variables.

{:practicalli.gameboard.service/http-server\n {:port #long #or [#env APP_SERVER_PORT 8888]}}\n
"},{"location":"service-repl-workflow/aero/#environment-variables","title":"Environment variables","text":"

#env tag will use Environment Variables from the Operating system as values in the configuration

Environment Variables must be defined in the Operating System before the Clojure REPL process is started. Adding Environment Variables after the REPL starup requires a restart of the REPL to pick up the values.

HTTP Server component with Profile and Environment Variables

{:practicalli.gameboard.service/http-server\n {:handler #ig/ref :practicalli.scoreboard.service/router\n  :port #profile {:develop #long #or [#env APP_SERVER_PORT 8888]\n                  :test    #long #or [#env APP_SERVER_PORT 8080]\n                  :stage   #long #or [#env APP_SERVER_PORT 8080]\n                  :live    #long #or [#env APP_SERVER_PORT 8000]}\n  :join? false}\n\n :practicalli.scoreboard.service/router\n {:persistence #ig/ref :practicalli.scoreboard.service/relational-store}\n\n :practicalli.scoreboard.service/relational-store\n {:connection #profile {:develop  {:url \"http://localhost/\" :port 57207 :database \"scoreboard-develop\"}\n                        :test     {:url \"http://localhost/\" :port 57207 :database \"scoreboard-test\"}\n                        :stage    {:url \"http://localhost/\" :port 57207 :database \"scoreboard-stage\"}\n                        :live     {:url \"http://localhost/\" :port 57207 :database \"scoreboard\"}}}}\n
"},{"location":"service-repl-workflow/aero/#gameboard-configuration","title":"Gameboard Configuration","text":"

An example configuration for the Practicalli Gameboard Web Service

Example configuration for Integrant and Aero

;; --------------------------------------------------\n;; Application Component configuration - Integrant & Integrant REPL\n;;\n;; - Event logging (mulog)\n;; - HTTP Server  (embedded jetty or http-kit)\n;; - Request routing (reitit)\n;; - Persistence (relational) connection\n;;\n;; #profile used by aero to select the configuration to use for a given profile (dev, test, prod)\n;; #long defines Long Integer type (required for Java HTTP server port)\n;; #env reads the environment variable of the given name\n;; #or uses first non nil value in sequence\n;;\n;; Environment variables should be defined locally and in deployment provisioner\n;; --------------------------------------------------\n\n\n{;; --------------------------------------------------\n ;; Event logging service - mulog\n\n ;; https://github.com/BrunoBonacci/mulog#publishers\n ;; https://github.com/openzipkin/zipkin\n :practicalli.gameboard.service/log-publish\n {;; Type of publisher to use for mulog events\n  ;; Publish json format logs, captured by fluentd and exposed via OpenDirectory\n  :mulog #profile {:dev  {:type :console-json :pretty? true}\n\n                   ;; Multiple publishers using Open Zipkin service (started via docker-compose)\n                   :docker  {:type :multi\n                             :publishers\n                             [{:type :console-json :pretty? false}\n                              {:type :zipkin :url \"http://localhost:9411/\"}]}\n\n                   :prod {:type :console-json :pretty? false}}}\n\n ;; --------------------------------------------------\n ;; HTTP Server - embedded service\n\n :practicalli.gameboard.service/http-server\n {;; Router function passed into the HTTP server form managing requests/responses\n  :handler #ig/ref :practicalli.gameboard.service/router\n\n  ;; Port number (Java Long type) - environment variable or default number\n  :port  #long #or [#env HTTP_SERVER_PORT 8080]\n\n  ;; Join REPL to HTTP server thread\n  :join? false}\n\n ;; --------------------------------------------------\n ;; persistence - connection to Practicall relational storage\n\n ;; TODO: add database connection pool ?\n\n :practicalli.gameboard.service/relational-store\n {:host #or [#env DATABASE_HOST \"localhost\"]\n  :port #or [#env DATABASE_PORT 3306]\n  :username #or [#env DATABASE_USERNAME \"gameboard\"]\n  :password #or [#env DATABASE_PASSWORD \"trustnoone\"]}\n\n ;; --------------------------------------------------\n ;; Data provider services\n ;; - connection to services that provide eSports data\n\n :practicalli.gameboard.service/data-provider\n {;; external data providers via Risky\n  :llamasoft-api-url  #or [#env LAMASOFT_API_URL \"http://localhost\"]\n  :polybus-report-uri \"/report/polybus\"\n  :moose-life-report-uri \"/api/v1/report/moose-life\"\n  :minotaur-arcade-report-uri \"/api/v2/minotar-arcade\"\n  :gridrunner-revolution-report-uri \"/api/v1.1/gridrunner\"\n  :space-giraffe-report-uri \"/api/v1/games/space-giraffe\"}\n\n;; --------------------------------------------------\n ;; routing\n\n ;; Configure web routing application with application environment\n ;; define top-level keys to access via the environment hash-map\n ;; - :persistence - database connection information\n ;; - :services - url, endpoint, tokens for services used by the Fraud API (e.g. risky)\n :practicalli.gameboard.service/router\n {:persistence #ig/ref :practicalli.gameboard.service/relational-store\n  :data-provider #ig/ref :practicalli.gameboard.service/data-provider}}\n
"},{"location":"service-repl-workflow/donut-system/","title":"Donut System","text":"

Donut system takes a system as data approach, using a hash-map to define the overall system with keys to define each component (or component group) in that system.

Component definitions are also a hash-map with :start, :stop, :config keys to express how to manage that component

Donut system configuration is a similar data-centric approach to that used by reitit for http request routing.

Practicalli uses ::donut alias instead of ::ds

The donut.system library is required using the :as donut alias.

::donut is used as the keyword qualifier

Practicalli recommends meaningful names to make code easier to read and searching considerably simpler (fewer false matches)

"},{"location":"service-repl-workflow/donut-system/#create-project-with-donut","title":"Create project with Donut","text":"

practicalli/service template from Practicalli Project Templates can be given a :component option to include the Donut System library and example code.

:project/create alias from Practicalli Clojure CLI Config

Create Clojure Web Service project with Donut

clojure -T:project/create :template practicalli/service :component :donut :name practicalli/web-service-name\n
"},{"location":"service-repl-workflow/donut-system/#including-donut","title":"Including Donut","text":"

Donut library includes a REPL workflow namespace, so there is only one library dependency to add to the project. This project must be included at runtime so should be added to the project deps.edn configuration

Donut dependency in Gameboard project

deps.edn
{\n:paths\n [\"src\" \"resources\"]\n\n :deps\n {;; Service\n  http-kit/http-kit {:mvn/version \"2.6.0\"}  ; latest \"2.7.0-alpha1\"\n  metosin/reitit    {:mvn/version \"0.5.13\"}\n\n  ;; Logging\n  com.brunobonacci/mulog             {:mvn/version \"0.9.0\"}\n  com.brunobonacci/mulog-adv-console {:mvn/version \"0.9.0\"}\n\n  ;; System\n  aero/aero           {:mvn/version \"1.1.6\"}\n  party.donut/system {:mvn/version \"0.0.202\"}\n  org.clojure/clojure {:mvn/version \"1.12.0\"}}}\n
"},{"location":"service-repl-workflow/donut-system/#define-a-system","title":"Define a System","text":"

Donut defines a system using a Clojure hash-map with the following top level keys

  • ::donut/defs to define components of a system or component group
  • ::donut/signals customise the startup/shutdown approach (optional)

Create a system namespace to define the donut system

Require libraries in the namespace form

(ns practicalli.gameboard.system\n  (:require\n   ;; Application dependencies\n   [practicalli.donoughty.router :as router]\n\n   ;; System dependencies\n   [org.httpkit.server     :as http-server]\n   [com.brunobonacci.mulog :as mulog]\n   [donut.system           :as donut]\n   [aero.core              :as aero]\n   [clojure.java.io        :as io]))\n

Define a system that runs a web server with event log publisher

The http server use the :env environment to determine the port, although this could be defined directly in the :http :server :config section.

There is a relationship inside the http component between server and handler. The handler depends on configuration within the :env environment configuration.

The :instance key is associated with the component reference that is returned when a component is started. The :instance reference is used to shut down the service.

The event log publisher and http service have no intrinsic relationship, so order of startup is not an issue as any mulog events created are cached until the publisher has started.

Simple Web Service

src/practicalli/gameboard/system.clj
(def system\n  \"System Component management with Donut\"\n  {::donut/defs\n   {:env  {:http-port 8080\n           :persistence {:database-host (System/getenv \"POSTGRES_HOST\")\n                         :database-port (System/getenv \"POSTGRES_PORT\")\n                         :database-username (System/getenv \"POSTGRES_USERNAME\")\n                         :database-password (System/getenv \"POSTGRES_PASSWORD\")\n                         :database-schema (System/getenv \"POSTGRES_SCHEMA\")}}\n    :event-log {:publisher\n                #::donut{:start (fn mulog-publisher-start\n                                  [{{:keys [dev]} ::donut/config}]\n                                  (mulog/start-publisher! dev))\n                         :stop (fn mulog-publisher-stop\n                                 [{::donut/keys [instance]}]\n                                 (instance))\n                         :config {:dev {:type :console :pretty? true}}}}\n    :http {:server\n           #::donut{:start (fn http-kit-run-server\n                             [{{:keys [handler options]} ::donut/config}]\n                             (http-server/run-server handler options))\n                    :stop  (fn http-kit-stop-server\n                             [{::donut/keys [instance]}]\n                             (instance))\n                    :config {:handler (donut/local-ref [:handler])\n                             :options {:port  (donut/ref [:env :http-port])\n                                       :join? false}}}\n           :handler (router/app (donut/ref [:env :persistence]))}}})\n
"},{"location":"service-repl-workflow/donut-system/#start-the-system","title":"Start the system","text":"

Use donut/signal with the ::donut/start key to start all the components in the system.

::donut/signals key is associated with a signal configuration to modify the start and stop process, although the default process should work in most cases.

Define a -main function in the main namespace of the service, e.g practicalli.gameboard.service

The -main function starts the Donut system and keeps the system reference as a local name

The system reference is used to shutdown the system, typically wrapped in code to handle SIGTERM signals from the infrastructure running the service (Operating system, Kubernettes, EC2, etc.)

Start a Donut system

(defn -main\n  \"practicalli service managed by donut system,\n  Aero is used to configure Integrant configuration based on profile (dev, test, prod),\n  allowing environment specific configuration, e.g. mulog publisher\n  The shutdown hook gracefully stops the service on receipt of a SIGTERM from the infrastructure,\n  giving the application 30 seconds before forced termination.\"\n  []\n\n  (mulog/set-global-context!\n   {:app-name \"practicalli donoughty service\" :version  \"0.1.0\"})\n\n  (mulog/log ::gameboard-system :system-config system/config)\n\n  (let [running-system (donut/signal system/system ::donut/start)]\n       (.addShutdownHook\n         (Runtime/getRuntime)\n         (Thread. ^Runnable #(donut/signal running-system ::donut/stop)))))\n
Mulog event logging is included and the system should start a mulog publisher via the system configuration

"},{"location":"service-repl-workflow/donut-system/#service-repl-workflow","title":"Service REPL Workflow","text":"

donut.system.repl namespace provides functions to start, stop and restart system components.

The main system configuration used when starting the service can also be used for the REPL, or other named systems can be defined allowing for a customised system during development.

Service REPL workflow

(ns system-repl\n  \"Tools for REPL Driven Development\"\n  (:require\n   [donut.system :as donut]\n   [donut.system.repl :as donut-repl]\n   [practicalli.donoughty.system :as donoughty]\n   [com.brunobonacci.mulog :as mulog]))\n\n(defmethod donut/named-system :donut.system/repl\n  [_] donoughty/main)\n\n(defn start\n  \"Start system with donut, optionally passing a named system\"\n  ([] (donut-repl/start))\n  ([system-config] (donut-repl/start system-config)))\n\n(defn stop\n  \"Stop the currently running system\"\n  []  (donut-repl/stop))\n\n(defn restart\n  \"Restart the system with donut repl,\n  Uses clojure.tools.namespace.repl to reload namespaces\n  `(clojure.tools.namespace.repl/refresh :after 'donut.system.repl/start)`\"\n  [] (donut-repl/restart))\n
"},{"location":"service-repl-workflow/mulog-events/","title":"Mulog","text":"

Mulog is library that defines log events as data, with a wide range of publisher for popular log aggregation services, e.g. Elastic Search, Cloudwatch, Kinesis, Prometheus, etc.)

Creating a custom publisher, all mulog events can be sent to portal data inspector.

"},{"location":"service-repl-workflow/mulog-events/#mulog-event","title":"Mulog event","text":"

mulog/log function is used to define an event message. The first argument is a unique event name, followed by key/value pairs that define the contents of the message.

Simple Mulog Event

mulog/log ::dev-user-ns :message \"Example event message\" :ns (ns-publics *ns*))\n
"},{"location":"service-repl-workflow/mulog-events/#mulog-configuration","title":"Mulog configuration","text":"

mulog/set-global-context! defines key/value pairs included in every mulog event allowing a separate context to be used for logs, e.g. :env :dev indicating development time events.

TapPublisher defines a custom Mulog publisher which wraps tap> around every mulog event created, sending each mulog event to Portal.

tap-publisher is a var that starts the custom mulog publisher, providing a reference to the publisher so it can be shut down.

stop function is provided as a convienient way to stop the publisher via the REPL.

Mulog events publisher

dev/mulog_events.clj
;; ---------------------------------------------------------\n;; Mulog Global Context and Custom Publisher\n;;\n;; - set event log global context\n;; - tap publisher for use with Portal and other tap sources\n;; - publish all mulog events to Portal tap source\n;; ---------------------------------------------------------\n\n(ns mulog-events\n  (:require\n   [com.brunobonacci.mulog        :as mulog]\n   [com.brunobonacci.mulog.buffer :as mulog-buffer]))\n\n;; ---------------------------------------------------------\n;; Set event global context\n;; - information added to every event for REPL workflow\n(mulog/set-global-context! {:service-name \"todo-tracker Service\",\n                            :version \"0.1.0\", :env \"dev\"})\n;; ---------------------------------------------------------\n\n;; ---------------------------------------------------------\n;; Mulog event publishing\n\n(deftype TapPublisher\n         [buffer transform]\n  com.brunobonacci.mulog.publisher.PPublisher\n  (agent-buffer [_] buffer)\n  (publish-delay [_] 200)\n  (publish [_ buffer]\n    (doseq [item (transform (map second (mulog-buffer/items buffer)))]\n      (tap> item))\n    (mulog-buffer/clear buffer)))\n\n#_{:clj-kondo/ignore [:unused-private-var]}\n(defn ^:private tap-events\n  [{:keys [transform] :as _config}]\n  (TapPublisher. (mulog-buffer/agent-buffer 10000) (or transform identity)))\n\n(def tap-publisher\n  \"Start mulog custom tap publisher to send all events to Portal\n  and other tap sources\n  `mulog-tap-publisher` to stop publisher\"\n  (mulog/start-publisher!\n   {:type :custom, :fqn-function \"mulog-events/tap-events\"}))\n\n#_{:clj-kondo/ignore [:unused-public-var]}\n(defn stop\n  \"Stop mulog tap publisher to ensure multiple publishers are not started\n Recommended before using `(restart)` or evaluating the `user` namespace\"\n  []\n  tap-publisher)\n\n;; ---------------------------------------------------------\n
"},{"location":"service-repl-workflow/portal/","title":"Portal","text":"

Start Portal and capture all evaluation results over nrepl when portal middleware included in the REPL startup.

All evaluation carried out over nREPL, i.e. between the connected editor and the Clojure REPL, will be sent to Portal.

Use Practicalli Clojure CLI Config aliases or define your own alias in the project or user deps.edn file.

Practicalli Clojure CLI ConfigAlias definition

:repl/reloaded aliases from Practicalli Clojure CLI Config starts a REPL process with Portal and nrepl middleware

Connect an editor to the REPL via the nREPL server port created during the REPL startup

clojure -M:repl/reloaded\n

make repl also launches Portal listening over nREPL in projects created with Practicalli Project Templates

Define an alias that includes the portal library in the :extra-deps section and portal.nrepl/wrap-portal nrepl middleware in the :main-opts section with the --middleware flag.

Clojure CLI alias including Portal & nREPL middleware

:repl/reloaded\n{:extra-paths [\"dev\" \"test\"]\n :extra-deps {nrepl/nrepl                  {:mvn/version \"1.0.0\"}\n              cider/cider-nrepl            {:mvn/version \"0.37.0\"}\n              com.bhauman/rebel-readline   {:mvn/version \"0.1.4\"}\n              djblue/portal                {:mvn/version \"0.46.0\"}\n              clj-commons/clj-yaml         {:mvn/version \"1.0.27\"}\n              org.clojure/tools.namespace  {:mvn/version \"1.4.4\"}\n              org.clojure/tools.trace      {:mvn/version \"0.7.11\"}\n              org.slf4j/slf4j-nop          {:mvn/version \"2.0.9\"}\n              com.brunobonacci/mulog       {:mvn/version \"0.9.0\"}\n              lambdaisland/kaocha          {:mvn/version \"1.86.1355\"}\n              org.clojure/test.check       {:mvn/version \"1.1.1\"}\n              ring/ring-mock               {:mvn/version \"0.4.0\"}\n              criterium/criterium          {:mvn/version \"0.4.6\"}}\n :main-opts  [\"-e\" \"(apply require clojure.main/repl-requires)\"\n              \"--main\" \"nrepl.cmdline\"\n              \"--middleware\" \"[cider.nrepl/cider-middleware,portal.nrepl/wrap-portal]\"\n              \"--interactive\"\n              \"-f\" \"rebel-readline.main/-main\"]}\n
"},{"location":"service-repl-workflow/portal/#launching-portal","title":"Launching Portal","text":"

Start Portal listening to all evaluations

dev/portal.clj
(ns portal\n  (:require\n   [portal.api :as inspect]))\n\n(def instance\n  \"Open portal window if no portal sessions have been created.\n   A portal session is created when opening a portal window\"\n  (or (seq (inspect/sessions))\n      (inspect/open {:portal.colors/theme :portal.colors/gruvbox})))\n\n;; Add portal as tapsource (add to clojure.core/tapset)\n(add-tap #'portal.api/submit)\n
"},{"location":"service-repl-workflow/portal/#themes","title":"Themes","text":"

A portal theme can be specified when starting portal, e.g. :portal.colors/gruvbox.

"},{"location":"service-repl-workflow/portal/#reference-docs","title":"Reference Docs","text":"

Portal nREPL connection documentation

"},{"location":"service-repl-workflow/system-repl/","title":"System REPL workflow","text":"

dev/system_repl.clj file provides functions to start, stop and restart the components in the Clojure system. This functions are required into the custom user namespace (dev/user.clj).

Practicalli Project templates creates a dev/system_repl.clj with one of the following approaches

  • (default) Atom reference to restart http server and refresh namespaces
  • :component :donut provides a donut-party/system definition and component management functions, including refresh namepaces
  • :component :integrant provides Integrant REPL: Integrant REPL using Integrant system definition and component management functions, including refresh namepaces
AtomDonut SystemIntegrant REPL

A reference to the http server started by http-kit is held in a Clojure atom named http-server-reference. The reference can be used to stop the http server without requiring a restart of the Clojure REPL.

System REPL - Atom based Restart

;; ---------------------------------------------------------\n;; System REPL - Atom Restart \n;;\n;; Tools for REPl workflow with Aton reference to HTTP server \n;; https://practical.li/clojure-web-services/app-servers/simple-restart/\n;; ---------------------------------------------------------\n\n(ns system-repl\n  (:require \n    [clojure.tools.namespace.repl :refer [refresh]]\n    [practicalli.todo-tracker.service :as service]))\n\n;; ---------------------------------------------------------\n;; HTTP Server State\n\n(defonce http-server-instance (atom nil))\n;; ---------------------------------------------------------\n\n\n;; ---------------------------------------------------------\n;; REPL workflow commands\n\n(defn stop\n  \"Gracefully shutdown the server, waiting 100ms\"\n  []\n  (when-not (nil? @http-server-instance)\n    (@http-server-instance :timeout 100)\n    (reset! http-server-instance nil)\n    (println \"INFO: HTTP server shutting down...\")))\n\n(defn start\n  \"Start the application server and run the application\"\n  [& port]\n  (let [port (Integer/parseInt\n              (or (first port)\n                  (System/getenv \"PORT\")\n                  \"8080\"))]\n    (println \"INFO: Starting server on port:\" port)\n\n    (reset! http-server-instance\n            (service/http-server-start port))))\n\n\n(defn restart\n  \"Stop the http server, refresh changed namespace and start the http server again\"\n  []\n  (stop)\n  (refresh)  ;; Refresh changed namespaces\n  (start))\n;; ---------------------------------------------------------\n

donut-party/system is a data-centric configuration that includes functions to :start and :stop each component.

dev/service_repl.clj defines functions to manage the components defined in the Clojure system

  • start the components, optionally passing a named system
  • stop components in the currently running system
  • restart stops components, reloads namespaces and starts components
  • system returns a hash-map of currently running system state

Donut System REPL functions

;; ---------------------------------------------------------\n;; Donut System REPL\n;;\n;; Tools for REPl workflow with Donut system components\n;; ---------------------------------------------------------\n\n(ns system-repl\n  \"Tools for REPl workflow with Donut system components\"\n  (:require\n   [donut.system :as donut]\n   [donut.system.repl :as donut-repl]\n   [donut.system.repl.state :as donut-repl-state]\n   [practicalli.todo-donut.system :as system]))\n\n\n(defmethod donut/named-system :donut.system/repl\n  [_] system/main)\n\n(defn start\n  \"Start system with donut, optionally passing a named system\"\n  ([] (donut-repl/start))\n  ([system-config] (donut-repl/start system-config)))\n\n(defn stop\n  \"Stop the currently running system\"\n  []  (donut-repl/stop))\n\n(defn restart\n  \"Restart the system with donut repl,\n  Uses clojure.tools.namespace.repl to reload namespaces\n  `(clojure.tools.namespace.repl/refresh :after 'donut.system.repl/start)`\"\n  [] (donut-repl/restart))\n\n(defn system\n  \"Return: fully qualified hash-map of system state\"\n  [] donut-repl-state/system)\n

Donut System configuration

;; ---------------------------------------------------------\n;; practicalli.todo-donut\n;;\n;; TODO: Provide a meaningful description of the project\n;;\n;; Start the service using donut configuration and an environment profile.\n;; ---------------------------------------------------------\n\n(ns practicalli.todo-donut.system\n  \"Service component lifecycle management\"\n  (:gen-class)\n  (:require\n   ;; Application dependencies\n   [practicalli.todo-donut.router :as router]\n\n   ;; Component system\n   [donut.system :as donut]\n   ;; [practicalli.todo-donut.parse-system :as parse-system]\n\n   ;; System dependencies\n   [org.httpkit.server     :as http-server]\n   [com.brunobonacci.mulog :as mulog]))\n\n;; ---------------------------------------------------------\n;; Donut Party System configuration\n\n(def main\n  \"System Component management with Donut\"\n  {::donut/defs\n   ;; Option: move :env data to resources/config.edn and parse with aero reader\n   {:env\n    {:http-port 8080\n     :persistence\n     {:database-host (or (System/getenv \"POSTGRES_HOST\") \"http://localhost\")\n      :database-port (or (System/getenv \"POSTGRES_PORT\") \"5432\")\n      :database-username (or (System/getenv \"POSTGRES_USERNAME\") \"clojure\")\n      :database-password (or (System/getenv \"POSTGRES_PASSWORD\") \"clojure\")\n      :database-schema (or (System/getenv \"POSTGRES_SCHEMA\") \"clojure\")}}\n\n    ;; mulog publisher for a given publisher type, i.e. console, cloud-watch\n    :event-log\n    {:publisher\n     #::donut{:start (fn mulog-publisher-start\n                       [{{:keys [publisher]} ::donut/config}]\n                       (mulog/log ::log-publish-component\n                                  :publisher-config publisher\n                                  :local-time (java.time.LocalDateTime/now))\n                       (mulog/start-publisher! publisher))\n\n              :stop (fn mulog-publisher-stop\n                      [{::donut/keys [instance]}]\n                      (mulog/log ::log-publish-component-shutdown :publisher instance :local-time (java.time.LocalDateTime/now))\n                      ;; Pause so final messages have chance to be published\n                      (Thread/sleep 250)\n                      (instance))\n\n              :config {:publisher {:type :console :pretty? true}}}}\n\n    ;; HTTP server start - returns function to stop the server\n    :http\n    {:server\n     #::donut{:start (fn http-kit-run-server\n                       [{{:keys [handler options]} ::donut/config}]\n                       (mulog/log ::http-server-component\n                                  :handler handler\n                                  :port (options :port)\n                                  :local-time (java.time.LocalDateTime/now))\n                       (http-server/run-server handler options))\n\n              :stop  (fn http-kit-stop-server\n                       [{::donut/keys [instance]}]\n                       (mulog/log ::http-server-component-shutdown\n                                  :http-server-instance instance\n                                  :local-time (java.time.LocalDateTime/now))\n                       (instance))\n\n              :config {:handler (donut/local-ref [:handler])\n                       :options {:port  (donut/ref [:env :http-port])\n                                 :join? false}}}\n\n     ;; Function handling all requests, passing system environment\n     ;; Configure environment for router application, e.g. database connection details, etc.\n     :handler (router/app (donut/ref [:env :persistence]))}}})\n\n;; End of Donut Party System configuration\n;; ---------------------------------------------------------\n

Integrant REPL manages Integrant components from the REPL.

Practicalli uses Integrant configuration, resources/config.edn also used by the service when deployed or otherwise calling the -main function of the service.

Integrant REPL functions

;; ---------------------------------------------------\n;; System component management for REPL workflow\n;;\n;; System config components defined in `resources/config.edn`\n;;\n;; `system` namespace automatically loaded via the `dev/user.clj` namespace\n;;\n;; Commands:\n;; `(start)` starts all components in system config\n;; `(restart)` reads system config, reloads changed namespaces & restarts system\n;; `(restart-all)` as above with all namespaces reloaded\n;; `(stop)` shutdown all components in the system (gracefully where appropriate)\n;; `(system)` show configuration of the running system\n;; `(config)` system configuration\n;;\n;; NOTE: standard IntegrantREPL code, maintenance should not be required\n;; ---------------------------------------------------\n\n(ns system-repl\n  \"Configure the system components and provide Integrant REPL convenience functions\n  to start/stop/restart components and show system configuration\"\n  (:require\n   ;; REPL workflow\n   [integrant.repl       :as ig-repl]\n   [integrant.repl.state :as ig-state]\n   [clojure.pprint :as pprint]\n\n   [practicalli.todo-integrant.parse-system :as parse-system]))\n\n(println \"Loading system namespace for Integrant REPL\")\n\n\n;; ---------------------------------------------------------\n;; System Configuration\n;; - `resources/config.edn` Integrant & Aero system configuration\n\n(defn environment-prep!\n  \"Parse system configuration with aero-reader and apply the given profile values\n  Return: Integrant configuration to be used to start the system\n  integrant.repl/set-prep! takes an anonymous function that returns an integrant configuration\n  Arguments: profile - a keyword determining the environment - :dev :test :stage :live\"\n\n  [profile]\n  (ig-repl/set-prep! #(parse-system/aero-prep profile)))\n\n;; ---------------------------------------------------------\n\n\n;; ---------------------------------------------------------\n;; Integrant REPL convenience functions\n;; - enable use of aero profiles (`dev`, `stage`, `prod`)\n;; - simplify Integrant REPL commands for managing the system\n\n(defn start\n  \"Prepare configuration and start the system services with Integrant-repl\"\n  ([] (start :dev))\n  ([profile] (environment-prep! profile) (ig-repl/go)))\n\n\n(defn restart\n  \"Read updates from the system configuration, reloads changed namespaces\n  and restart the system services with Integrant-repl\"\n  ([] (restart :dev))\n  ([profile] (environment-prep! profile) (ig-repl/reset)))\n\n\n(defn restart-all\n  \"Read updates from the configuration, reloads all namespaces\n  and restart the system services with Integrant-repl\"\n  ([] (restart-all :dev))\n  ([profile] (environment-prep! profile) (ig-repl/reset-all)))\n\n\n(defn stop\n  \"Shutdown all services\"\n  []\n  (ig-repl/halt))\n\n\n(defn system\n  \"The running system configuration,\n  including component references and specific profile values\"\n  []\n  ig-state/system)\n\n\n(defn config\n  \"The current system configuration used by Integrant\"\n  []\n  (pprint/pprint ig-state/config))\n\n;; End of Integrant REPL convenience functions\n;; ---------------------------------------------------------\n

Integrant System configuration

;; --------------------------------------------------\n;; System Component configuration - Integrant & Integrant REPL\n;;\n;; - Event logging (mulog)\n;; - HTTP Server  (embedded jetty or http-kit)\n;; - Request routing (reitit)\n;; - Persistence (relational) connection\n;;\n;; Components managed in practicalli.todo-integrant.system namespace\n;;\n;; #profile used by aero to select the configuration to use for a given profile (dev, test, prod)\n;; #long defines Long Integer type (required for Java HTTP server port)\n;; #env reads the environment variable of the given name\n;; #or uses first non nil value in sequence\n;;\n;; Environment variables should be defined locally and in deployment provisioner tooling\n;; --------------------------------------------------\n\n\n{;; --------------------------------------------------\n ;; Event logging service - mulog\n\n ;; https://github.com/BrunoBonacci/mulog#publishers\n ;; https://github.com/openzipkin/zipkin\n :practicalli.todo-integrant.system/log-publish\n {;; Type of publisher to use for mulog events\n  ;; Publish json format logs, captured by fluentd and exposed via OpenDirectory\n  :mulog #profile {:dev  {:type :console-json :pretty? true}\n\n                   ;; Multiple publishers using Open Zipkin service (started via docker-compose)\n                   :docker  {:type :multi\n                             :publishers\n                             [{:type :console-json :pretty? false}\n                              {:type :zipkin :url \"http://localhost:9411/\"}]}\n\n                   :prod {:type :console-json :pretty? false}}}\n\n ;; --------------------------------------------------\n ;; HTTP Server - embedded service\n\n :practicalli.todo-integrant.system/http-server\n {;; Router function passed into the HTTP server form managing requests/responses\n  :handler #ig/ref :practicalli.todo-integrant.system/router\n\n  ;; Port number (Java Long type) - environment variable or default number\n  :port  #long #or [#env HTTP_SERVER_PORT 8080]\n\n  ;; Join REPL to HTTP server thread\n  :join? false}\n\n ;; --------------------------------------------------\n ;; persistence - connection to relational storage\n\n ;; TODO: add database connection pool ?\n\n :practicalli.todo-integrant.system/relational-store\n {:host #or [#env DATABASE_HOST \"localhost\"]\n  :port #or [#env DATABASE_PORT 3306]\n  :username #or [#env DATABASE_USERNAME \"gameboard\"]\n  :password #or [#env DATABASE_PASSWORD \"trustnoone\"]}\n\n ;; --------------------------------------------------\n ;; Data provider services\n ;; - connection to services that provide eSports data\n\n :practicalli.todo-integrant.system/data-provider\n {;; external data providers\n  :game-service-base-url  #or [#env GAME_SERVICE_BASE_URL \"http://localhost\"]\n  :llamasoft-api-uri  #or [#env LAMASOFT_API_URI \"http://localhost\"]\n  :polybus-report-uri \"/report/polybus\"\n  :moose-life-report-uri \"/api/v1/report/moose-life\"\n  :minotaur-arcade-report-uri \"/api/v2/minotar-arcade\"\n  :gridrunner-revolution-report-uri \"/api/v1.1/gridrunner\"\n  :space-giraffe-report-uri \"/api/v1/games/space-giraffe\"}\n\n ;; --------------------------------------------------\n ;; routing\n\n ;; Configure web routing application with application environment\n ;; define top-level keys to access via the environment hash-map\n ;; - :persistence - database connection information\n ;; - :data-provider - url, endpoint, tokens for external services \n :practicalli.todo-integrant.system/router\n {:persistence #ig/ref :practicalli.todo-integrant.system/relational-store\n  :data-provider #ig/ref :practicalli.todo-integrant.system/data-provider}}\n

Integrant System components

;; ---------------------------------------------------------\n;; practicalli.todo-integrant\n;;\n;; TODO: Provide a meaningful description of the project\n;;\n;; Start the service using Integrant configuration and an environment profile.\n;; A profile is injected into the configuration in the `practicalli.gameboard.environment` namespace\n;; and the resulting configuration is used by Integrant to start the system components\n;;\n;; The service consist of\n;; - httpkit web application server\n;; - metosin/reitit for routing and ring for request / response management\n;; - mulog event logging service\n;;\n;; Related namespaces\n;; `resources/config.edn` system configuration with environment #profile placeholders\n;; `practicalli.environment` injects profile & other aero tag values into a resulting configuration\n;; ---------------------------------------------------------\n\n(ns practicalli.todo-integrant.system\n  \"Service component lifecycle management\"\n  (:gen-class)\n  (:require\n   ;; Application dependencies\n   [practicalli.todo-integrant.router :as router]\n\n   ;; Component system\n   [practicalli.todo-integrant.parse-system :as parse-system]\n\n   ;; System dependencies\n   [org.httpkit.server     :as http-server]\n   [integrant.core         :as ig]\n   [com.brunobonacci.mulog :as mulog]))\n\n;; --------------------------------------------------\n;; Configure and start application components\n\n(defn initialise\n  \"initialise the system using Integrant\"\n  [profile]\n  (ig/init (parse-system/aero-prep profile)))\n\n;; Start mulog publisher for the given publisher type, i.e. console, cloud-watch\n#_{:clj-kondo/ignore [:unused-binding]}\n(defmethod ig/init-key ::log-publish\n  [_ {:keys [mulog] :as config}]\n  (mulog/log ::log-publish-component :publisher-config mulog :local-time (java.time.LocalDateTime/now))\n  (let [publisher (mulog/start-publisher! mulog)]\n    publisher))\n\n;; Connection for Relational Database Persistence\n;; return hash-map of connection values: endpoint, access-key, secret-key\n;; TODO: add example of connection pool\n(defmethod ig/init-key ::relational-store\n  [_ {:keys [connection] :as config}]\n  (mulog/log ::persistence-component :connection connection :local-time (java.time.LocalDateTime/now))\n  config)\n\n;; Connections for data services\n(defmethod ig/init-key ::data-provider\n  [_ config]\n  (mulog/log ::data-provider-component :configuration config :local-time (java.time.LocalDateTime/now))\n  config)\n\n;; Configure environment for router application, e.g. database connection details, etc.\n(defmethod ig/init-key ::router\n  [_ config]\n  (mulog/log ::app-routing-component :app-config config)\n  (router/app config))\n\n;; HTTP server start - returns function to stop the server\n(defmethod ig/init-key ::http-server\n  [_ {:keys [handler port join?]}]\n  (mulog/log ::http-server-component :handler handler :port port :local-time (java.time.LocalDateTime/now))\n  (http-server/run-server handler {:port port :join? join?}))\n\n;; Shutdown HTTP service\n(defmethod ig/halt-key! ::http-server\n  [_ http-server-instance]\n  (mulog/log ::http-server-component-shutdown  :http-server-object http-server-instance :local-time (java.time.LocalDateTime/now))\n  ;; Calling http instance shuts down that instance\n  (http-server-instance))\n\n;; Shutdown Log publishing\n(defmethod ig/halt-key! ::log-publish\n  [_ publisher]\n  (mulog/log ::log-publish-component-shutdown :publisher-object publisher :local-time (java.time.LocalDateTime/now))\n  ;; Pause so final messages have chance to be published\n  (Thread/sleep 250)\n  ;; Call publisher again to stop publishing\n  (publisher))\n\n(defn stop\n  \"Stop service using Integrant halt!\"\n  [system]\n  (mulog/log ::http-server-sigterm :system system :local-time (java.time.LocalDateTime/now))\n  ;; (println \"Shutdown of service via Integrant\")\n  (ig/halt! system))\n\n;; --------------------------------------------------\n
"},{"location":"service-repl-workflow/integrant/","title":"Integrant Overview","text":"

Integrant manages the life-cycle of components that are composed to create the Clojure service, i.e. start, stop, restart.

Integrant uses a declarative configuration (resources/config.edn) to define a system configuration.

Components are managed using runtime polymorphism, i.e. defmethod, to define how each component is managed.

  • init-key start a component
  • halt-key! stop a component

Integrant REPL manages components during development to restart the services, loading all code changes into the REPL (especially useful after ranaming functions and namespaces)

integrant.repl.state/config shows the configuration used to start the service. integrant.repl.state/system to inspect the configuration state of the running system.

Integrant and Integrant REPL can share the same system configuration file, although are otherwise separate ways of working with a system.

"},{"location":"service-repl-workflow/integrant/#integrant-configuration","title":"Integrant configuration","text":"

Define the configuration for each part of the system, such as http server (jetty, httpkit), router application (reitit, compojure, ring) and persistence storage (postgres, crux)

Use a shared resources/config.edn file with Integrant for consistency. Or if there is significant experimentation to be done, create a dev/resources/config.edn file

Define composite components

resources/config.edn
{:practicalli.gameboard.service/http-server\n {:handler #ig/ref :practicalli.gameboard.service/router\n  :port  8888\n  :join? false}\n\n :practicalli.gameboard.service/router\n {:persistence #ig/ref :practicalli.gameboard.service/relational-store}\n\n :practicalli.gameboard.service/relational-store\n {:connection  {:url \"http://localhost/\" :port 57207 :database \"gameboard\"}}}\n

Fully qualified keywords, e.g. domain.service.name/component, are used so that keys are unique throughout the system.

The fully qualified name is the namespace that contains the defmethod init-key for the key. The Integrant load-namespaces function will automatically load all namespaces that match key names

"},{"location":"service-repl-workflow/integrant/#composite-components","title":"Composite components","text":"

Components can be composed of configuration and references to other components, creating a composite component.

For example an HTTP component may reference a request handler component and in turn the request handler may include a database connection component.

Integrant uses the #ig/ref tag literal to define a references to anther component.

Component relationships in a Clojure web service
  • relational-store defines a database connection
  • an request router includes a reference to the database connection, so handlers can be passed connection details
  • http-server includes a reference to the router that will assign all requests to the relevant handler functions
    {:practicalli.gameboard.service/relational-store\n {:connection {:url \"http://localhost/\" :port 57207 :database \"scoreboard\"}}\n\n :practicalli.gameboard.service/router\n {:persistence #ig/ref :practicalli.gameboard.service/relational-store}}\n\n :practicalli.gameboard.service/http-server\n {:handler #ig/ref :practicalli.gameboard.service/router\n  :join? false}\n
"},{"location":"service-repl-workflow/integrant/#aero-and-integrant","title":"Aero and Integrant","text":"

Aero defines a range of tag literals that can be used in a system configuration.

Aero does not include the #ig/ref reference so needs to be taught how to handle this tag using a defmethod.

Define integrant ref tag for Aero reader

Define ig/ref tag for Aero reader
(defmethod aero/reader 'ig/ref\n  [_ tag value]\n  (ig/ref value))\n

Now aero can parse a system configuration EDN file that contains Integrant references

"},{"location":"service-repl-workflow/integrant/#integrant-system-configuration","title":"Integrant System Configuration","text":"

The system configuration is a hash-map with each component defined as a top-level key and its associated values (a hash-map of key-value pairs). Component dependencies are defined by including a component key within the definition of another component.

Defining relationships between services, such as an HTTP server and a Persistent store, are achieved by passing the relevant parts of the configuration to each service. As this is information is managed at the top level of a Clojure system, it avoids unnecessary coupling between system services.

The request router is dependant on a database connection and an external data provider connection to provide information to the handlers that satisfy the requests.

Define the request router as a composite component including these dependant components via an Integrant reference, #ig/ref

Integrant dependant components

resources/clojure.edn
 {:practicalli.gameboard.service/router\n  {:persistence #ig/ref :practicalli.gameboard.service/relational-store\n   :data-provider #ig/ref :practicalli.gameboard.service/data-provider}}\n
"},{"location":"service-repl-workflow/integrant/integrant-system/","title":"Integrant implementation","text":"

Define

  • an integrant system configuration in resources/config.edn
  • Integrant init-key used to start each component
  • Integrant halt-key! to stop each component
  • Define -main function to load the system configuration, optionally parse with aero, start all components and hold a reference to the running system that listens to SIGTERM events
"},{"location":"service-repl-workflow/integrant/integrant-system/#prepare-system","title":"Prepare system","text":"

Use prep function to load namespaces into the REPL.

Aero Parse system config and load component namespaces

Prepare system confige and load namespaces
(defn aero-prep\n  \"Parse the system config and update values for the given profile (:dev, :test :prod)\n  Top-level keys in `config.edn` use fully qualified namespace name for `ig/init-key` defmethod\n  `ig/load-namespaces` automatically loads each namespace referenced by a top-level key\n  Return: configuration hash-map for specified profile (:dev :test :prod) with aero tags resolved\"\n  [profile]\n  (let [config (aero-config profile)]\n    ;; (mulog/log ::integrant-load-namespaces :config config :local-time (java.time.LocalDateTime/now)\n    (ig/load-namespaces config)\n    config))\n
"},{"location":"service-repl-workflow/integrant/integrant-system/#initialise-components","title":"Initialise Components","text":"

Define how components are initialised (started and/or configured)

Start mulog publisher for the given publisher type, i.e. console, cloud-watch

Start mulog publisher

(defmethod ig/init-key ::log-publish\n  [_ {:keys [mulog] :as config}]\n  (mulog/log ::log-publish-component :publisher-config mulog :local-time (java.time.LocalDateTime/now))\n  (let [publisher (mulog/start-publisher! mulog)]\n    publisher))\n

Connection for a Relational Database Persistence which returns a hash-map of connection values: endpoint, access-key, secret-key

Database Connection

(defmethod ig/init-key ::relational-store\n  [_ {:keys [connection] :as config}]\n  (mulog/log ::persistence-component :connection connection :local-time (java.time.LocalDateTime/now))\n  config)\n

TODO: add example of connection pool

Configure environment for router application, e.g. database connection details, etc.

Request Router

(defmethod ig/init-key ::router\n  [_ config]\n  (mulog/log ::app-routing-component :app-config config)\n  (router/app config))\n

HTTP server start - returns function to stop the server

HTTP Server start

(defmethod ig/init-key ::http-server\n  [_ {:keys [handler port join?]}]\n  (mulog/log ::http-server-component :handler handler :port port :local-time (java.time.LocalDateTime/now))\n  (http-server/run-server handler {:port port :join? join?}))\n
"},{"location":"service-repl-workflow/integrant/integrant-system/#shutdown-components","title":"Shutdown Components","text":"

Define how each component should be halted (if required)

Server processes should be halted gracefully

Shut down components via Integrant

Shut down all components using the Integrant system configuration
(defn stop\n  \"Stop service using Integrant halt!\"\n  [system]\n  (mulog/log ::http-server-sigterm :system system :local-time (java.time.LocalDateTime/now))\n  ;; (println \"Shutdown of Practicall Gameboard service via Integrant\")\n  (ig/halt! system))\n

Shutdown HTTP service

Halt HTTP server

(defmethod ig/halt-key! ::http-server\n  [_ http-server-instance]\n  (mulog/log ::http-server-component-shutdown  :http-server-object http-server-instance :local-time (java.time.LocalDateTime/now))\n  ;; Calling http instance shuts down that instance\n  (http-server-instance))\n
"},{"location":"service-repl-workflow/integrant/integrant-system/#halt-process-gracefully","title":"Halt process gracefully","text":"

Use the Java addShutdownHook method to obtains detail of the current JVM runtime environment and call a Clojure stop function with the Integrant system configuration to stop the components.

(.addShutdownHook (Runtime/getRuntime) (Thread. ^Runnable #(stop system)))\n
Typical 30 second shutdown

Most Cloud Infrastructure should provide 30 seconds to shut down all services before the operating system begins to shut down.

The -main function from the Gameboard Web Service which starts all components with a given Aero profile (:dev profile default).

The -main function creates a system local name from preparing the Integrant system configuration, which is passed to the Clojure service stop function via an .addShutdownHook to gracefully shut down the services

-main function to start and shut down an HTTP server gracefully

 (defn -main\n   \"Gameboard service is started with `ig/init` and the Integrant configuration,\n   with the return value bound to the namespace level `system` name.\n   Aero is used to configure Integrant configuration based on profile (dev, test, prod),\n   allowing environment specific configuration, e.g. mulog publisher\n   The shutdown hook calling a zero arity function, gracefully stopping the service\n   on receipt of a SIGTERM from the infrastructure, giving the application 30 seconds before forced termination.\"\n   []\n\n   (let [profile (or (keyword (System/getenv \"SERVICE_PROFILE\"))\n                     :dev)\n\n         ;; Add keys to every event / publish profile use to start the service\n         _ (mulog/set-global-context!\n            {:app-name \"Practicalli Gameboard Service\" :version  \"0.1.0\" :env profile})\n\n         system (ig/init (environment/aero-prep profile))\n\n         _ (mulog/log ::gameboard-system :system-config system)]\n\n     ;; Gracefully shutdown the HTTP server on recieving a SIGTERM\n     (.addShutdownHook (Runtime/getRuntime) (Thread. ^Runnable #(stop system)))))\n
"},{"location":"service-repl-workflow/integrant/integrant-system/#halt-event-log-publisher","title":"Halt Event Log Publisher","text":"

The event log publisher should be the last component to be shut down to ensure all events have been captured and had time to be published.

Shutdown the mulog event publisher process, including a (Thread/sleep 250) to sleep the thread for 250ms to give time for all events to be published.

Halt event log publisher

(defmethod ig/halt-key! ::log-publish\n  [_ publisher]\n  (mulog/log ::log-publish-component-shutdown :publisher-object publisher :local-time (java.time.LocalDateTime/now))\n  ;; Pause so final messages have chance to be published\n  (Thread/sleep 250)\n  ;; Call publisher again to stop publishing\n  (publisher))\n

Important to shut down the mulog publisher

If the mulog publisher is not shut down then multiple publishers could be run when restarting system components. Each publisher running will publish each event, leading to events being published multiple times.

The mulog configuration can be defined to run multiple types of publishers, which if not shut down on a system restart will publish mutltipe events to each type of publisher.

Ensuring all types of mulog publishers are shut down will avoid this issue.

Stoping the REPL process will also terminate any mulog publishers that were initialised.

Practicalli Gameboard Service Integrant System Configuration
(ns practicalli.gameboard.system\n  \"Service component lifecycle management\"\n  (:gen-class)\n  (:require\n   ;; Component system\n   [{{top/ns}}.{{main/ns}}.parse-system :as parse-system]\n\n   ;; System dependencies\n   [integrant.core         :as ig]\n   [com.brunobonacci.mulog :as mulog]))\n\n;; --------------------------------------------------\n;; Configure and start application components\n\n;; Start mulog publisher for the given publisher type, i.e. console, cloud-watch\n#_{:clj-kondo/ignore [:unused-binding]}\n(defmethod ig/init-key ::log-publish\n  [_ {:keys [mulog] :as config}]\n  (mulog/log ::log-publish-component :publisher-config mulog :local-time (java.time.LocalDateTime/now))\n  (let [publisher (mulog/start-publisher! mulog)]\n    publisher))\n\n;; Connection for Relational Database Persistence\n;; return hash-map of connection values: endpoint, access-key, secret-key\n;; TODO: add example of connection pool\n(defmethod ig/init-key ::relational-store\n  [_ {:keys [connection] :as config}]\n  (mulog/log ::persistence-component :connection connection :local-time (java.time.LocalDateTime/now))\n  config)\n\n;; Connections for data services\n(defmethod ig/init-key ::data-provider\n  [_ config]\n  (mulog/log ::data-provider-component :configuration config :local-time (java.time.LocalDateTime/now))\n  config)\n\n;; Configure environment for router application, e.g. database connection details, etc.\n(defmethod ig/init-key ::router\n  [_ config]\n  (mulog/log ::app-routing-component :app-config config)\n  (router/app config))\n\n;; HTTP server start - returns function to stop the server\n(defmethod ig/init-key ::http-server\n  [_ {:keys [handler port join?]}]\n  (mulog/log ::http-server-component :handler handler :port port :local-time (java.time.LocalDateTime/now))\n  (http-server/run-server handler {:port port :join? join?}))\n\n;; Shutdown HTTP service\n(defmethod ig/halt-key! ::http-server\n  [_ http-server-instance]\n  (mulog/log ::http-server-component-shutdown  :http-server-object http-server-instance :local-time (java.time.LocalDateTime/now))\n  ;; Calling http instance shuts down that instance\n  (http-server-instance))\n\n;; Shutdown Log publishing\n(defmethod ig/halt-key! ::log-publish\n  [_ publisher]\n  (mulog/log ::log-publish-component-shutdown :publisher-object publisher :local-time (java.time.LocalDateTime/now))\n  ;; Pause so final messages have chance to be published\n  (Thread/sleep 250)\n  ;; Call publisher again to stop publishing\n  (publisher))\n\n(defn stop\n  \"Stop service using Integrant halt!\"\n  [system]\n  (mulog/log ::http-server-sigterm :system system :local-time (java.time.LocalDateTime/now))\n  ;; (println \"Shutdown of Billie Fraud API service via Integrant\")\n  (ig/halt! system))\n;; --------------------------------------------------\n
"},{"location":"service-repl-workflow/integrant/repl/","title":"Integrant REPL","text":"

Integrant REPL is a library to manage components as part of a REPL workflow, to extend features provided by Integrant.

Integrant REPL includes functions to start, stop and restart services during development, enabling changes to the system without restarting the REPL process.

(start), (reset) and (stop) functions are evaluated in the REPL to control the system.

To assist debugging, (config) displays the parsed system configuration and (system) shows how the configuration has being resolved (service instances, profile values, etc.). Viewing the live system configuration is especially useful when using aero and environment variables to confirm the expected values are used.

"},{"location":"service-repl-workflow/integrant/repl/#user-namespace","title":"User namespace","text":"

Common practice is to place the Integrant REPl code in a user namespace, which is automatically loaded when the REPL process starts.

The user namespace is defined separately from the source code, as it is code to develop the service rather than part of the service itself. The user namespace is added to the dev/user.clj file and added to the classpath via an alias, e.g. :env/dev or :repl/reloaded aliases from Practicalli Clojure CLI Config

Custom user namespace

dev/user.clj
(ns user\n  (:require\n   ;; REPL workflow\n   [integrant.repl       :as ig-repl]\n   [integrant.repl.state :as ig-state]\n\n   ;; Environment parsing\n   [aero.core :as aero]\n\n   ;; Utilities\n   [clojure.pprint :as pprint]))\n

Practicalli REPL Startup - detailed examples

Practicalli Clojure CLI Config aliases defines aliases that include the dev directory that contains the user namespace on the class path

REPL ReloadedDev ToolsPath

:repl/reloaded alias starts a rich terminal REPL prompt, with the dev path and several tools to enhance the REPL workflow

clojure -M:repl/reloaded\n

:dev/reloaded alias adds the dev path and several tools to enhance the REPL workflow

clojure -M:dev/reloaded:repl/rebl\n

:env/dev alias adds the dev path on REPL start up, include the dev/user.clj file

clojure -M:env/dev:repl/rebl\n
"},{"location":"service-repl-workflow/integrant/repl/#environment-configuration","title":"Environment Configuration","text":"

Using aero with the Integrant configuration file includes tag literals that need to be resolved.

Parsing Aero tags in Integrant system configuration

;;;; Aero environment management\n\n;; extra reader tag for Integrant references\n(defmethod aero/reader 'ig/ref\n  [_ tag value]\n  (ig/ref value))\n\n(defn aero-config\n  \"Profile specific configuration for all services.\n  Profiles supported: :develop :stage :live\"\n  [profile]\n  (aero/read-config (io/resource \"config.edn\") {:profile profile}))\n\n(defn aero-prep\n  \"Parse the system config and update values for the given profile (:develop, :stag :live)\n  Top-level keys in the config.edn use a qualified name of the Clojure namespace the ig/init-key defmethod is defined in\n  ig/load-namespaces will automatically load each namespace referenced by a top-level key in the Integrant configuration\n  Return: configuration hash-map for the specified profile (:develop :stage :live)\"\n  [profile]\n  (let [config (aero-config profile)]\n    (ig/load-namespaces config)\n    config))\n
"},{"location":"service-repl-workflow/integrant/repl/#parse-configuration","title":"Parse Configuration","text":"

Parsing Integrant system configuration

(defn integrant-prep!\n  \"Parse system configuration with aero-reader and apply the given profile values\n  Return: Integrant configuration to be used to start the system\n\n  integrant.repl/set-prep! takes an anonymous function that returns an integrant configuration\n\n  Arguments: profile - a keyword determining the environment - :develop :test :stage :live\"\n\n  [profile]\n  (ig-repl/set-prep!\n   #(aero-prep profile)))\n
"},{"location":"service-repl-workflow/integrant/repl/#repl-convenience-functions","title":"REPL convenience functions","text":"

REPL convenience functions

(defn go\n  \"Prepare configuration and start the system services with Integrant-repl\"\n  ([] (go :develop))\n  ([profile] (integrant-prep! profile) (ig-repl/go)))\n\n(defn reset\n  \"Read updates from the configuration and restart the system services with Integrant-repl\"\n  ([] (reset :develop))\n  ([profile] (integrant-prep! profile) (ig-repl/reset)))\n\n(defn reset-all\n  \"Read updates from the configuration and restart the system services with Integrant-repl\"\n  ([] (reset-all :develop))\n  ([profile] (integrant-prep! profile) (ig-repl/reset-all)))\n\n(defn stop\n  \"Shutdown all services\"\n  [] (ig-repl/halt))\n\n(defn system\n  \"The running system configuration\"\n  [] ig-state/system)\n\n(defn config\n  \"The current system configuration used by Integrant\"\n  [] ig-state/config)\n
"},{"location":"service-repl-workflow/integrant/repl/#repl-commands","title":"REPL Commands","text":"

REPL commands

(comment\n  ;; Prepare and start the system using the :develop profile or specify the environment\n  (go)\n  (go :test)\n\n  ;; Reload changed and new source code files and restart the system\n  (reset)\n  (reset :develop)\n\n  ;; Reload all source code files on the Classpath and restart the system\n  (reset-all)\n  (reset-all :develop)\n\n  ;; Return the current Integrant configuration (already parsed by environment)\n  (config)\n\n  ;; Show the running system configuration, returns nil when system not running\n  (system)\n\n  ;; Shutdown the system using the app-server object reference in the Integrant state\n  (stop)\n\n  ;; Pretty print the system state in the REPL\n  (pprint/pprint ig-state/system)\n\n  #_()) ;; End of rich comment block\n
requiring-resolve for Just In Time requires

Integrant in practice provides an example of using requiring-resolve to avoid including all requires in the ns form, potentially reducing REPL startup time by not adding library

When calling an Integrant function, requiring-resolve returns the name of the symbol if already available in the REPL, or requires the functions namespace if the function is not available.

The library containing the namespace must be part of the class path when the REPL starts (or library has been hotloaded into the REPL)

(ns user\n  \"Reduce REPL startup time by not including requires\")\n\n(defmacro jit\n  \"Resolve a symbol name and require its namespace if not currently available in the REPL\"\n  [qualified-symbol]\n  `(requiring-resolve '~qualified-symbol))\n\n(defn set-prep! []\n  ((jit integrant.repl/set-prep!) #((jit feralberry.system/prep) :dev)))\n\n(defn go []\n  (set-prep!)\n  ((jit integrant.repl/go)))\n\n(defn reset []\n  (set-prep!)\n  ((jit integrant.repl/reset)))\n\n(defn system []\n  @(jit integrant.repl.state/system))\n\n(defn config []\n  @(jit integrant.repl.state/config))\n

"}]} \ No newline at end of file diff --git a/service-repl-workflow/aero/index.html b/service-repl-workflow/aero/index.html index 525090b2..804f6d09 100644 --- a/service-repl-workflow/aero/index.html +++ b/service-repl-workflow/aero/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/service-repl-workflow/donut-system/index.html b/service-repl-workflow/donut-system/index.html index f8fc5372..385561f3 100644 --- a/service-repl-workflow/donut-system/index.html +++ b/service-repl-workflow/donut-system/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/service-repl-workflow/index.html b/service-repl-workflow/index.html index d16a77e4..dfb39ac8 100644 --- a/service-repl-workflow/index.html +++ b/service-repl-workflow/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/service-repl-workflow/integrant/index.html b/service-repl-workflow/integrant/index.html index 0e8549b3..02c17c42 100644 --- a/service-repl-workflow/integrant/index.html +++ b/service-repl-workflow/integrant/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/service-repl-workflow/integrant/integrant-system/index.html b/service-repl-workflow/integrant/integrant-system/index.html index 473eb272..81fb7495 100644 --- a/service-repl-workflow/integrant/integrant-system/index.html +++ b/service-repl-workflow/integrant/integrant-system/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/service-repl-workflow/integrant/repl/index.html b/service-repl-workflow/integrant/repl/index.html index 03806973..9432584c 100644 --- a/service-repl-workflow/integrant/repl/index.html +++ b/service-repl-workflow/integrant/repl/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/service-repl-workflow/mulog-events/index.html b/service-repl-workflow/mulog-events/index.html index 3ef25db3..8467c666 100644 --- a/service-repl-workflow/mulog-events/index.html +++ b/service-repl-workflow/mulog-events/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/service-repl-workflow/portal/index.html b/service-repl-workflow/portal/index.html index ec9d085b..50da4643 100644 --- a/service-repl-workflow/portal/index.html +++ b/service-repl-workflow/portal/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/service-repl-workflow/system-repl/index.html b/service-repl-workflow/system-repl/index.html index 94b17171..2468f879 100644 --- a/service-repl-workflow/system-repl/index.html +++ b/service-repl-workflow/system-repl/index.html @@ -29,7 +29,7 @@ - + @@ -37,7 +37,7 @@ - + diff --git a/sitemap.xml b/sitemap.xml index 110cab28..276363a8 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -2,1132 +2,1132 @@ https://practical.li/clojure-web-services/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/adding-more-route/using-cond-function/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/app-servers/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/app-servers/app-server-logging/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/app-servers/atom-based-restart/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/app-servers/clojure-project/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/app-servers/create-server/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/app-servers/debugging/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/app-servers/http-kit-server-options/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/app-servers/java-system-properties/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/app-servers/jetty-server-options/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/app-servers/middleware/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/app-servers/overview/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/app-servers/route-requests/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/app-servers/routing-libraries/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/app-servers/routing/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/app-servers/set-listen-port/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/app-servers/simple-restart/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/app-servers/start-server/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/app-servers/static-content/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/assets/images/social/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/building-api/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/building-api/cheshire/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/building-api/compojure-api-template/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/building-api/create-compojure-api-project/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/building-api/json-files/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/building-api/plumatic-schema/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/building-api/ring-mock/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/building-api/ring-swagger/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/building-api/swagger/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/building-api/terminology/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/building-api/testing-api/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/building-api/end-to-end-testing/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/building-api/end-to-end-testing/curl/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/building-api/end-to-end-testing/httpie/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/building-api/end-to-end-testing/postman/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/building-api/end-to-end-testing/swagger/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/building-api/projects/game-scoreboard/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/building-api/projects/game-scoreboard/defining-scoreboard/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/building-api/projects/game-scoreboard/defining-scores/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/building-api/projects/game-scoreboard-ui/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/building-api/projects/game-scoreboard-ui/create-project/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/building-api/reitit/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/clojure-databases/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/clojure-databases/crux/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/full-app/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/introduction/contributing/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/introduction/overview/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/introduction/repl-workflow/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/introduction/requirements/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/introduction/writing-tips/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/libraries/reitit/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/libraries/reitit/constructing-routes/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/micro-framework/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/micro-framework/edge/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/micro-framework/luminus/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/micro-framework/pedestal/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/micro-services/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/project-url-shortner/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/project-url-shortner/add-alias-to-database/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/project-url-shortner/add-static-resources/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/project-url-shortner/alias-generator/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/project-url-shortner/compojure-template/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/project-url-shortner/create-database/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/project-url-shortner/create-html-form/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/project-url-shortner/create-project/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/project-url-shortner/delete-alias-from-database/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/project-url-shortner/design-data-structure/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/project-url-shortner/disable-anti-forgery-check/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/project-url-shortner/get-alias-from-database/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/project-url-shortner/html-form/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/project-url-shortner/if-let-function/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/project-url-shortner/named-alias-handler/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/project-url-shortner/persist-aliases/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/project-url-shortner/postgres-setup/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/project-url-shortner/redirect-to-full-url/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/project-url-shortner/redis-setup/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/project-url-shortner/refacor-hiccup-form/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/project-url-shortner/return-short-url/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/project-url-shortner/return-url-aliases/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/project-url-shortner/run-project/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/project-url-shortner/test-app-reloading/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/project-url-shortner/using-ring-redirect/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/project-url-shortner/whats-in-a-request/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/banking-on-clojure/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/banking-on-clojure/account-overview-page/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/banking-on-clojure/clojure-server-project/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/banking-on-clojure/clojure-spec-generate-mock-data/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/banking-on-clojure/continuous-integration/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/banking-on-clojure/create-records/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/banking-on-clojure/cyclic-load-dependency/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/banking-on-clojure/database-queries/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/banking-on-clojure/database-tables/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/banking-on-clojure/delete-records/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/banking-on-clojure/deployment-pipeline/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/banking-on-clojure/deployment-via-ci/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/banking-on-clojure/development-database/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/banking-on-clojure/generate-data-from-specs/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/banking-on-clojure/honeysql/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/banking-on-clojure/instrument-next-jdbc-functions/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/banking-on-clojure/namespace-design/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/banking-on-clojure/production-database/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/banking-on-clojure/read-records/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/banking-on-clojure/refactor-handler/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/banking-on-clojure/spec-generative-testing/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/banking-on-clojure/ui-handler-functions/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/banking-on-clojure/unit-testing-the-database/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/banking-on-clojure/unit-tests/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/banking-on-clojure/update-records/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/game-scoreboard-api/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/compojure/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/compojure/about/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/compojure/adding-dependency/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/compojure/adding-goodbye-route/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/compojure/code-so-far/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/compojure/defroutes/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/compojure/lisp-calculator/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/compojure/show-request-info/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/compojure/theory-local-name-bindings/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/compojure/theory-routing/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/compojure/theory-using-hash-maps/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/compojure/using-compojure/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/compojure/variable-path-elements/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/connect-to-postgres/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/connect-to-postgres/add-database-dependencies/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/connect-to-postgres/define-db-connection/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/create-a-handler-function/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/create-a-handler-function/add-not-found/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/create-a-handler-function/code-so-far/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/create-a-handler-function/if-function/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/create-a-handler-function/maps-and-keywords/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/create-a-project/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/create-a-project/code-so-far/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/create-a-project/update-project-details/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/create-a-webserver-with-ring/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/create-a-webserver-with-ring/add-a-jetty-webserver/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/create-a-webserver-with-ring/add-ring-dependency/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/create-a-webserver-with-ring/code-so-far/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/create-a-webserver-with-ring/coersing-types-and-java-lang/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/create-a-webserver-with-ring/configure-main-namespace/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/create-a-webserver-with-ring/include-ring-library/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/create-a-webserver-with-ring/namespaces/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/create-a-webserver-with-ring/run-webserver/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/database-model/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/database-model/alternative-approaches/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/database-model/create-table/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/database-model/create-task/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/database-model/delete-task/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/database-model/show-all-task/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/heroku/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/heroku/code-so-far/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/heroku/deploy/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/heroku/procfile/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/heroku/update-project/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/hiccup/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/hiccup/code-so-far/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/hiccup/create-new-handler/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/hiccup/updating-handlers-with-hiccup/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/introducing-ring/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/postgres/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/postgres/connect-to-heroku-postgres-from-clients/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/postgres/dataclips/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/postgres/environment-variables/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/postgres/install/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/postgres/jira-ticket/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/postgres/lobo-table-creation/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/postgres/pg-admin/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/postgres/postgres-cli/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/postgres/postgres-commands/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/postgres/postgres-performance-analytics/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/postgres/postgres-toolbelt-commands/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/refactor-namespace/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/refactor-namespace/base-routes/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/refactor-namespace/code-so-far/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/refactor-namespace/core/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/refactor-namespace/play-routes/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/refactor-namespace/task-routes/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/reloading-the-application/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/reloading-the-application/code-so-far/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/reloading-the-application/middleware/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/reloading-the-application/test-your-code-reloads/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/task-handlers/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/task-handlers/add-a-task/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/task-handlers/delete-a-task/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/task-handlers/show-task/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/todo-app/unit-test-handler-function/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/leiningen/working-example/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/slack-app/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/slack-app/create-slack-app/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/slack-app/slack-api-methods/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/slack-app/slack-scopes/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/status-monitor-deps/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/status-monitor-deps/application-server/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/status-monitor-deps/continuous-integration/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/status-monitor-deps/debugging-requests/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/status-monitor-deps/deployment-via-ci/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/status-monitor-deps/refactor-handlers-and-tests/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/projects/status-monitor-deps/unit-test-mocking-handlers/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/reference/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/reference/continuous-integration/heroku/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/reference/ring/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/reference/ring/request-map/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/relational-databases-and-sql/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/relational-databases-and-sql/h2-database/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/relational-databases-and-sql/managing-connections/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/relational-databases-and-sql/postgresql-database/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/relational-databases-and-sql/h2-database/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/relational-databases-and-sql/h2-database/database-tools/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/relational-databases-and-sql/h2-database/schema-design/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/relational-databases-and-sql/next-jdbc-library/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/relational-databases-and-sql/next-jdbc-library/add-to-project/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/relational-databases-and-sql/next-jdbc-library/connection-pool-lifecycle/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/relational-databases-and-sql/next-jdbc-library/database-specifications/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/relational-databases-and-sql/next-jdbc-library/next-jdbc-and-resultsets/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/relational-databases-and-sql/next-jdbc-library/simple-example/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/service-repl-workflow/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/service-repl-workflow/aero/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/service-repl-workflow/donut-system/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/service-repl-workflow/mulog-events/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/service-repl-workflow/portal/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/service-repl-workflow/system-repl/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/service-repl-workflow/integrant/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/service-repl-workflow/integrant/integrant-system/ - 2023-10-10 + 2023-10-20 daily https://practical.li/clojure-web-services/service-repl-workflow/integrant/repl/ - 2023-10-10 + 2023-10-20 daily \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz index 1d68910f32cbeeb444d46f07a58042bbedd3aeec..138be65edeaefc7cf7ee694fb509e2a1ee3dee96 100644 GIT binary patch literal 2264 zcmV;}2q*U+iwFp}nzzdwnuPk71F_Te%$NjJg};r&HRV=vE#4=r7;!zS2&C!*B# zD-*NFKSWEMuld zuwD?Ne+5NP{0K6`@|K$L8jgkJu&z+wje83G&QW1H z5j;I(Cz&}|plYf$p^2J0C_7$UcUL%eXngYt2YWi0S&sNFd1a)fAosBt+qSkc7;^He z3DRU}SOK?!`#JP$@SQ+|GFaS~(8K84saqOCTf-Ze*W{fTjHO{6+=w1*nVZ7w9y)UJ z$L?EGZ!{%OXE}~E7TFu&smh%p&c=D`#|uH(Q>MDyg3}^<(SCId4hRbG&Vi%e$p8nE z;88m1IG|8fv5OIx4x^j_%?cmk=w;M=Kr{@-wbh$FTw((ziebgFY@n!anIf2MNQk{Q zYMEsXz$ThPt608MkbEfC;s^;8WHl~FuS?J1s2;~?j%2*;H^Ng2F5L(oAy{$t@oli4 zqV(^@M3}+Rh>YC`e~zZ@N3#hegujOH=qx^ek{Pv3%CYtUe)BD~O5(epjf!3j5$%{d zSt*JPdIXAc+cFzhNRE;Kx&}}!9oZ=*rY>6tfj1Fp@-aIa-iBSdrPe}PBU5#wtX3~+ znkA5pdvISO5U2)7 zoo(9XaJUaytxEjI+7h;pLp{ismYG7>n5+Tb5p80;ESEqQBZ4-uw&HH3dAx~a5_}3n zze5J8d=!&Wss!5lH)2mj*WVK#SPi7MQP*3sIP%tg%}lAGIT68nsfon7c2W8f?3RML zs$#M%=Sk)yj1zJ_Wkm=jk&^W&-CG?q-ihTx>f%eKd~sQ<0bmj2NFHkaz;aZTp5DDr z6+o@-RA{;HZPQC0&%10~@uljr3?tIf+C1)4b;lkqFg|MX%1&LBHA>XgUuX#6G&psk zPPMv>1TLObj`;o*Pp-Z``Z(RG%w!7Tw6XqDGk!!c0jzPr2`P=-Fx33oW55fSt! z?=o!$LyKfIS9vug$j_(AJGi8GOP#~CyskFn9DL8>RUg&nLMPu#)s>!tR5<)vJdHX} z*wP@`C2yclHU1Euj_lR38Yb4b>tAn-P-SYip@{{bvb{=CLig>i%DgNBx=j~9Wr zmR8i=xi}hEl}Td9NT>qlFLCu8hDoML)H?fo_}2RP>kHueJ3|7@KuybB&;QV42ZKn<2WMdxL+DUe@P_FoRsi zYD*gDik9A(kuAyb49QyXOZG!0E3KI%OR`x>FPcSfn!An$q7!ebkTPF;8s8(Ns z2?Zxbun=`a!NHki>iFkiZ5H)>frgd(K7_|`T+33gH65t)a0#Pt93Mi=U8`@A=6(`Q zS;HX{;u=0jQx=&5A0luk6TRz)JeM>UK6Lf)9d0$)@VuHR1>WZkG8-oAkG?x6L|dg9 z#KAYVFbb(>!w6_q@2CYCLiu?{Gb-^T3lMARJLy!d5i9KlZ_W@idvA|mGn<~=4!p97 zi0u$H=OB;66FYQ_Hq*Owk>ijIR$llH$?AD$L${QyY-h5u#p10v*pQ!tu~I0XLC!{N z3unneyL`sj>aJdViK2CBzmWWlY|3TzM)HE`iH02FmbYB%<2@LnwP-U3tgTE3mt!vV zNpMvw!go0bVo}XxrFtV*<>D&!5)Pw|vA!~5hCF#t=9fA z3xfh0_6$UDG+fG8VLwr3bTTooC{%JB#d^(ELp!>*jeZYeEHnBoWYJyo3s?vRz>{>~k(6Py1 zB&yElTDpf$e*7;2UbVakINz{+fchXBV92^@mRj-K42ypuF(kdV)N63zD>f&{98r_0 zj%!K|?BxY^OR%Ck;0No^bOo2V=c6mV_O4`+E-AxImtro~j{;6q^8EKHf{$5Ry7Ko? m)EenREbbhaIUsDW$j|UA)Bk38wO?p5y!;zE3TNP}wg3P;Qeraz literal 2265 zcmV;~2qyO*iwFp|ktJmU|8r?{Wo=<_E_iKh0Og&{a^pA-fba7ZUAZ&v>Ambur8BpE zg53w8Nl0Qskqm$A_S-K=N^;zzc)Dwg3lyD5CEa4-@ncF(+OfS-hF-e z_3`t=CveJZ`C!*cTQ`SD*5&%b=apL&CS)PA032zgkRM{wLAFVkk>t9L)afYKv5Kl%y zIy@cf_3u9}Q=Ywk{_^?DKg6$}@fR;y&b_-#P121pM0k7F(%8$>;oX@o*I^UvuZbvi z{mR7b@r~$9hC@m9gz`>{p*Z}wcO*bUXfLo!P!sohEUX_#{QAieWIv0eOrS-Clx57+ z2-XWi^e>?3i622`SYA>SUc<4F?Dq9j;??AJ^!iY>B{}``B2QAJOTx*o_WuAe8=yx4 zIbVpk#B8*-aF!9MN$yYeES#yy-E1D5wB8ENTZy9d)mpRW7-b|lXcqu#V&j?uzjIWW zP6SWS*hyv%7O0wPO=zN~4$6)f*VPsF9U9+!!p@!!W|kwqN?sXhDad^+#GM8hjd19}o?Lac%Zy50}`0iDFo>EDI>AOQr}W8xmr# zC$-G724E9Sp;av3C`djOYq5s}3bGoPqt~Tpa8!?DG)FSt_8Z|T1($9Fj}WXl`}jIo zPf_}}Vj|4oXhguC@gTR}JH2Ihv4R6D)+)`^Ht&yp^QC6#$ zG|dvo#x=Mvk#d*n%`yTW%k1fPv1_c7Qq*-Hr8^JEkCTOC%Ci-Nk27@;Lf)KR3=I|# zqmTF@`k8(gLkq-=u5pNNd6!6-#w2;P*g`IwI$}nM7ojGg5kKEW8X>dlaP*a#83d{U zQhS?rIUKG-R;v>Kv9^Tm<4`y9rDdcLHYRI;cSM^QFUuv6#fYFytY>kx(mdWoG6_C} zq2C~bR6dHyC{+S&{Ts0*qU*1TcdQ0d&r#P~u{iS9ea%d%p*a!3dZ~%TxpqG?`_db9?!dMT=AvqvJ4~A(ONw2Q+3B4PB1=d^2$zKlr>7!*u;jpp(QZs%;FafM_zzFGNq)>)F&4a$ve2DH%pzvw7jm?hZ3T0D(9 zPuS8R*(eA!R5h1Lws~`Nb?1@`+wAhZU-Fp~P})Oz53889f%s^;;!IxKN8}0E=!XrA zeiH>vbxr>LMld3K!8iFWw9SR!4%ur@WuMPI@wco%J|ObwOJ;=A3-A*4SN#au=)FE( z*7Cw^=#RcR+tI8y?V~K74C^e0GM_Nk-GsuY=5!083}%?|nY! zmQ=@-=foGR_F3n%7<_S>FD9pj z!iSJ%_&?~d)sPW*QoO?DOwK!Z50zpP+K=#X9D6I<;wWCTZZ%|-j!&1wPb?XdXM^@= zK(NpLnuGbXhtP|WC3C=eJ7o&HDaZ`;LRCgX7BA0Y-ZDv+lE&* z5wRVj<{ad)d18l-(PDa+E^_RW!O9EYE?GVAZ0MGfm2FQpp0Ri<4mRY+V5}6%N0778 z+QM0K&@LY_wz{hqU!rJD+AkzOBAaqqy^*|NdZHnRxaBR^`gjk9Xf4{z0c$JM!DX9E zeG**NittU&fml>CS*c#hRk^rIy@bQ4W2{F;%#bHfZ0`J?BPkz<8RYd@b|s0~qjJI( zhK4|2{&yCdAeh3rhs2-+^B$IlV;RR@>%Y}U8f}t`Au7-X>OPNlh?ILRGr&l3gpQ6WTWU zlSI|oTub-R$&dd@z^j%Q0mmDjAD}*n1{kt#nx$6!HpAjyNDN7@XX-UL@fDjBWR9rG zRL3