Previous part of this series left us ready to begin actual implementation of our toy digital bank, as an attempt to get hands-on experience in Clojure REPL driven development (RDD), tackling the first endpoint in our implementation strategy: account view.
Implemented in the following PR.
Assuming you have already read both tutorials on Datomic mentioned in the previous part, section “References”, this PR wouldn’t demand any further explanation.
In that PR, the part that, in my opinion, does require explanation is setup related with REPL, exposed in the following section.
The default namespace Clojure REPLs target on startup is user. So, a common practice in RDD is implementing it explicitly in dev environment, as to avoid including it with the rest of our application when we build and deploy it.
To do so, we:
dev alias in deps.edn file, :aliases section, and within it, :extra-paths ["dev"].dir-locals.el Emacs project config file in the project’s root directory with the following contents:((nil
(cider-clojure-cli-global-options . "-A:dev")))
To do so, press C-x d to open Dired, navigate to the project’s root directory and once there, press C-x C-f to create a file named .dir-locals.el, and copy the config just shown into it.
State managed by this PR is:
src/accounts/db/conn.clj(defstate conn :start (new-connection config)
:stop (disconnect config conn))
resources directory in src/accounts/conf.clj(ns accounts.conf
(:require [mount.core :as mount :refer [defstate]]
[clojure.edn :as edn]))
(defn load-config [path]
(-> path
slurp
edn/read-string))
(defstate config
:start (load-config "resources/config.edn"))
Those operations, start and stop, are invoked precisely from the user namespace we have just been dealing with in the previous section:
(ns user
(:require [mount.core :as mount]
[accounts.db.conn :as c]
[accounts.db.queries :as q]))
(defn start []
(mount/start))
(defn stop []
(mount/stop))
Ahead in the implementation path, they will be invoked from main function in accounts.accounts namespace as well:
(ns accounts.accounts
(:require [mount.core :as mount])
(:gen-class))
(defn -main
"I don't do a whole lot ... yet."
[& args]
(println "\nCreating your server...")
(mount/start))
At this point, these are the only states declared and managed by Mount: a couple more will be added ahead in the implementation path.
In order to get the branch of this PR running, check it out:
$:(main) git checkout account-detail-db
Switched to branch 'account-detail-db'
$:(account-detail-db)
Then, in Emacs, from any Clojure source file, type C-c M-j and REPL should be started up inside Emacs. With C-x o you can switch from source code buffer (or window, in Emacs lingo) to the REPL one, back and forth; from any buffer rendering a Clojure source code file, we can go straight to REPL typing C-c C-z. The REPL loads the user namespace as explained above and, in that context, we:
load-database function in order to load the database’s schema and run the migrations to load data into the database bound to that schemaThese tasks are shown below in the context of an actual REPL session:
user> (start)
{:started ["#'accounts.conf/config" "#'accounts.db.conn/conn"]}
user> (c/load-database)
{:db-before datomic.db.Db@4eac34a8,
:db-after datomic.db.Db@72d65ed1,
:tx-data
[#datom[13194139534317 50 #inst "2021-05-03T22:29:57.745-00:00" 13194139534317 true] #datom[17592186045418 64 10000.0 13194139534317 true] #datom[17592186045418 64 0.0 13194139534317 false] #datom[17592186045419 64 20000.0 13194139534317 true] #datom[17592186045419 64 0.0 13194139534317 false] #datom[17592186045420 64 30000.0 13194139534317 true] #datom[17592186045420 64 0.0 13194139534317 false]],
:tempids {}}
user> (q/pull-account-by-id "account-1")
#:account{:id "account-1", :balance 10000.0}
user> (stop)
{:stopped ["#'accounts.db.conn/conn"]}
user>
Coded in the following PRs, involving almost exclusively Pedestal interceptors implementation:
As suggested in Pedestal documentation, we embraced interceptors as much as possible, and organized them as shown below:
request parametersresponse as the result of the interceptor chain executionTo handle data, making it flow step by step along the interceptor chain bound to every endpoint, we have to device a data structure to store data in, or take it from, the following way:
:request: this is the data which comes with the HTTP GET request, that is bound to this key in the interceptor chain’s Pedestal context:query-data: prepare-retrieve interceptors bind data to this key, leaving it prepared for retrieve interceptors:retrieved: retrieve interceptors store retrieved data here:result: display interceptors store data here in order to have it ready for the entity-render interceptor to set it in responseThe following is an example of this data structure:
{:request {:path-params {:account-id "account-1"}}
:query-data {:debit {:id "account-1"}}
:retrieved {:accounts {:report #:account{:id "account-1", :balance 10000.0}}}
:result {#:account{:id "account-1", :balance 10000.0}}}
This data structure might be built by the end of the execution of the interceptor chain bound to this endpoint: account view.
Let’s switch to the branch corresponding to the last PR for this endpoint: account-detail-e2e-testing.
This e2e testing session will begin and end in exactly the same way as our previous one on the last interceptor set, branch account-detail-db.
Functionality added in this interceptor set can be tested with:
response-for function from io.pedestal.test namespace, wrapped in our own util function, test-requestaccount-view, in our user namespace, which has basically that purpose: holding development utils.accounts.web.interceptors.validate-test>
user> (start)
{:started
["#'accounts.conf/config"
"#'accounts.db.conn/conn"
"#'accounts.web.server/server"]}
user> (c/load-database)
{:db-before datomic.db.Db@8cbce164,
:db-after datomic.db.Db@c4fe9953,
:tx-data
[#datom[13194139534317 50 #inst "2021-05-05T09:11:57.819-00:00" 13194139534317 true] #datom[17592186045418 64 10000.0 13194139534317 true] #datom[17592186045418 64 0.0 13194139534317 false] #datom[17592186045419 64 20000.0 13194139534317 true] #datom[17592186045419 64 0.0 13194139534317 false] #datom[17592186045420 64 30000.0 13194139534317 true] #datom[17592186045420 64 0.0 13194139534317 false]],
:tempids {}}
user> (test-request :get "/accounts/account-1")
{:status 200,
:body "{\"account/id\":\"account-1\",\"account/balance\":10000.0}",
:headers
{"Strict-Transport-Security" "max-age=31536000; includeSubdomains",
"X-Frame-Options" "DENY",
"X-Content-Type-Options" "nosniff",
"X-XSS-Protection" "1; mode=block",
"X-Download-Options" "noopen",
"X-Permitted-Cross-Domain-Policies" "none",
"Content-Security-Policy"
"object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;",
"Content-Type" "application/json;charset=UTF-8"}}
user> (account-view)
{:status 200,
:body "{\"account/id\":\"account-1\",\"account/balance\":10000.0}",
:headers
{"Strict-Transport-Security" "max-age=31536000; includeSubdomains",
"X-Frame-Options" "DENY",
"X-Content-Type-Options" "nosniff",
"X-XSS-Protection" "1; mode=block",
"X-Download-Options" "noopen",
"X-Permitted-Cross-Domain-Policies" "none",
"Content-Security-Policy"
"object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;",
"Content-Type" "application/json;charset=UTF-8"}}
user> (stop)
{:stopped ["#'accounts.web.server/server" "#'accounts.db.conn/conn"]}
user>
Hence, we are now ready to go on with the REPL-driven Development (RDD) Session Demo.
So, let’s jump to next part !