├── system.properties ├── Procfile ├── .dir-locals.el ├── bin ├── build └── kaocha ├── project.clj ├── test └── practicalli │ ├── banking_on_clojure_test.clj │ ├── request_handler_test.clj │ └── handler_helpers_test.clj ├── .gitattributes ├── .gitignore ├── deps.edn ├── .circleci └── config.yml ├── README.md ├── src └── practicalli │ ├── data │ ├── connection.clj │ ├── specs.clj │ ├── access.clj │ ├── schema.clj │ └── design_journal.clj │ ├── hanlder_helpers.clj │ ├── banking_on_clojure.clj │ └── request_handler.clj ├── tests.edn ├── pom.xml └── LICENSE /system.properties: -------------------------------------------------------------------------------- 1 | java.runtime.version=21 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: java -jar banking-on-clojure.jar $PORT 2 | -------------------------------------------------------------------------------- /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ((clojure-mode . ((cider-clojure-cli-aliases . ":env/test:lib/ring-mock")))) 2 | -------------------------------------------------------------------------------- /bin/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # TODO: replace with tools.build 4 | 5 | clojure -A:uberjar 6 | -------------------------------------------------------------------------------- /bin/kaocha: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## Script to run the kaocha test runner 4 | ## for unit tests and clojure spec generative tests 5 | 6 | clojure -M:test/kaocha "$@" 7 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | ;; --------------------------------------------------------- 2 | ;; Only used to inform Heroku this is a Clojure project 3 | ;; --------------------------------------------------------- 4 | -------------------------------------------------------------------------------- /test/practicalli/banking_on_clojure_test.clj: -------------------------------------------------------------------------------- 1 | (ns practicalli.banking-on-clojure-test 2 | (:require [clojure.test :refer [deftest is testing]] 3 | [practicalli.banking-on-clojure :as SUT])) 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Git Attributes 2 | 3 | # reclassifies `.edn` as Clojure files for Linguist statistics 4 | # https://github.com/github/linguist/blob/master/docs/overrides.md 5 | **/*.edn linguist-language=Clojure 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ------------------------ 2 | # Clojure Project Git Ignore file patterns 3 | # 4 | # Ignore all except patterns starting with ! 5 | # Add comments on separate lines, not same line as pattern 6 | # ------------------------ 7 | 8 | # ------------------------ 9 | # Ignore everthing in root directory 10 | /* 11 | 12 | # ------------------------ 13 | # Common project files 14 | !CHANGELOG.md 15 | !README.md 16 | !LICENSE 17 | 18 | # ------------------------ 19 | # Include Clojure project & config 20 | !build.clj 21 | !deps.edn 22 | !pom.xml 23 | !dev/ 24 | !docs/ 25 | !resources/ 26 | !src/ 27 | !test/ 28 | 29 | # ------------------------ 30 | # Include Clojure tools 31 | !.cljstyle 32 | !.dir-locals.el 33 | !compose.yaml 34 | !Dockerfile 35 | !.dockerignore 36 | !.clj-kondo/config.edn 37 | !Makefile 38 | !tests.edn 39 | 40 | # ------------------------ 41 | # Include Git & CI workflow 42 | !.gitattributes 43 | !.gitignore 44 | !.github/ 45 | 46 | # ------------------------ 47 | # Include ClojureScript Figwheel 48 | !figwheel-main.edn 49 | !*.cljs.edn 50 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths 2 | ["src" "resources"] 3 | 4 | :deps 5 | {org.clojure/clojure {:mvn/version "1.12.0"} 6 | 7 | ;; Web Application 8 | http-kit/http-kit {:mvn/version "2.3.0"} 9 | ring/ring-core {:mvn/version "1.8.1"} 10 | ring/ring-devel {:mvn/version "1.8.1"} 11 | compojure/compojure {:mvn/version "1.6.1"} 12 | hiccup/hiccup {:mvn/version "2.0.0-alpha2"} 13 | 14 | ;; Database 15 | seancorfield/next.jdbc {:mvn/version "1.1.582"} 16 | camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.1"} 17 | com.h2database/h2 {:mvn/version "1.4.200"} 18 | org.postgresql/postgresql {:mvn/version "42.2.16"}} 19 | 20 | :aliases 21 | { 22 | :test 23 | {:extra-paths ["test"] 24 | :extra-deps {org.clojure/test.check {:mvn/version "1.0.0"} 25 | ring/ring-mock {:mvn/version "0.4.0"}}} 26 | 27 | :runner 28 | {:extra-deps {com.cognitect/test-runner 29 | {:git/url "https://github.com/cognitect-labs/test-runner" 30 | :sha "f7ef16dc3b8332b0d77bc0274578ad5270fbfedd"}} 31 | :main-opts ["-m" "cognitect.test-runner" 32 | "-d" "test"]} 33 | 34 | :runner-kaocha 35 | {:extra-paths ["test"] 36 | :extra-deps {lambdaisland/kaocha {:mvn/version "1.0-612"}} 37 | :main-opts ["-m" "kaocha.runner"]} 38 | 39 | ;; TODO: replace with tools.build 40 | :uberjar 41 | {:extra-deps {seancorfield/depstar {:mvn/version "1.0.94"}} 42 | :main-opts ["-m" "hf.depstar.uberjar" "banking-on-clojure.jar" 43 | "-C" "-m" "practicalli.banking-on-clojure"]}}} 44 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 # circleci configuration version 2 | 3 | orbs: 4 | kaocha: lambdaisland/kaocha@0.0.1 # Org settings > Security > uncertified orbs 5 | heroku: circleci/heroku@1.1.1 # Invoke the Heroku orb 6 | 7 | workflows: 8 | heroku_deploy: 9 | jobs: 10 | - build 11 | - heroku/deploy-via-git: # Use the pre-configured job, deploy-via-git 12 | requires: 13 | - build 14 | filters: 15 | branches: 16 | only: live 17 | 18 | jobs: # basic units of work in a run 19 | build: # runs not using Workflows must have a `build` job as entry point 20 | working_directory: ~/build # directory where steps will run 21 | docker: # run the steps with Docker 22 | - image: circleci/clojure:openjdk-11-tools-deps-1.10.1.536 # image is primary container where `steps` are run 23 | environment: # environment variables for primary container 24 | JVM_OPTS: -Xmx3200m # limit the maximum heap size to prevent out of memory errors 25 | steps: # commands that comprise the `build` job 26 | - checkout # check out source code to working directory 27 | - restore_cache: # restores saved cache if checksum hasn't changed since the last run 28 | key: banking-on-clojure-webapp-{{ checksum "deps.edn" }} 29 | - run: clojure -R:test:runner -Spath 30 | - save_cache: # generate and store cache in the .m2 directory using a key template 31 | paths: 32 | - ~/.m2 33 | - ~/.gitlibs 34 | key: banking-on-clojure-webapp-{{ checksum "deps.edn" }} 35 | - run: bin/kaocha --reporter kaocha.report/documentation --no-randomize --no-color --plugin kaocha.plugin.alpha/spec-test-check 36 | -------------------------------------------------------------------------------- /test/practicalli/request_handler_test.clj: -------------------------------------------------------------------------------- 1 | (ns practicalli.request-handler-test 2 | (:require 3 | ;; Unit testing 4 | [clojure.test :refer [deftest is testing]] 5 | [ring.mock.request :as mock] 6 | 7 | ;; Clojure Spec and Generative testing 8 | [clojure.spec.alpha :as spec] 9 | [clojure.spec.gen.alpha :as spec-gen] 10 | [clojure.spec.test.alpha :as spec-test] 11 | 12 | ;; System under test 13 | [practicalli.request-handler :as SUT])) 14 | 15 | ;; --------------------------------------------------------- 16 | ;; Unit tests 17 | 18 | (deftest welcome-page-test 19 | (testing "Testing elements on the welcome page" 20 | (is (= 200 21 | (:status (SUT/welcome-page (mock/request :get "/"))))))) 22 | 23 | (deftest accounts-overview-page-test 24 | (testing "Testing elements on the accounts overview page" 25 | (is (= 200 26 | (:status (SUT/accounts-overview-page (mock/request :get "/accounts"))))))) 27 | 28 | (deftest account-history-test 29 | (testing "Testing elements on the account history page" 30 | (is (= 200 31 | (:status (SUT/account-history (mock/request :get "/account"))))))) 32 | 33 | (deftest money-transfer-test 34 | (testing "Testing elements on the money transfer page" 35 | (is (= 200 36 | (:status (SUT/money-transfer (mock/request :get "/money-transfer"))))))) 37 | 38 | (deftest money-payment-test 39 | (testing "Testing elements on the money payment page" 40 | (is (= 200 41 | (:status (SUT/money-payment (mock/request :get "/money-payment"))))))) 42 | 43 | (deftest register-account-holder-test 44 | (testing "Testing elements on the register account holder page" 45 | (is (= 200 46 | (:status (SUT/account-history (mock/request :get "/register-account-holder"))))) 47 | #_(is (valid?)))) 48 | 49 | ;; End of Unit tests 50 | ;; --------------------------------------------------------- 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # banking-on-clojure-webapp 2 | 3 | A Clojure web application using `clojure.spec` libraries for defining data and function contracts to be used for generative testing. 4 | 5 | A [guide to the development of this project](http://practicalli.github.io/clojure-webapps/projects/banking-on-clojure/) is on the Practicalli ClojureWebApps website. 6 | 7 | [![CircleCI](https://circleci.com/gh/circleci/circleci-docs.svg?style=svg)](https://circleci.com/gh/practicalli/banking-on-clojure-webapp) 8 | 9 | Code Repository: [practicalli/banking-on-clojure-webapp](https://github.com/practicalli/banking-on-clojure-webapp) 10 | 11 | ## Development 12 | Open the code in a Clojure aware editor and start a REPL session. 13 | 14 | Run all the tests in the project using the Cognitect Labs test runner, setting the classpath to include `test` directory. The aliases are included in the project `deps.edn` file. 15 | 16 | ```shell 17 | clojure -A:test:runner 18 | ``` 19 | 20 | ## Running the code 21 | Use the `-M -m` option to set the main namespace to inform Clojure where it can find the `-main` function to start the code running. 22 | 23 | ```shell 24 | clojure -M -m practicalli.banking-on-clojure 25 | ``` 26 | > Use the `-m` option by itself if using Clojure CLI tools version before 1.10.1.697 27 | 28 | ## Packaging / Deployment 29 | Clojure is deployed as a Java archive (jar) file, an archive created using zip compression. To package the code to run in a JVM environment, an uberjar is created which included the project code and the Clojure standard library. This is called an uberjar. 30 | 31 | Use the alias for depstar tool to build an uberjar for this project. The alias is defined in the `deps.edn` file for this project. 32 | 33 | ```shell 34 | clojure -A:uberjar 35 | ``` 36 | 37 | The code can be run from the uberjar on the command line 38 | 39 | ```shell 40 | java -jar banking-on-clojure-webapp.jar 41 | ``` 42 | 43 | 44 | ## License 45 | 46 | Copyright © 2020 Practicalli 47 | 48 | Distributed under the Creative Commons Attribution Share-Alike 4.0 International 49 | -------------------------------------------------------------------------------- /src/practicalli/data/connection.clj: -------------------------------------------------------------------------------- 1 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 2 | ;; 3 | ;; Database specifications for relational databases 4 | ;; 5 | ;; - DONE: next.jdbc database specifications 6 | ;; - TODO: connection pool 7 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 8 | 9 | (ns practicalli.data.connection) 10 | 11 | 12 | ;; H2 database - embedded relational db 13 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 14 | 15 | ;; Evironment: development 16 | (def db-spec-dev 17 | "H2 development database specification for next.jdbc" 18 | {:dbtype "h2" :dbname "banking-on-clojure"}) 19 | 20 | 21 | ;; Heroku Postgres database - PostgreSQL as a service 22 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 23 | 24 | (def db-spec-staging 25 | "PostgreSQL Staging database specification for next.jdbc" 26 | (System/getenv "JDBC_DATABASE_URL_STAGING")) 27 | 28 | (def db-spec 29 | "PostgreSQL production database specification for next.jdbc" 30 | (System/getenv "JDBC_DATABASE_URL")) 31 | 32 | 33 | ;; Create environment variables using Heroku CLI tool 34 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 35 | ;; Run an instance of the app (container) and send the command `echo $JDBC_DATABASE_URL` 36 | ;; heroku run echo \$JDBC_DATABASE_URL --app banking-on-clojure-staging 37 | 38 | ;; This returns the correct JDBC connection string 39 | ;; jdbc:postgresql://:port/?user=&password=&sslmode=require 40 | 41 | ;; This jdbc connection string is generated from the DATABASE_URL config var that is added to the heroku app when a database is provisioned. 42 | 43 | ;; The Heroku build scripts automatically create a JDBC_DATABASE_URL value from the DATABASE_URL, so it is reasonable to expect the 44 | 45 | 46 | 47 | 48 | ;; Rich comment block with redefined vars ignored 49 | #_{:clj-kondo/ignore [:redefined-var]} 50 | (comment 51 | 52 | (:require '[next.jdbc :as jdbc]) 53 | 54 | (defn information-tables 55 | [data-source] 56 | (jdbc/execute! data-source ["select * from information_schema.tables"])) 57 | 58 | (information-tables db-spec-dev) 59 | (information-tables db-spec-staging) 60 | (information-tables db-spec) 61 | 62 | ) ;; End of rich comment block 63 | -------------------------------------------------------------------------------- /tests.edn: -------------------------------------------------------------------------------- 1 | #kaocha/v1 2 | {:kaocha/tests 3 | [{:kaocha.testable/id :unit 4 | :kaocha.testable/type :kaocha.type/clojure.test 5 | :kaocha/ns-patterns ["-test$"], 6 | :kaocha/source-paths ["src"], 7 | :kaocha/test-paths ["test"], 8 | :kaocha.filter/skip-meta [:kaocha/skip]} 9 | 10 | {:kaocha.testable/id :generative-fdef-checks 11 | :kaocha.testable/type :kaocha.type/spec.test.check 12 | :kaocha/source-paths ["src"] 13 | :kaocha.spec.test.check/checks [{:kaocha.spec.test.check/syms :all-fdefs 14 | :clojure.spec.test.check/instrument? true 15 | :clojure.spec.test.check/check-asserts? true 16 | :clojure.spec.test.check/opts {:num-tests 10}}]} 17 | ] 18 | 19 | :kaocha/reporter [kaocha.report/documentation] 20 | ;; :kaocha/reporter [kaocha.report/dots] 21 | 22 | :kaocha/color? #profile {:default true 23 | :ci false} 24 | 25 | ;; Run tests of file changes, unless running in CI server 26 | :kaocha/watch #profile {:default true :ci false} 27 | 28 | :kaocha/fail-fast? true 29 | 30 | :kaocha.plugin.randomize/randomize? false 31 | 32 | :kaocha/plugins 33 | [:kaocha.plugin/randomize 34 | :kaocha.plugin/filter 35 | :kaocha.plugin/capture-output 36 | :kaocha.plugin.alpha/spec-test-check] 37 | 38 | :kaocha.plugin.capture-output/capture-output? true 39 | 40 | } 41 | 42 | 43 | ;; print config 44 | ;; {:kaocha/tests 45 | ;; [{:kaocha.testable/type :kaocha.type/clojure.test, 46 | ;; :kaocha.testable/id :unit, 47 | ;; :kaocha/ns-patterns ["-test$"], 48 | ;; :kaocha/source-paths ["src"], 49 | ;; :kaocha/test-paths ["test"], 50 | ;; :kaocha.filter/skip-meta [:kaocha/skip]} 51 | 52 | ;; {:kaocha.testable/id :generative-fdef-checks 53 | ;; :kaocha.testable/type :kaocha.type/spec.test.check 54 | ;; :kaocha/source-paths ["src"]}] 55 | 56 | ;; :kaocha/watch #profile {:default true 57 | ;; :ci false} 58 | 59 | ;; :color? #profile {:default true 60 | ;; :ci false} 61 | 62 | ;; :kaocha/fail-fast? true 63 | 64 | ;; :kaocha/color? :color? #profile {:default true :ci false} 65 | 66 | ;; :kaocha/cli-options {:config-file "tests.edn", :print-config true}, 67 | 68 | ;; :kaocha.plugin.randomize/seed 196869937 69 | 70 | ;; :kaocha.plugin.randomize/randomize? false 71 | 72 | ;; :kaocha/plugins 73 | ;; [:kaocha.plugin/randomize 74 | ;; :kaocha.plugin/filter 75 | ;; :kaocha.plugin/capture-output 76 | ;; :kaocha.plugin.alpha/spec-test-check] 77 | 78 | ;; :kaocha.plugin.capture-output/capture-output? true 79 | 80 | ;; :kaocha/reporter [kaocha.report/dots]} 81 | -------------------------------------------------------------------------------- /src/practicalli/hanlder_helpers.clj: -------------------------------------------------------------------------------- 1 | (ns practicalli.hanlder-helpers 2 | (:require 3 | [practicalli.data.access 4 | :refer [create-record update-record read-record delete-record 5 | create-tables delete-tables]] 6 | [practicalli.data.connection :as connection])) 7 | 8 | ;; --------------------------------------------------------- 9 | ;; Business Logic - helpers for request handlers 10 | 11 | ;; Database schemas 12 | (defn create-database [] (create-tables connection/db-spec-dev)) 13 | (defn delete-database [] (delete-tables connection/db-spec-dev)) 14 | 15 | ;; Create 16 | 17 | (defn new-customer 18 | [customer-details] 19 | (create-record connection/db-spec-dev :public.customer customer-details)) 20 | 21 | (defn new-account 22 | [account-details] 23 | (create-record connection/db-spec-dev :public.account account-details)) 24 | 25 | (defn new-transaction 26 | [transaction-details] 27 | (create-record connection/db-spec-dev :public.transaction transaction-details)) 28 | 29 | ;; Update 30 | 31 | (defn update-customer 32 | [updated-values customer-id] 33 | (update-record connection/db-spec-dev :public.customer updated-values customer-id)) 34 | 35 | (defn update-account 36 | [updated-values account-id] 37 | (update-record connection/db-spec-dev :public.account updated-values account-id)) 38 | 39 | (defn update-transaction 40 | [updated-values transaction-id] 41 | (update-record connection/db-spec-dev :public.transaction updated-values transaction-id)) 42 | 43 | ;; Read 44 | 45 | (defn customer-account-overview 46 | [customer-id] 47 | (read-record connection/db-spec-dev 48 | ["select * from public.customer where id = ?" customer-id])) 49 | 50 | (defn account-details 51 | [account-id] 52 | (read-record connection/db-spec-dev ["select * from public.account where id = ?" account-id])) 53 | 54 | (defn transaction-history 55 | [transaction-id] 56 | (read-record connection/db-spec-dev ["select * from public.transaction where id = ?" transaction-id])) 57 | 58 | ;; Delete 59 | 60 | (defn delete-customer 61 | [customer-id] 62 | (delete-record connection/db-spec-dev :public.customer customer-id)) 63 | 64 | (defn delete-account 65 | [account-id] 66 | (delete-record connection/db-spec-dev :public.account account-id)) 67 | 68 | (defn delete-transaction 69 | [transaction-id] 70 | (delete-record connection/db-spec-dev :public.transaction transaction-id)) 71 | 72 | ;; --------------------------------------------------------- 73 | 74 | ;; --------------------------------------------------------- 75 | ;; Rich comment block with redefined vars ignored 76 | #_{:clj-kondo/ignore [:redefined-var]} 77 | (comment 78 | 79 | ;; Create or delete the database tables from practicalli.data.schema 80 | (create-database) 81 | (delete-database)) 82 | 83 | ;; End of rich comment block 84 | ;; --------------------------------------------------------- 85 | -------------------------------------------------------------------------------- /test/practicalli/handler_helpers_test.clj: -------------------------------------------------------------------------------- 1 | ;; --------------------------------------------------------- 2 | ;; Request Handler Unit tests 3 | ;; --------------------------------------------------------- 4 | 5 | (ns practicalli.handler-helpers-test 6 | (:require 7 | ;; Unit testing 8 | [clojure.test :refer [deftest is testing use-fixtures]] 9 | 10 | ;; Specifications and Generative testing 11 | [clojure.spec.alpha :as spec] 12 | [clojure.spec.gen.alpha :as spec-gen] 13 | [practicalli.data.specs] 14 | 15 | ;; System under test 16 | [practicalli.hanlder-helpers :as SUT])) 17 | 18 | ;; TODO fixtures to setup database... 19 | ;; once: create / clean the database by dropping tables then creating tables 20 | ;; each: where we are using pre-existing data - drop-create-insert ?? 21 | 22 | ;; --------------------------------------------------------- 23 | ;; Fixtures 24 | 25 | (defn database-reset-fixture 26 | "Setup: drop all tables, creates new tables 27 | Teardown: drop all tables 28 | SQL schema code has if clauses to avoid errors running SQL code. 29 | Arguments: 30 | test-function - a function to run a specific test" 31 | [test-function] 32 | (SUT/create-database) 33 | (test-function) 34 | (SUT/delete-database)) 35 | 36 | (use-fixtures :each database-reset-fixture) 37 | ;; --------------------------------------------------------- 38 | 39 | ;; --------------------------------------------------------- 40 | ;; Test selector 41 | 42 | (deftest ^:database new-customer-test 43 | (testing "New customer generative testing" 44 | (is (spec/valid? 45 | :customer/id 46 | (:customer/id (SUT/new-customer 47 | (spec-gen/generate (spec/gen :customer/unregistered)))))))) 48 | 49 | (deftest ^:kaocha/skip-test new-transaction-test 50 | (testing "New customer generative testing" 51 | (is (spec/valid? 52 | :customer/id 53 | (:customer/id (SUT/new-customer 54 | (spec-gen/generate (spec/gen :customer/unregistered)))))))) 55 | 56 | ;; --------------------------------------------------------- 57 | 58 | ;; --------------------------------------------------------- 59 | ;; Rich comment block with redefined vars ignored 60 | 61 | #_{:clj-kondo/ignore [:redefined-var]} 62 | (comment 63 | 64 | (SUT/new-customer 65 | (spec-gen/generate (spec/gen :customer/unregistered))) 66 | 67 | ;; Register a customer and use the returned uuid to get that customers details 68 | (SUT/customer-account-overview 69 | (:customer/id (SUT/new-customer 70 | (spec-gen/generate (spec/gen :customer/unregistered))))) 71 | 72 | ;; Register a customer and use the returned uuid to get that customers details 73 | #_(SUT/customer-details 74 | (:account-holders/id (SUT/new-customer 75 | (spec-gen/generate (spec/gen :customer/unregistered)))))) 76 | 77 | ;; End of rich comment block 78 | ;; --------------------------------------------------------- 79 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | practicalli 5 | banking-on-clojure 6 | 0.1.0-SNAPSHOT 7 | practicalli/banking-on-clojure 8 | FIXME: my new application. 9 | https://github.com/practicalli/banking-on-clojure-webapp 10 | 11 | 12 | Eclipse Public License 13 | http://www.eclipse.org/legal/epl-v10.html 14 | 15 | 16 | 17 | 18 | Practicalli 19 | 20 | 21 | 22 | https://github.com/practicalli/banking-on-clojure-webapp 23 | scm:git:git://github.com/practicalli/banking-on-clojure-webapp.git 24 | scm:git:ssh://git@github.com/practicalli/banking-on-clojure-webapp.git 25 | HEAD 26 | 27 | 28 | 29 | org.clojure 30 | clojure 31 | 1.11.2 32 | 33 | 34 | ring 35 | ring-devel 36 | 1.8.1 37 | 38 | 39 | compojure 40 | compojure 41 | 1.6.1 42 | 43 | 44 | com.h2database 45 | h2 46 | 2.3.232 47 | 48 | 49 | org.postgresql 50 | postgresql 51 | 42.3.9 52 | 53 | 54 | hiccup 55 | hiccup 56 | 2.0.0-alpha2 57 | 58 | 59 | seancorfield 60 | next.jdbc 61 | 1.1.582 62 | 63 | 64 | camel-snake-kebab 65 | camel-snake-kebab 66 | 0.4.1 67 | 68 | 69 | ring 70 | ring-core 71 | 1.8.1 72 | 73 | 74 | http-kit 75 | http-kit 76 | 2.3.0 77 | 78 | 79 | 80 | src 81 | 82 | 83 | 84 | clojars 85 | https://repo.clojars.org/ 86 | 87 | 88 | 89 | 90 | clojars 91 | Clojars repository 92 | https://clojars.org/repo 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /src/practicalli/banking_on_clojure.clj: -------------------------------------------------------------------------------- 1 | ;; --------------------------------------------------------- 2 | ;; Banking On Clojure 3 | ;; 4 | ;; Web Application example of a high street bank 5 | ;; --------------------------------------------------------- 6 | 7 | (ns practicalli.banking-on-clojure 8 | (:gen-class) 9 | (:require 10 | [org.httpkit.server :as app-server] 11 | [compojure.core :refer [defroutes GET POST]] 12 | [practicalli.request-handler :as handler])) 13 | 14 | ;; --------------------------------------------------------- 15 | ;; Request Routing 16 | ;; TODO: change to Reitit / Reitit-ring 17 | 18 | (defroutes app 19 | (GET "/" [] handler/welcome-page) 20 | (GET "/accounts" [] handler/accounts-overview-page) 21 | (GET "/account" [] handler/account-history) 22 | (GET "/transfer" [] handler/money-transfer) 23 | (GET "/payment" [] handler/money-payment) 24 | (GET "/register" [] handler/register-customer)) 25 | ;; --------------------------------------------------------- 26 | 27 | ;; --------------------------------------------------------- 28 | ;; System 29 | 30 | ;; Reference to application server instance for stopping/restarting 31 | (defonce app-server-instance (atom nil)) 32 | 33 | (defn app-server-start 34 | "Start the application server and log the time of start." 35 | 36 | [http-port] 37 | (println (str (java.util.Date.) 38 | " INFO: Starting server on port: " http-port)) 39 | (reset! app-server-instance 40 | (app-server/run-server #'app {:port http-port}))) 41 | 42 | (defn app-server-stop 43 | "Gracefully shutdown the server, waiting 100ms. Log the time of shutdown" 44 | [] 45 | (when-not (nil? @app-server-instance) 46 | (@app-server-instance :timeout 100) 47 | (reset! app-server-instance nil) 48 | (println (str (java.util.Date.) 49 | " INFO: Application server shutting down...")))) 50 | 51 | (defn app-server-restart 52 | "Convenience function to stop and start the application server" 53 | [http-port] 54 | (app-server-stop) 55 | (app-server-start http-port)) 56 | 57 | ;; --------------------------------------------------------- 58 | ;; Entry point 59 | 60 | (defn -main 61 | "Select a value for the http port the app-server will listen to 62 | and call app-server-start 63 | 64 | The http port is either an argument passed to the function, 65 | an operating system environment variable or a default value." 66 | 67 | [& [http-port]] 68 | (let [http-port (Integer. (or http-port (System/getenv "PORT") "8888"))] 69 | (app-server-start http-port))) 70 | 71 | ;; End of Entry point 72 | ;; --------------------------------------------------------- 73 | 74 | ;; --------------------------------------------------------- 75 | ;; REPL driven development helpers 76 | 77 | (comment 78 | 79 | ;; Start application server - via `-main` or `app-server-start` 80 | (-main) 81 | (app-server-start 8888) 82 | 83 | ;; Stop / restart application server 84 | (app-server-stop) 85 | (app-server-restart 8888) 86 | 87 | ;; Get PORT environment variable from Operating System 88 | (System/getenv "PORT") 89 | 90 | ;; Get all environment variables 91 | ;; use a data inspector to view environment-variables name 92 | (def environment-variables 93 | (System/getenv)) 94 | 95 | ;; Check values set in the default system properties 96 | (def system-properties 97 | (System/getProperties)) 98 | 99 | #()) 100 | ;; --------------------------------------------------------- 101 | -------------------------------------------------------------------------------- /src/practicalli/data/specs.clj: -------------------------------------------------------------------------------- 1 | (ns practicalli.data.specs 2 | (:require 3 | [clojure.spec.alpha :as spec] 4 | [clojure.spec.gen.alpha :as spec-gen])) 5 | 6 | 7 | 8 | ;; Customer detail specifications 9 | 10 | (spec/def :customer/id uuid?) 11 | (spec/def :customer/legal-name string?) 12 | (spec/def :customer/email-address string?) 13 | (spec/def :customer/residential-address string?) 14 | (spec/def :customer/social-security-number string?) 15 | (spec/def :customer/preferred-name string?) 16 | 17 | ;; Data to send to the database 18 | (spec/def :customer/unregistered 19 | (spec/keys 20 | :req [:customer/legal-name 21 | :customer/email-address 22 | :customer/residential-address 23 | :customer/social-security-number] 24 | :opt [:customer/preferred-name])) 25 | 26 | 27 | ;; Data received from the database 28 | (spec/def :customer/registered 29 | (spec/keys 30 | :req [:customer/id 31 | :customer/legal-name 32 | :customer/email-address 33 | :customer/residential-address 34 | :customer/social-security-number] 35 | :opt [:customer/preferred-name])) 36 | 37 | 38 | 39 | 40 | 41 | ;; Rich comment block with redefined vars ignored 42 | #_{:clj-kondo/ignore [:redefined-var]} 43 | (comment 44 | 45 | (spec-gen/generate (spec/gen :customer/unregistered)) 46 | ;; => #:customer{:legal-name "7hl4PT89AO3Pe04958YBWxWH0m6tnG", :email-address "iz83P60EtVM9lMX6zg6", :residential-address "FJ7Mh6nNJviX", :social-security-number "9bYAS85axW42KnOPcPjMtkg06qb4Tr"} 47 | (spec-gen/generate (spec/gen :customer/registered)) 48 | ;; => #:customer{:preferred-name "S8i45tGAgaO60uPVW6q48Emg1", :legal-name "FFsv7pCavtC5V9qD52wO91i9Y", :email-address "6Wl3O11i3L66q800f3JcgkQ7414V0", :residential-address "vzl93YDnD74Zh5", :social-security-number "120J"} 49 | 50 | 51 | (spec-gen/generate (spec/gen :customer/id)) 52 | 53 | (spec/valid? :customer/id 54 | (spec-gen/generate (spec/gen :customer/id))) 55 | 56 | (spec/valid? :customer/id 57 | #:customer{:id #uuid "323ecc53-d676-4b7a-bea0-a4b3ca075c2c"}) 58 | ;; => false 59 | 60 | (spec/valid? :customer/id 61 | (:id #:customer{:id #uuid "323ecc53-d676-4b7a-bea0-a4b3ca075c2c"})) 62 | 63 | 64 | ) ;; End of rich comment block 65 | 66 | 67 | 68 | ;; I am using the following schema with a H2 database, which generates a uuid. 69 | ;; (def schema-customer 70 | ;; ["create table if not exists public.customer ( 71 | 72 | ;; id uuid default random_uuid() not null, 73 | ;; legal_name varchar(32) not null, 74 | ;; email_address varchar(32) not null, 75 | ;; residential_address varchar(255) not null, 76 | ;; social_security_number varchar(32) not null, 77 | ;; preferred_name varchar(32), 78 | 79 | ;; constraint customer_pk primary key (id))"]) 80 | ;; This uuid value is returned from a call to next.jdbc.sql/insert! which used the following spec to generate the new record. 81 | ;; (spec/def :customer/unregistered 82 | ;; (spec/keys 83 | ;; :req [:customer/legal-name 84 | ;; :customer/email-address 85 | ;; :customer/residential-address 86 | ;; :customer/social-security-number] 87 | ;; :opt [:customer/preferred-name])) 88 | ;; The update! function returns values in the form: #:customer{:id #uuid "323ecc53-d676-4b7a-bea0-a4b3ca075c2c"} 89 | ;; I check the result from insert! against the :customer/id specification: 90 | ;; (spec/def :customer/id uuid?) 91 | ;; Doing so in a unit test, that test is failing suggesting the returned value from insert! is not conforming to the clojure.spec specification. 92 | ;; expected: (spec/valid? 93 | ;; :customer/id 94 | ;; (SUT/new-customer 95 | ;; (spec-gen/generate (spec/gen :customer/unregistered)))) 96 | 97 | ;; actual: (not 98 | ;; (spec/valid? 99 | ;; :customer/id 100 | ;; #:customer{:id #uuid "323ecc53-d676-4b7a-bea0-a4b3ca075c2c"})) 101 | ;; Not sure where I have gone wrong here... any suggestions? 102 | -------------------------------------------------------------------------------- /src/practicalli/data/access.clj: -------------------------------------------------------------------------------- 1 | (ns practicalli.data.access 2 | (:require 3 | [next.jdbc :as jdbc] 4 | [next.jdbc.sql :as jdbc-sql] 5 | 6 | [practicalli.data.schema :as schema])) 7 | 8 | 9 | ;; Helper functions 10 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 11 | 12 | (defn ^:private execute-transaction 13 | "Execute SQL statements within a transaction, for a given database source" 14 | [sql-statements data-spec] 15 | (with-open [connection (jdbc/get-connection data-spec)] 16 | (jdbc/with-transaction [transaction connection] 17 | (doseq [sql-statement sql-statements] 18 | (jdbc/execute-one! transaction sql-statement))))) 19 | 20 | 21 | ;; Schema management 22 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 23 | 24 | (defn delete-tables 25 | "Reset all database tables" 26 | [db-spec] 27 | (execute-transaction 28 | [schema/customer-drop schema/account-drop schema/transaction-drop] db-spec)) 29 | 30 | (defn create-tables 31 | "Delete all database tables" 32 | [db-spec] 33 | (delete-tables db-spec) 34 | (execute-transaction 35 | [schema/customer-create schema/account-create schema/transaction-create] db-spec)) 36 | 37 | 38 | (defn show-table 39 | [db-spec sql-statement] 40 | (with-open [connection (jdbc/get-connection db-spec)] 41 | (jdbc/execute! connection sql-statement))) 42 | 43 | 44 | ;; Create, Read, Update, Delete 45 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 46 | 47 | (defn create-record 48 | "Insert a single record into the database using a managed connection. 49 | Arguments: 50 | - table - name of database table to be affected 51 | - record-data - Clojure data representing a new record 52 | - db-spec - database specification to establish a connection" 53 | [db-spec table record-data] 54 | (with-open [connection (jdbc/get-connection db-spec)] 55 | (jdbc-sql/insert! 56 | connection 57 | table 58 | record-data 59 | jdbc/snake-kebab-opts))) 60 | 61 | 62 | ;; Inserting multiple records 63 | 64 | (defn create-records 65 | "Insert a single record into the database using a managed connection. 66 | Arguments: 67 | - table - name of database table to be affected 68 | - columns - vector of column names for which values are provided 69 | - record-data - Clojure vector representing a new records 70 | - db-spec - database specification to establish a connection" 71 | [db-spec table columns record-data] 72 | (with-open [connection (jdbc/get-connection db-spec)] 73 | (jdbc-sql/insert-multi! 74 | connection 75 | table 76 | columns 77 | record-data 78 | jdbc/snake-kebab-opts))) 79 | 80 | 81 | (defn read-record 82 | "Insert a single record into the database using a managed connection. 83 | Arguments: 84 | - table - name of database table to be affected 85 | - record-data - Clojure data representing a new record 86 | - db-spec - database specification to establish a connection" 87 | [db-spec sql-query] 88 | (with-open [connection (jdbc/get-connection db-spec)] 89 | (jdbc-sql/query connection sql-query))) 90 | 91 | 92 | (defn update-record 93 | "Insert a single record into the database using a managed connection. 94 | Arguments: 95 | - table - name of database table to be affected 96 | - record-data - Clojure data representing a new record 97 | - db-spec - database specification to establish a connection 98 | - where-clause - column and value to identify a record to update" 99 | [db-spec table record-data where-clause] 100 | (with-open [connection (jdbc/get-connection db-spec)] 101 | (jdbc-sql/update! 102 | connection 103 | table 104 | record-data 105 | where-clause 106 | jdbc/snake-kebab-opts))) 107 | 108 | 109 | (defn delete-record 110 | "Insert a single record into the database using a managed connection. 111 | Arguments: 112 | - table - name of database table to be affected 113 | - record-data - Clojure data representing a new record 114 | - db-spec - database specification to establish a connection" 115 | [db-spec table where-clause] 116 | (with-open [connection (jdbc/get-connection db-spec)] 117 | (jdbc-sql/delete! connection table where-clause))) 118 | 119 | 120 | 121 | 122 | 123 | ;; Instrument next.jdbc specifications 124 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 125 | (comment 126 | 127 | ;; Require the next.jdbc.specs namespace before instrumenting functions 128 | (require '[next.jdbc.specs :as jdbc-spec]) 129 | 130 | ;; Instrument all next.jdbc functions 131 | (jdbc-spec/instrument) 132 | 133 | ;; Remove instrumentation from all next.jdbc functions 134 | (jdbc-spec/unstrument) 135 | 136 | ) 137 | -------------------------------------------------------------------------------- /src/practicalli/data/schema.clj: -------------------------------------------------------------------------------- 1 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 2 | ;; 3 | ;; Schema definition for relational databases 4 | ;; 5 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 6 | 7 | (ns practicalli.data.schema) 8 | 9 | 10 | ;; Create Database tables 11 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 12 | 13 | (def customer-create 14 | ["create table if not exists public.customer ( 15 | 16 | id uuid default random_uuid() not null, 17 | legal_name varchar(32) not null, 18 | email_address varchar(32) not null, 19 | residential_address varchar(255) not null, 20 | social_security_number varchar(32) not null, 21 | preferred_name varchar(32), 22 | 23 | constraint customer_pk primary key (id))"]) 24 | 25 | 26 | (def account-create 27 | ["create table if not exists public.account ( 28 | 29 | number integer not null identity, 30 | sort_code varchar(6) not null, 31 | name varchar(32) not null, 32 | balance varchar(255) not null, 33 | balance_updated date not null, 34 | customer_id varchar(100) not null, 35 | 36 | constraint account_pk primary key (number))"] ) 37 | 38 | 39 | (def transaction-create 40 | ["create table if not exists public.transaction ( 41 | 42 | id uuid default random_uuid() not null, 43 | date date not null, 44 | account_to integer not null, 45 | account_from integer, 46 | reference varchar(32), 47 | 48 | constraint transaction_pk primary key (id))"]) 49 | 50 | 51 | ;; Delete Database tables 52 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 53 | 54 | (def customer-drop 55 | ["drop table if exists public.customer"]) 56 | 57 | (def account-drop 58 | ["drop table if exists public.account"]) 59 | 60 | (def transaction-drop 61 | ["drop table if exists public.transaction"]) 62 | 63 | 64 | 65 | ;; View Database tables 66 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 67 | 68 | (def customer-table 69 | ["show columns from public.customer"]) 70 | 71 | (def account-table 72 | ["show columns from public.account"]) 73 | 74 | (def transaction-table 75 | ["show columns from public.transaction"]) 76 | 77 | 78 | 79 | ;; Database schema - helper functions 80 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 81 | 82 | ;; define a function to open a database connect, create all table schema as a transaction and then close the connection. 83 | ;; TODO: remove `with-open` when using a connection pool 84 | ;; pass pool connection to `jdbc/with-transaction` 85 | 86 | 87 | #_(defn create-tables 88 | "Establish a connection to the data source and create all tables within a transaction. 89 | Close the database connection. 90 | Arguments: 91 | - table-schemas: a vector of sql statements, each creating a table 92 | - data-spec: next.jdbc database specification" 93 | [table-schemas data-spec] 94 | 95 | (with-open [connection (jdbc/get-connection data-spec)] 96 | (jdbc/with-transaction [transaction connection] 97 | (doseq [sql-statement table-schemas] 98 | (jdbc/execute! transaction sql-statement) )))) 99 | 100 | ;; could also use `clojure.core/run!` instead of `doseq` 101 | 102 | 103 | #_(defn show-schema 104 | "Show the columns and types that define the table schema 105 | Arguments: 106 | - table-schemas: a vector of sql statements, each creating a table 107 | - data-spec: next.jdbc database specification" 108 | [db-spec table-name] 109 | (with-open [connection (jdbc/get-connection db-spec)] 110 | (jdbc/execute! connection [(str "show columns from " table-name)]))) 111 | 112 | 113 | #_(defn drop-table 114 | "Drop table in database if it exists 115 | Arguments: 116 | - next.jdbc database specification 117 | - string of table name to drop from database 118 | Returns: 119 | - next.jdbc hash-map of result, 1=table dropped, 0=table does not exist" 120 | [db-spec table-name] 121 | (with-open [connection (jdbc/get-connection db-spec)] 122 | (jdbc/execute! connection [(str "drop table if exists " table-name)]))) 123 | 124 | 125 | #_(defn drop-tables 126 | [sql-statements data-spec] 127 | (with-open [connection (jdbc/get-connection data-spec)] 128 | (jdbc/with-transaction [transaction connection] 129 | (doseq [sql-statement sql-statements] 130 | (jdbc/execute! transaction sql-statement))))) 131 | 132 | 133 | 134 | 135 | 136 | (comment ;; Managing Schema - development 137 | 138 | ;; Required database specifications before calling helper functions 139 | (require '[practicalli.data.connection 140 | :refer [db-spec-dev db-spec-staging db-spec]]) 141 | 142 | ;; Create all tables in the development database 143 | (create-tables [schema-customer schema-account schema-transaction] 144 | db-spec-dev) 145 | 146 | 147 | ;; View application table schema in development database 148 | (show-schema db-spec-dev "public.customer") 149 | (show-schema db-spec-dev "public.account") 150 | (show-schema db-spec-dev "public.transaction") 151 | 152 | ;; View database system schema in development database 153 | (show-schema db-spec-dev "information_schema.tables") 154 | 155 | ;; Remove tables from the development database 156 | (drop-table db-spec-dev "public.customer") 157 | (drop-table db-spec-dev "public.account") 158 | (drop-table db-spec-dev "public.transaction") 159 | 160 | ) ;; End of rich comment block 161 | 162 | 163 | (comment ;; Managing Schema - staging 164 | 165 | ;; Create all tables in the development database 166 | (create-tables! [schema-customer schema-account schema-transaction] 167 | db-spec-staging) 168 | 169 | ;; View application table schema in development database 170 | (show-schema db-spec-staging "public.customer") 171 | (show-schema db-spec-staging "public.account") 172 | (show-schema db-spec-staging "public.transaction") 173 | 174 | ;; View database system schema in development database 175 | (show-schema db-spec-staging "information_schema.tables") 176 | 177 | ;; Remove tables from the development database 178 | (drop-table db-spec-staging "public.customer") 179 | (drop-table db-spec-staging "public.account") 180 | (drop-table db-spec-staging "public.transaction") 181 | 182 | 183 | ) ;; End of rich comment block 184 | 185 | 186 | (comment ;; Managing Schema - production 187 | 188 | ;; Create all tables in the development database 189 | (create-tables! [schema-customer schema-account schema-transaction] 190 | db-spec) 191 | 192 | ;; View application table schema in development database 193 | (show-schema db-spec "public.customer") 194 | (show-schema db-spec "public.account") 195 | (show-schema db-spec "public.transaction") 196 | 197 | ;; View database system schema in development database 198 | (show-schema db-spec "information_schema.tables") 199 | 200 | ;; Remove tables from the development database 201 | (drop-table db-spec "public.customer") 202 | (drop-table db-spec "public.account") 203 | (drop-table db-spec "public.transaction") 204 | 205 | ) ;; End of rich comment block 206 | -------------------------------------------------------------------------------- /src/practicalli/request_handler.clj: -------------------------------------------------------------------------------- 1 | ;; --------------------------------------------------------- 2 | ;; Request handlers 3 | ;; --------------------------------------------------------- 4 | 5 | (ns practicalli.request-handler 6 | (:require 7 | ;; Web Application 8 | [ring.util.response :refer [response]] 9 | [hiccup.core :refer [html]] 10 | [hiccup.page :refer [html5 include-js include-css]] 11 | [hiccup.element :refer [link-to]] 12 | 13 | ;; Data access 14 | [practicalli.hanlder-helpers :as helper])) 15 | 16 | ;; --------------------------------------------------------- 17 | ;; Markup Generators 18 | 19 | (defn bank-account-media-object 20 | [account-details] 21 | [:article {:class "media"} 22 | [:figure {:class "media-left"} 23 | [:p {:class "image is-64x64"} 24 | [:img {:src "https://raw.githubusercontent.com/jr0cket/developer-guides/master/clojure/clojure-bank-coin.png"}]]] 25 | [:div {:class "media-content"} 26 | [:div {:class "content"} 27 | [:h3 {:class "subtitle"} 28 | (str (:account-type account-details) " : λ" (:account-value account-details))]] 29 | 30 | [:div {:class "field is-grouped"} 31 | [:div {:class "control"} 32 | [:div {:class "tags has-addons"} 33 | [:span {:class "tag"} "Account number"] 34 | [:span {:class "tag is-success is-light"} (:account-number account-details)]]] 35 | 36 | [:div {:class "tags has-addons"} 37 | [:span {:class "tag"} "Sort Code"] 38 | [:span {:class "tag is-success is-light"} (:account-sort-code account-details)]]]] 39 | 40 | [:div {:class "media-right"} 41 | (link-to {:class "button is-primary"} "/transfer" "Transfer") 42 | (link-to {:class "button is-info"} "/payment" "Payment")]]) 43 | ;; --------------------------------------------------------- 44 | 45 | ;; --------------------------------------------------------- 46 | ;; HTML Pages 47 | 48 | (defn welcome-page 49 | [request] 50 | (response 51 | (html5 52 | {:lang "en"} 53 | [:head 54 | (include-css "https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css")] 55 | [:body 56 | [:section {:class "hero is-info"} 57 | [:div {:class "hero-body"} 58 | [:div {:class "container"} 59 | [:h1 {:class "title"} "Banking on Clojure"] 60 | [:p {:class "subtitle"} 61 | "Making your money immutable"]]]] 62 | 63 | [:section {:class "section"} 64 | [:div {:class "container"} 65 | (link-to {:class "button is-primary"} "/accounts" "Login") 66 | (link-to {:class "button is-danger"} "/register" "Register") 67 | [:p {:class "content"} 68 | "Manage your money without unexpected side-effects using a simple made easy banking service"] 69 | [:img {:src "https://raw.githubusercontent.com/jr0cket/developer-guides/master/clojure/clojure-piggy-bank.png"}]]]]))) 70 | 71 | (defn register-customer 72 | [request] 73 | (response 74 | (html 75 | [:div 76 | [:h1 "Banking on Clojure"] 77 | [:p "New account holder"] 78 | (helper/new-customer 79 | #:customer{:legal-name "Terry Able" 80 | :email_address "terry@able.org" 81 | :residential_address "1 Hard Code Drive, Altar IV" 82 | :social_security_number "xx104312D" 83 | :preferred_name "Terri"}) 84 | 85 | [:img {:src "https://raw.githubusercontent.com/jr0cket/developer-guides/master/clojure/clojure-piggy-bank.png"}]]))) 86 | 87 | (defn accounts-overview-page 88 | "Overview of each bank account owned by the current customer. 89 | 90 | Using Bulma media object style 91 | https://bulma.io/documentation/layout/media-object/ 92 | 93 | Request hash-map is not currently used" 94 | 95 | [request] 96 | (response 97 | (html5 98 | {:lang "en"} 99 | [:head 100 | (include-css "https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css")] 101 | [:body 102 | [:section {:class "hero is-info"} 103 | [:div {:class "hero-body"} 104 | [:div {:class "container"} 105 | [:h1 {:class "title"} "Banking on Clojure"] 106 | [:p {:class "subtitle"} 107 | "Making your money immutable"]]]] 108 | 109 | [:section {:class "section"} 110 | (bank-account-media-object {:account-type "Current Account" :account-number "123456789" 111 | :account-value "1,234" :account-sort-code "01-02-01"}) 112 | 113 | (bank-account-media-object {:account-type "Savings Account" :account-number "123454321" 114 | :account-value "2,000" :account-sort-code "01-02-01"}) 115 | 116 | (bank-account-media-object {:account-type "Tax Free Savings Account" :account-number "123454321" 117 | :account-value "20,000" :account-sort-code "01-02-01"}) 118 | 119 | (bank-account-media-object {:account-type "Mortgage Account" :account-number "98r9e8r79wr87e9232" 120 | :account-value "354,000" :account-sort-code "01-02-01"})]]))) 121 | 122 | (defn account-history 123 | [request] 124 | (response 125 | (html5 126 | {:lang "en"} 127 | [:head 128 | (include-css "https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css")] 129 | [:body 130 | [:section {:class "hero is-info"} 131 | [:div {:class "hero-body"} 132 | [:div {:class "container"} 133 | [:h1 {:class "title"} "Banking on Clojure"] 134 | [:p {:class "subtitle"} 135 | "Making your money immutable"]]]] 136 | 137 | [:section {:class "section"} 138 | [:div {:class "container"} 139 | [:h1 {:class "title"} 140 | "Account History"] 141 | [:p {:class "content"} 142 | "Manage your money without unexpected side-effects using a simple made easy banking service"] 143 | [:img {:src "https://raw.githubusercontent.com/jr0cket/developer-guides/master/clojure/clojure-piggy-bank.png"}]]]]))) 144 | 145 | ;; TODO: Add form to transfer money from one account to another 146 | (defn money-transfer 147 | [request] 148 | (response 149 | (html5 150 | {:lang "en"} 151 | [:head 152 | (include-css "https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css")] 153 | [:body 154 | [:section {:class "hero is-info"} 155 | [:div {:class "hero-body"} 156 | [:div {:class "container"} 157 | [:h1 {:class "title"} "Banking on Clojure"] 158 | [:p {:class "subtitle"} 159 | "Making your money immutable"]]]] 160 | 161 | [:section {:class "section"} 162 | [:div {:class "container"} 163 | [:h1 {:class "title"} 164 | "Transfer Dashboard"] 165 | [:p {:class "content"} 166 | "Manage your money without unexpected side-effects using a simple made easy banking service"] 167 | [:img {:src "https://raw.githubusercontent.com/jr0cket/developer-guides/master/clojure/clojure-piggy-bank.png"}]]]]))) 168 | 169 | ;; TODO: Add form to transfer money from one account to another 170 | (defn money-payment 171 | [request] 172 | (response 173 | (html5 174 | {:lang "en"} 175 | [:head 176 | (include-css "https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css")] 177 | [:body 178 | [:section {:class "hero is-info"} 179 | [:div {:class "hero-body"} 180 | [:div {:class "container"} 181 | [:h1 {:class "title"} "Banking on Clojure"] 182 | [:p {:class "subtitle"} 183 | "Making your money immutable"]]]] 184 | 185 | [:section {:class "section"} 186 | [:div {:class "container"} 187 | [:h1 {:class "title"} 188 | "Payment Dashboard"] 189 | [:p {:class "content"} 190 | "Manage your money without unexpected side-effects using a simple made easy banking service"] 191 | [:img {:src "https://raw.githubusercontent.com/jr0cket/developer-guides/master/clojure/clojure-piggy-bank.png"}]]]]))) 192 | 193 | ;; --------------------------------------------------------- 194 | 195 | ;; --------------------------------------------------------- 196 | (comment 197 | 198 | ;; Test handler function with empty request 199 | (welcome-page {}) 200 | 201 | (html5 {}) 202 | ;; => "\n" 203 | 204 | (html5 205 | {:lang "en"} 206 | [:head 207 | (include-js "myscript.js") 208 | (include-css "mystyle.css")] 209 | [:body 210 | [:div 211 | [:h1 {:class "info"} "Hiccup"]]])) 212 | ;; => "\n

Hiccup

" 213 | 214 | ;; --------------------------------------------------------- 215 | ;; RDD 216 | 217 | (comment 218 | 219 | ;; Simple handler 220 | 221 | (defn welcome-page 222 | [request] 223 | (response (html [:div 224 | [:h1 "Banking on Clojure"] 225 | [:img {:src "https://raw.githubusercontent.com/jr0cket/developer-guides/master/clojure/clojure-piggy-bank.png"}]]))) 226 | 227 | ;; Initial design of accounts page 228 | 229 | (defn account-dashboard 230 | [request] 231 | (html5 232 | {:lang "en"} 233 | [:head 234 | (include-css "https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css")] 235 | [:body 236 | [:section {:class "hero is-info"} 237 | [:div {:class "hero-body"} 238 | [:div {:class "container"} 239 | [:h1 {:class "title"} "Banking on Clojure"] 240 | [:p {:class "subtitle"} 241 | "Making your money immutable"]]]] 242 | 243 | [:section {:class "section"} 244 | [:div {:class "container"} 245 | [:h1 {:class "title"} 246 | "Account Dashboard"] 247 | [:p {:class "content"} 248 | "Manage your money without unexpected side-effects using a simple made easy banking service"] 249 | 250 | [:div {:class "box"} 251 | [:h1 {:class "title"} 252 | "Current Account"] 253 | [:p {:class "content"} 254 | "Manage your money without unexpected side-effects using a simple made easy banking service"]] 255 | 256 | [:div {:class "box"} 257 | [:h1 {:class "title"} 258 | "Savings Account"] 259 | [:p {:class "content"} 260 | "Manage your money without unexpected side-effects using a simple made easy banking service"]] 261 | 262 | [:div {:class "box"} 263 | [:h1 {:class "title"} 264 | "Tax Free Savings Account"] 265 | [:p {:class "content"} 266 | "Manage your money without unexpected side-effects using a simple made easy banking service"]] 267 | 268 | [:div {:class "box"} 269 | [:h1 {:class "title"} 270 | "Credit Card Account"] 271 | [:p {:class "content"} 272 | "Manage your money without unexpected side-effects using a simple made easy banking service"]] 273 | 274 | [:div {:class "box"} 275 | [:h1 {:class "title"} 276 | "Mortgage"] 277 | [:p {:class "content"} 278 | "Manage your money without unexpected side-effects using a simple made easy banking service"]]]]])) 279 | 280 | ;; Abstracting hiccup with functions 281 | 282 | (defn unordered-list [items] 283 | [:ul 284 | (for [i items] 285 | [:li i])]) 286 | 287 | ;; Many lines of code can now be reduced to a single line 288 | 289 | ;; [:div 290 | ;; (unordered-list ["collection" "of" "list" "items"])] 291 | 292 | (defn bank-account-media-object 293 | [account-details] 294 | [:article {:class "media"} 295 | [:figure {:class "media-left"} 296 | [:p {:class "image is-64x64"} 297 | [:img {:src "https://raw.githubusercontent.com/jr0cket/developer-guides/master/clojure/clojure-bank-coin.png"}]]] 298 | [:div {:class "media-content"} 299 | [:div {:class "content"} 300 | [:h3 {:class "subtitle"} 301 | "Current Account : λ" (:account-value account-details)] 302 | [:p "Account number: " {:account-number account-details} " Sort code: " {:account-sort-code account-details}]]] 303 | [:div {:class "media-right"} 304 | (link-to {:class "button is-primary"} "/transfer" "Transfer") 305 | (link-to {:class "button is-info"} "/payment" "Payment")]]) 306 | 307 | #_(bank-account-media-object 308 | {:account-type "Current Account" 309 | :account-number "123456789" 310 | :account-value "i1,000,000" 311 | :account-sort-code "01-02-01"})) 312 | ;; => [:article {:class "media"} [:figure {:class "media-left"} [:p {:class "image is-64x64"} [:img {:src "https://raw.githubusercontent.com/jr0cket/developer-guides/master/clojure/clojure-bank-coin.png"}]]] [:div {:class "media-content"} [:div {:class "content"} [:h3 {:class "subtitle"} "Current Account : λ" "i1,000,000"] [: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"}}]]] [:div {:class "media-right"} [:a {:href #object[java.net.URI 0x3516357f "/transfer"], :class "button is-primary"} ("Transfer")] [:a {:href #object[java.net.URI 0x4cf62d27 "/payment"], :class "button is-info"} ("Payment")]]] 313 | 314 | ;; End of Rich Comment Block 315 | -------------------------------------------------------------------------------- /src/practicalli/data/design_journal.clj: -------------------------------------------------------------------------------- 1 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 2 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 3 | ;; Design Journal 4 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 5 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 6 | 7 | 8 | (ns practicalli.data.design-journal 9 | (:require [clojure.spec.alpha :as spec] 10 | [clojure.spec.gen.alpha :as spec-gen] 11 | [clojure.spec.test.alpha :as spec-test] 12 | [next.jdbc :as jdbc] 13 | [next.jdbc.sql :as jdbc-sql] 14 | [next.jdbc.specs :as jdbc-spec])) 15 | 16 | ;; Rich comment block with redefined vars ignored 17 | #_{:clj-kondo/ignore [:redefined-var]} 18 | (comment 19 | 20 | ;; Database specification 21 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 22 | 23 | ;; Create a new database to experiment with 24 | (def db-specification-dev {:dbtype "h2" :dbname "banking-redux"}) 25 | 26 | 27 | ;; Schema definition 28 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 29 | 30 | (def schema-account-holders-table 31 | ["CREATE TABLE IF NOT EXISTS PUBLIC.ACCOUNT_HOLDERS( 32 | ID UUID DEFAULT RANDOM_UUID() NOT NULL, 33 | LEGAL_NAME VARCHAR(32) NOT NULL, 34 | EMAIL_ADDRESS VARCHAR(32) NOT NULL, 35 | RESIDENTIAL_ADDRESS VARCHAR(255) NOT NULL, 36 | SOCIAL_SECURITY_NUMBER VARCHAR(32) NOT NULL, 37 | PREFERRED_NAME VARCHAR(32), 38 | CONSTRAINT ACCOUNT_HOLDERS_PK PRIMARY KEY (ID))"]) 39 | 40 | ;; CRUD Helpers 41 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 42 | 43 | (defn create-record 44 | "Insert a single record into the database using a managed connection. 45 | Arguments: 46 | - table - name of database table to be affected 47 | - record-data - Clojure data representing a new record 48 | - db-spec - database specification to establish a connection" 49 | [db-spec table record-data] 50 | (with-open [connection (jdbc/get-connection db-spec)] 51 | (jdbc-sql/insert! 52 | connection 53 | table 54 | record-data 55 | jdbc/snake-kebab-opts))) 56 | 57 | (defn read-record 58 | "Insert a single record into the database using a managed connection. 59 | Arguments: 60 | - table - name of database table to be affected 61 | - record-data - Clojure data representing a new record 62 | - db-spec - database specification to establish a connection" 63 | [db-spec sql-query] 64 | (with-open [connection (jdbc/get-connection db-spec)] 65 | (jdbc-sql/query connection sql-query))) 66 | 67 | 68 | ;; Abstraction 69 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 70 | 71 | (defn register-customer 72 | [customer-details] 73 | (create-record 74 | db-specification-dev 75 | :public.account_holders 76 | customer-details)) 77 | 78 | (defn customer-details 79 | [customer-id] 80 | (read-record 81 | db-specification-dev 82 | ["select * from public.account_holders where id = ?" customer-id]) 83 | ) 84 | 85 | ) ;; End of rich comment block 86 | 87 | (comment 88 | 89 | (register-customer 90 | (spec-gen/generate (spec/gen :customer/unregistered))) 91 | 92 | ;; Register a customer and use the returned uuid to get that customers details 93 | (customer-details 94 | (:account-holders/id (register-customer 95 | (spec-gen/generate (spec/gen :customer/unregistered))))) 96 | ;; => [#:ACCOUNT_HOLDERS{:ID #uuid "a752d1eb-42f4-412f-90e7-f8f185f6761b", :LEGAL_NAME "MV0lFDATSZ7zL7Cl8q0", :EMAIL_ADDRESS "o61HRbJU4MQg2l2veGB2U84nR2", :RESIDENTIAL_ADDRESS "EAnhE1NKSX1L06yjIxj271R2", :SOCIAL_SECURITY_NUMBER "VAt0Jg", :PREFERRED_NAME nil}] 97 | 98 | 99 | #_(new-account-holder 100 | (practicalli.specifications-banking/mock-data-customer-details)) 101 | ;; => #:account-holders{:account-holder-id #uuid "036ecad3-138d-4467-b161-56cbcb9730aa"} 102 | 103 | #_(new-account-holder 104 | #:practicalli.specification-banking{:first_name "Rachel" 105 | :last_name "Requests" 106 | :email_address "rach@requests.org" 107 | :residential_address "1 Emotive Drive, Altar IV" 108 | :social_security_number "AB140123D"}) 109 | ;; => #:account-holders{:account-holder-id #uuid "a7e7c9a3-b007-424f-8702-1c8908a8d8ba"} 110 | ) 111 | 112 | 113 | ;; Rich comment block with redefined vars ignored 114 | #_{:clj-kondo/ignore [:redefined-var]} 115 | (comment 116 | 117 | 118 | ;; Customer detail specifications 119 | 120 | (spec/def :customer/id uuid?) 121 | (spec/def :customer/legal-name string?) 122 | (spec/def :customer/email-address string?) 123 | (spec/def :customer/residential-address string?) 124 | (spec/def :customer/social-security-number string?) 125 | (spec/def :customer/preferred-name string?) 126 | 127 | ;; Data to send to the database 128 | (spec/def :customer/unregistered 129 | (spec/keys 130 | :req [:customer/legal-name 131 | :customer/email-address 132 | :customer/residential-address 133 | :customer/social-security-number] 134 | :opt [:customer/preferred-name])) 135 | 136 | 137 | ;; Data received from the database 138 | (spec/def :customer/registered 139 | (spec/keys 140 | :req [:customer/legal-name 141 | :customer/email-address 142 | :customer/residential-address 143 | :customer/social-security-number] 144 | :opt [:customer/preferred-name])) 145 | 146 | 147 | ;; This design seems so obvious now, but its had me going round in circles trying to optomise for weeks. 148 | 149 | 150 | (spec-gen/generate (spec/gen :customer/unregistered)) 151 | ;; => #:customer{:legal-name "7hl4PT89AO3Pe04958YBWxWH0m6tnG", :email-address "iz83P60EtVM9lMX6zg6", :residential-address "FJ7Mh6nNJviX", :social-security-number "9bYAS85axW42KnOPcPjMtkg06qb4Tr"} 152 | (spec-gen/generate (spec/gen :customer/registered)) 153 | ;; => #:customer{:preferred-name "S8i45tGAgaO60uPVW6q48Emg1", :legal-name "FFsv7pCavtC5V9qD52wO91i9Y", :email-address "6Wl3O11i3L66q800f3JcgkQ7414V0", :residential-address "vzl93YDnD74Zh5", :social-security-number "120J"} 154 | 155 | 156 | ) ;; End of rich comment block 157 | 158 | 159 | 160 | ;; Rich comment Block with redefined functions and other vars ignored 161 | #_{:clj-kondo/ignore [:redefined-var]} 162 | (comment 163 | 164 | ;; Create a new database to experiment with 165 | (def db-specification-dev {:dbtype "h2" :dbname "banking-redux"}) 166 | 167 | 168 | ;; initial design 169 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 170 | 171 | (def schema-account-holders-table 172 | ["CREATE TABLE IF NOT EXISTS PUBLIC.ACCOUNT_HOLDERS( 173 | ACCOUNT_HOLDER_ID UUID DEFAULT RANDOM_UUID() NOT NULL, 174 | FIRST_NAME VARCHAR(32), 175 | LAST_NAME VARCHAR(32), 176 | EMAIL_ADDRESS VARCHAR(32) NOT NULL, 177 | RESIDENTIAL_ADDRESS VARCHAR(255), 178 | SOCIAL_SECURITY_NUMBER VARCHAR(32), 179 | CONSTRAINT ACCOUNT_HOLDERS_PK PRIMARY KEY (ACCOUNT_HOLDER_ID))"]) 180 | 181 | 182 | (def schema-account-holders-table 183 | ["CREATE TABLE IF NOT EXISTS PUBLIC.ACCOUNT_HOLDERS( 184 | ID UUID DEFAULT RANDOM_UUID() NOT NULL, 185 | LEGAL_NAME VARCHAR(32) NOT NULL, 186 | EMAIL_ADDRESS VARCHAR(32) NOT NULL, 187 | RESIDENTIAL_ADDRESS VARCHAR(255) NOT NULL, 188 | SOCIAL_SECURITY_NUMBER VARCHAR(32) NOT NULL, 189 | PREFERRED_NAME VARCHAR(32), 190 | CONSTRAINT ACCOUNT_HOLDERS_PK PRIMARY KEY (ID))"]) 191 | 192 | (defn create-tables! 193 | "Establish a connection to the data source and create all tables within a transaction. 194 | Close the database connection. 195 | Arguments: 196 | - table-schemas: a vector of sql statements, each creating a table 197 | - data-spec: next.jdbc database specification" 198 | [table-schemas data-spec] 199 | 200 | (with-open [connection (jdbc/get-connection data-spec)] 201 | (jdbc/with-transaction [transaction connection] 202 | (doseq [sql-statement table-schemas] 203 | (jdbc/execute! transaction sql-statement) )))) 204 | (defn show-schema 205 | [db-spec table-name] 206 | (with-open [connection (jdbc/get-connection db-spec)] 207 | (jdbc/execute! connection [(str "SHOW COLUMNS FROM " table-name)]))) 208 | (defn drop-table 209 | [db-spec table-name] 210 | (with-open [connection (jdbc/get-connection db-spec)] 211 | (jdbc/execute! connection [(str "DROP TABLE " table-name)]))) 212 | 213 | ;; Create all tables in the development database 214 | (create-tables! [schema-account-holders-table] 215 | db-specification-dev) 216 | 217 | #_(create-tables! 218 | [schema-account-holders-table 219 | schema-accounts-table 220 | schema-transaction-history-table] 221 | db-specification-dev) 222 | 223 | ;; View application table schema in development database 224 | (show-schema db-specification-dev "PUBLIC.ACCOUNT_HOLDERS") 225 | (show-schema db-specification-dev "PUBLIC.ACCOUNTS") 226 | (show-schema db-specification-dev "PUBLIC.TRANSACTION_HISTORY") 227 | 228 | ;; View database system schema in development database 229 | (show-schema db-specification-dev "INFORMATION_SCHEMA.TABLES") 230 | 231 | ;; Remove tables from the development database 232 | (drop-table db-specification-dev "PUBLIC.ACCOUNT_HOLDERS") 233 | (drop-table db-specification-dev "PUBLIC.ACCOUNTS") 234 | (drop-table db-specification-dev "PUBLIC.TRANSACTION_HISTORY") 235 | 236 | 237 | 238 | ;; The table names in the h2 database are relatively simple, 239 | ;; it does not seem that they can be qualified in the same way clojure.spec keywords are 240 | ;; so when returning a qualfied result, the table name is used and not the catalog(?) 241 | ;; The table PUBLIC.ACCOUNT will return a result namespaced :ACCOUNT{,,,} 242 | 243 | 244 | ;; => [#:ACCOUNT_HOLDERS{:ACCOUNT_HOLDER_ID #uuid "f6d6c3ba-c5cc-49de-8c85-21904f8c5b4d", :FIRST_NAME "Rachel", :LAST_NAME "Rocketpack", :EMAIL_ADDRESS "rachel+update@rockketpack.org", :RESIDENTIAL_ADDRESS "1 Ultimate Question Lane, Altar IV", :SOCIAL_SECURITY_NUMBER "BB104312D"}] 245 | 246 | 247 | ;; Specifications 248 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 249 | 250 | (spec/def ::id uuid?) 251 | (spec/def ::legal-name string?) 252 | (spec/def ::email-address string?) 253 | (spec/def ::residential-address string?) 254 | (spec/def ::social-security-number string?) 255 | (spec/def ::preferred-name string?) 256 | 257 | (spec/def ::account-holder 258 | (spec/keys 259 | :req [::id ::legal-name ::email-address ::residential-address ::social-security-number] 260 | :opt [::preferred-name])) 261 | 262 | 263 | (spec-gen/generate (spec/gen ::account-holder)) 264 | 265 | 266 | (spec/def ::customer 267 | (spec/keys 268 | :req [::id ::legal-name ::email-address ::residential-address ::social-security-number] 269 | :opt [::preferred-name])) 270 | 271 | 272 | (spec-gen/generate (spec/gen ::account-holder)) 273 | ;; => #:practicalli.data.relational{:id #uuid "bce879ad-3b1c-4e87-948c-4e559474901d", :legal-name "asGT3s8lJi01z45d9a3lG5V36lh13", :email-address "zoW8L41tIV498f5Pi", :residential-address "w3e0y", :social-security-number "4F34X8"} 274 | 275 | 276 | 277 | (spec/def :customer/details 278 | (spec/keys 279 | :req [::legal-name ::email-address ::residential-address ::social-security-number] 280 | :opt [::preferred-name])) 281 | 282 | 283 | (spec-gen/generate (spec/gen :customer/details)) 284 | ;; => #:practicalli.data.relational{:id #uuid "64561b61-7f33-4700-80d4-de9c8e4c9ced", :legal-name "gx5DbXIoOA8a06u837", :email-address "2M6691M9886U7sI", :residential-address "6c23GWul3Fk1nl1807H42UEHgUqt6", :social-security-number "DOAiLgjYcSHI"} 285 | 286 | 287 | ;; Using autoresolve spec in a test, then define use a specific namespace 288 | ;; or use an alias that is the same (but aliases cannot all be the same) 289 | 290 | 291 | 292 | 293 | 294 | 295 | ) ;; End of Rich comment block 296 | 297 | 298 | ;; Rich comment block with redefined vars ignored 299 | #_{:clj-kondo/ignore [:redefined-var]} 300 | (comment 301 | 302 | ;; Discarded 303 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 304 | 305 | ;; Account holder values 306 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 307 | (spec/def :account-holder/id uuid?) 308 | (spec/def :account-holder/legal-name string?) 309 | (spec/def :account-holder/email-address string?) 310 | (spec/def :account-holder/residential-address string?) 311 | (spec/def :account-holder/social-security-number string?) 312 | (spec/def :account-holder/preferred-name string?) 313 | 314 | (spec/describe :account-holder/id) 315 | 316 | ;; Account holder - composite specification 317 | (spec/def :account-holder/details 318 | (spec/keys 319 | :req [:account-holder/id 320 | :account-holder/legal-name 321 | :account-holder/email-address 322 | :account-holder/residential-address 323 | :account-holder/social-security-number] 324 | :opt [:account-holder/preferred-name])) 325 | 326 | 327 | (spec-gen/generate (spec/gen :account-holder/details)) 328 | 329 | (spec-gen/sample (spec/gen ::account-holder)) 330 | 331 | 332 | ;; refactored specification 333 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 334 | 335 | (spec/def :customer/legal-name string?) 336 | (spec/def :customer/email-address string?) 337 | (spec/def :customer/residential-address string?) 338 | (spec/def :customer/social-security-number string?) 339 | (spec/def :customer/preferred-name string?) 340 | 341 | 342 | ;; Account holder - composite specification 343 | (spec/def :customer/details 344 | (spec/keys 345 | :req [:customer/legal-name 346 | :customer/email-address 347 | :customer/residential-address 348 | :customer/social-security-number] 349 | :opt [:customer/preferred-name])) 350 | 351 | 352 | (spec-gen/generate (spec/gen :customer/details)) 353 | ;; => #:customer{:preferred-name "dx3Aui1X8978XO0D", :legal-name "t68kpUHIYDyT6lHljXQqysPO", :email-address "fcK5rJhcO7m", :residential-address "P", :social-security-number "2dDxJe78QrGt3v4S0Tw"} 354 | ;; => #:customer{:preferred-name "C9i8257Gp05o9a", :legal-name "VTVE7Rd53W2s53", :email-address "i3Muq", :residential-address "X3MQXIkp959q8C62O95swf529Qt5q8", :social-security-number "K4ufLlJh25c"} 355 | 356 | (spec-gen/sample (spec/gen ::customer)) 357 | 358 | 359 | ;; Why not just give them the same name 360 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 361 | 362 | ;; Specific customer details 363 | 364 | (spec/def :customer/id uuid?) 365 | (spec/def :customer/legal-name string?) 366 | (spec/def :customer/email-address string?) 367 | (spec/def :customer/residential-address string?) 368 | (spec/def :customer/social-security-number string?) 369 | (spec/def :customer/preferred-name string?) 370 | 371 | ;; Details to send to the database 372 | (spec/def :customer/unregistered 373 | (spec/keys 374 | :req [:customer/legal-name 375 | :customer/email-address 376 | :customer/residential-address 377 | :customer/social-security-number] 378 | :opt [:customer/preferred-name])) 379 | 380 | 381 | ;; Details recieved from the database 382 | (spec/def :customer/registered 383 | (spec/keys 384 | :req [:customer/legal-name 385 | :customer/email-address 386 | :customer/residential-address 387 | :customer/social-security-number] 388 | :opt [:customer/preferred-name])) 389 | 390 | 391 | ;; This seems so obvious now, but its had me going round in circles trying to optomise for weeks. 392 | 393 | 394 | (spec-gen/generate (spec/gen :customer/unregistered)) 395 | ;; => #:customer{:legal-name "7hl4PT89AO3Pe04958YBWxWH0m6tnG", :email-address "iz83P60EtVM9lMX6zg6", :residential-address "FJ7Mh6nNJviX", :social-security-number "9bYAS85axW42KnOPcPjMtkg06qb4Tr"} 396 | (spec-gen/generate (spec/gen :customer/registered)) 397 | ;; => #:customer{:preferred-name "S8i45tGAgaO60uPVW6q48Emg1", :legal-name "FFsv7pCavtC5V9qD52wO91i9Y", :email-address "6Wl3O11i3L66q800f3JcgkQ7414V0", :residential-address "vzl93YDnD74Zh5", :social-security-number "120J"} 398 | 399 | 400 | ) ;; End of rich comment block 401 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Attribution-ShareAlike 4.0 International 2 | 3 | ======================================================================= 4 | 5 | Creative Commons Corporation ("Creative Commons") is not a law firm and 6 | does not provide legal services or legal advice. Distribution of 7 | Creative Commons public licenses does not create a lawyer-client or 8 | other relationship. Creative Commons makes its licenses and related 9 | information available on an "as-is" basis. Creative Commons gives no 10 | warranties regarding its licenses, any material licensed under their 11 | terms and conditions, or any related information. Creative Commons 12 | disclaims all liability for damages resulting from their use to the 13 | fullest extent possible. 14 | 15 | Using Creative Commons Public Licenses 16 | 17 | Creative Commons public licenses provide a standard set of terms and 18 | conditions that creators and other rights holders may use to share 19 | original works of authorship and other material subject to copyright 20 | and certain other rights specified in the public license below. The 21 | following considerations are for informational purposes only, are not 22 | exhaustive, and do not form part of our licenses. 23 | 24 | Considerations for licensors: Our public licenses are 25 | intended for use by those authorized to give the public 26 | permission to use material in ways otherwise restricted by 27 | copyright and certain other rights. Our licenses are 28 | irrevocable. Licensors should read and understand the terms 29 | and conditions of the license they choose before applying it. 30 | Licensors should also secure all rights necessary before 31 | applying our licenses so that the public can reuse the 32 | material as expected. Licensors should clearly mark any 33 | material not subject to the license. This includes other CC- 34 | licensed material, or material used under an exception or 35 | limitation to copyright. More considerations for licensors: 36 | wiki.creativecommons.org/Considerations_for_licensors 37 | 38 | Considerations for the public: By using one of our public 39 | licenses, a licensor grants the public permission to use the 40 | licensed material under specified terms and conditions. If 41 | the licensor's permission is not necessary for any reason--for 42 | example, because of any applicable exception or limitation to 43 | copyright--then that use is not regulated by the license. Our 44 | licenses grant only permissions under copyright and certain 45 | other rights that a licensor has authority to grant. Use of 46 | the licensed material may still be restricted for other 47 | reasons, including because others have copyright or other 48 | rights in the material. A licensor may make special requests, 49 | such as asking that all changes be marked or described. 50 | Although not required by our licenses, you are encouraged to 51 | respect those requests where reasonable. More_considerations 52 | for the public: 53 | wiki.creativecommons.org/Considerations_for_licensees 54 | 55 | ======================================================================= 56 | 57 | Creative Commons Attribution-ShareAlike 4.0 International Public 58 | License 59 | 60 | By exercising the Licensed Rights (defined below), You accept and agree 61 | to be bound by the terms and conditions of this Creative Commons 62 | Attribution-ShareAlike 4.0 International Public License ("Public 63 | License"). To the extent this Public License may be interpreted as a 64 | contract, You are granted the Licensed Rights in consideration of Your 65 | acceptance of these terms and conditions, and the Licensor grants You 66 | such rights in consideration of benefits the Licensor receives from 67 | making the Licensed Material available under these terms and 68 | conditions. 69 | 70 | 71 | Section 1 -- Definitions. 72 | 73 | a. Adapted Material means material subject to Copyright and Similar 74 | Rights that is derived from or based upon the Licensed Material 75 | and in which the Licensed Material is translated, altered, 76 | arranged, transformed, or otherwise modified in a manner requiring 77 | permission under the Copyright and Similar Rights held by the 78 | Licensor. For purposes of this Public License, where the Licensed 79 | Material is a musical work, performance, or sound recording, 80 | Adapted Material is always produced where the Licensed Material is 81 | synched in timed relation with a moving image. 82 | 83 | b. Adapter's License means the license You apply to Your Copyright 84 | and Similar Rights in Your contributions to Adapted Material in 85 | accordance with the terms and conditions of this Public License. 86 | 87 | c. BY-SA Compatible License means a license listed at 88 | creativecommons.org/compatiblelicenses, approved by Creative 89 | Commons as essentially the equivalent of this Public License. 90 | 91 | d. Copyright and Similar Rights means copyright and/or similar rights 92 | closely related to copyright including, without limitation, 93 | performance, broadcast, sound recording, and Sui Generis Database 94 | Rights, without regard to how the rights are labeled or 95 | categorized. For purposes of this Public License, the rights 96 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 97 | Rights. 98 | 99 | e. Effective Technological Measures means those measures that, in the 100 | absence of proper authority, may not be circumvented under laws 101 | fulfilling obligations under Article 11 of the WIPO Copyright 102 | Treaty adopted on December 20, 1996, and/or similar international 103 | agreements. 104 | 105 | f. Exceptions and Limitations means fair use, fair dealing, and/or 106 | any other exception or limitation to Copyright and Similar Rights 107 | that applies to Your use of the Licensed Material. 108 | 109 | g. License Elements means the license attributes listed in the name 110 | of a Creative Commons Public License. The License Elements of this 111 | Public License are Attribution and ShareAlike. 112 | 113 | h. Licensed Material means the artistic or literary work, database, 114 | or other material to which the Licensor applied this Public 115 | License. 116 | 117 | i. Licensed Rights means the rights granted to You subject to the 118 | terms and conditions of this Public License, which are limited to 119 | all Copyright and Similar Rights that apply to Your use of the 120 | Licensed Material and that the Licensor has authority to license. 121 | 122 | j. Licensor means the individual(s) or entity(ies) granting rights 123 | under this Public License. 124 | 125 | k. Share means to provide material to the public by any means or 126 | process that requires permission under the Licensed Rights, such 127 | as reproduction, public display, public performance, distribution, 128 | dissemination, communication, or importation, and to make material 129 | available to the public including in ways that members of the 130 | public may access the material from a place and at a time 131 | individually chosen by them. 132 | 133 | l. Sui Generis Database Rights means rights other than copyright 134 | resulting from Directive 96/9/EC of the European Parliament and of 135 | the Council of 11 March 1996 on the legal protection of databases, 136 | as amended and/or succeeded, as well as other essentially 137 | equivalent rights anywhere in the world. 138 | 139 | m. You means the individual or entity exercising the Licensed Rights 140 | under this Public License. Your has a corresponding meaning. 141 | 142 | 143 | Section 2 -- Scope. 144 | 145 | a. License grant. 146 | 147 | 1. Subject to the terms and conditions of this Public License, 148 | the Licensor hereby grants You a worldwide, royalty-free, 149 | non-sublicensable, non-exclusive, irrevocable license to 150 | exercise the Licensed Rights in the Licensed Material to: 151 | 152 | a. reproduce and Share the Licensed Material, in whole or 153 | in part; and 154 | 155 | b. produce, reproduce, and Share Adapted Material. 156 | 157 | 2. Exceptions and Limitations. For the avoidance of doubt, where 158 | Exceptions and Limitations apply to Your use, this Public 159 | License does not apply, and You do not need to comply with 160 | its terms and conditions. 161 | 162 | 3. Term. The term of this Public License is specified in Section 163 | 6(a). 164 | 165 | 4. Media and formats; technical modifications allowed. The 166 | Licensor authorizes You to exercise the Licensed Rights in 167 | all media and formats whether now known or hereafter created, 168 | and to make technical modifications necessary to do so. The 169 | Licensor waives and/or agrees not to assert any right or 170 | authority to forbid You from making technical modifications 171 | necessary to exercise the Licensed Rights, including 172 | technical modifications necessary to circumvent Effective 173 | Technological Measures. For purposes of this Public License, 174 | simply making modifications authorized by this Section 2(a) 175 | (4) never produces Adapted Material. 176 | 177 | 5. Downstream recipients. 178 | 179 | a. Offer from the Licensor -- Licensed Material. Every 180 | recipient of the Licensed Material automatically 181 | receives an offer from the Licensor to exercise the 182 | Licensed Rights under the terms and conditions of this 183 | Public License. 184 | 185 | b. Additional offer from the Licensor -- Adapted Material. 186 | Every recipient of Adapted Material from You 187 | automatically receives an offer from the Licensor to 188 | exercise the Licensed Rights in the Adapted Material 189 | under the conditions of the Adapter's License You apply. 190 | 191 | c. No downstream restrictions. You may not offer or impose 192 | any additional or different terms or conditions on, or 193 | apply any Effective Technological Measures to, the 194 | Licensed Material if doing so restricts exercise of the 195 | Licensed Rights by any recipient of the Licensed 196 | Material. 197 | 198 | 6. No endorsement. Nothing in this Public License constitutes or 199 | may be construed as permission to assert or imply that You 200 | are, or that Your use of the Licensed Material is, connected 201 | with, or sponsored, endorsed, or granted official status by, 202 | the Licensor or others designated to receive attribution as 203 | provided in Section 3(a)(1)(A)(i). 204 | 205 | b. Other rights. 206 | 207 | 1. Moral rights, such as the right of integrity, are not 208 | licensed under this Public License, nor are publicity, 209 | privacy, and/or other similar personality rights; however, to 210 | the extent possible, the Licensor waives and/or agrees not to 211 | assert any such rights held by the Licensor to the limited 212 | extent necessary to allow You to exercise the Licensed 213 | Rights, but not otherwise. 214 | 215 | 2. Patent and trademark rights are not licensed under this 216 | Public License. 217 | 218 | 3. To the extent possible, the Licensor waives any right to 219 | collect royalties from You for the exercise of the Licensed 220 | Rights, whether directly or through a collecting society 221 | under any voluntary or waivable statutory or compulsory 222 | licensing scheme. In all other cases the Licensor expressly 223 | reserves any right to collect such royalties. 224 | 225 | 226 | Section 3 -- License Conditions. 227 | 228 | Your exercise of the Licensed Rights is expressly made subject to the 229 | following conditions. 230 | 231 | a. Attribution. 232 | 233 | 1. If You Share the Licensed Material (including in modified 234 | form), You must: 235 | 236 | a. retain the following if it is supplied by the Licensor 237 | with the Licensed Material: 238 | 239 | i. identification of the creator(s) of the Licensed 240 | Material and any others designated to receive 241 | attribution, in any reasonable manner requested by 242 | the Licensor (including by pseudonym if 243 | designated); 244 | 245 | ii. a copyright notice; 246 | 247 | iii. a notice that refers to this Public License; 248 | 249 | iv. a notice that refers to the disclaimer of 250 | warranties; 251 | 252 | v. a URI or hyperlink to the Licensed Material to the 253 | extent reasonably practicable; 254 | 255 | b. indicate if You modified the Licensed Material and 256 | retain an indication of any previous modifications; and 257 | 258 | c. indicate the Licensed Material is licensed under this 259 | Public License, and include the text of, or the URI or 260 | hyperlink to, this Public License. 261 | 262 | 2. You may satisfy the conditions in Section 3(a)(1) in any 263 | reasonable manner based on the medium, means, and context in 264 | which You Share the Licensed Material. For example, it may be 265 | reasonable to satisfy the conditions by providing a URI or 266 | hyperlink to a resource that includes the required 267 | information. 268 | 269 | 3. If requested by the Licensor, You must remove any of the 270 | information required by Section 3(a)(1)(A) to the extent 271 | reasonably practicable. 272 | 273 | b. ShareAlike. 274 | 275 | In addition to the conditions in Section 3(a), if You Share 276 | Adapted Material You produce, the following conditions also apply. 277 | 278 | 1. The Adapter's License You apply must be a Creative Commons 279 | license with the same License Elements, this version or 280 | later, or a BY-SA Compatible License. 281 | 282 | 2. You must include the text of, or the URI or hyperlink to, the 283 | Adapter's License You apply. You may satisfy this condition 284 | in any reasonable manner based on the medium, means, and 285 | context in which You Share Adapted Material. 286 | 287 | 3. You may not offer or impose any additional or different terms 288 | or conditions on, or apply any Effective Technological 289 | Measures to, Adapted Material that restrict exercise of the 290 | rights granted under the Adapter's License You apply. 291 | 292 | 293 | Section 4 -- Sui Generis Database Rights. 294 | 295 | Where the Licensed Rights include Sui Generis Database Rights that 296 | apply to Your use of the Licensed Material: 297 | 298 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 299 | to extract, reuse, reproduce, and Share all or a substantial 300 | portion of the contents of the database; 301 | 302 | b. if You include all or a substantial portion of the database 303 | contents in a database in which You have Sui Generis Database 304 | Rights, then the database in which You have Sui Generis Database 305 | Rights (but not its individual contents) is Adapted Material, 306 | 307 | including for purposes of Section 3(b); and 308 | c. You must comply with the conditions in Section 3(a) if You Share 309 | all or a substantial portion of the contents of the database. 310 | 311 | For the avoidance of doubt, this Section 4 supplements and does not 312 | replace Your obligations under this Public License where the Licensed 313 | Rights include other Copyright and Similar Rights. 314 | 315 | 316 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 317 | 318 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 319 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 320 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 321 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 322 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 323 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 324 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 325 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 326 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 327 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 328 | 329 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 330 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 331 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 332 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 333 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 334 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 335 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 336 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 337 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 338 | 339 | c. The disclaimer of warranties and limitation of liability provided 340 | above shall be interpreted in a manner that, to the extent 341 | possible, most closely approximates an absolute disclaimer and 342 | waiver of all liability. 343 | 344 | 345 | Section 6 -- Term and Termination. 346 | 347 | a. This Public License applies for the term of the Copyright and 348 | Similar Rights licensed here. However, if You fail to comply with 349 | this Public License, then Your rights under this Public License 350 | terminate automatically. 351 | 352 | b. Where Your right to use the Licensed Material has terminated under 353 | Section 6(a), it reinstates: 354 | 355 | 1. automatically as of the date the violation is cured, provided 356 | it is cured within 30 days of Your discovery of the 357 | violation; or 358 | 359 | 2. upon express reinstatement by the Licensor. 360 | 361 | For the avoidance of doubt, this Section 6(b) does not affect any 362 | right the Licensor may have to seek remedies for Your violations 363 | of this Public License. 364 | 365 | c. For the avoidance of doubt, the Licensor may also offer the 366 | Licensed Material under separate terms or conditions or stop 367 | distributing the Licensed Material at any time; however, doing so 368 | will not terminate this Public License. 369 | 370 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 371 | License. 372 | 373 | 374 | Section 7 -- Other Terms and Conditions. 375 | 376 | a. The Licensor shall not be bound by any additional or different 377 | terms or conditions communicated by You unless expressly agreed. 378 | 379 | b. Any arrangements, understandings, or agreements regarding the 380 | Licensed Material not stated herein are separate from and 381 | independent of the terms and conditions of this Public License. 382 | 383 | 384 | Section 8 -- Interpretation. 385 | 386 | a. For the avoidance of doubt, this Public License does not, and 387 | shall not be interpreted to, reduce, limit, restrict, or impose 388 | conditions on any use of the Licensed Material that could lawfully 389 | be made without permission under this Public License. 390 | 391 | b. To the extent possible, if any provision of this Public License is 392 | deemed unenforceable, it shall be automatically reformed to the 393 | minimum extent necessary to make it enforceable. If the provision 394 | cannot be reformed, it shall be severed from this Public License 395 | without affecting the enforceability of the remaining terms and 396 | conditions. 397 | 398 | c. No term or condition of this Public License will be waived and no 399 | failure to comply consented to unless expressly agreed to by the 400 | Licensor. 401 | 402 | d. Nothing in this Public License constitutes or may be interpreted 403 | as a limitation upon, or waiver of, any privileges and immunities 404 | that apply to the Licensor or You, including from the legal 405 | processes of any jurisdiction or authority. 406 | 407 | 408 | ======================================================================= 409 | 410 | Creative Commons is not a party to its public 411 | licenses. Notwithstanding, Creative Commons may elect to apply one of 412 | its public licenses to material it publishes and in those instances 413 | will be considered the “Licensor.” The text of the Creative Commons 414 | public licenses is dedicated to the public domain under the CC0 Public 415 | Domain Dedication. Except for the limited purpose of indicating that 416 | material is shared under a Creative Commons public license or as 417 | otherwise permitted by the Creative Commons policies published at 418 | creativecommons.org/policies, Creative Commons does not authorize the 419 | use of the trademark "Creative Commons" or any other trademark or logo 420 | of Creative Commons without its prior written consent including, 421 | without limitation, in connection with any unauthorized modifications 422 | to any of its public licenses or any other arrangements, 423 | understandings, or agreements concerning use of licensed material. For 424 | the avoidance of doubt, this paragraph does not form part of the 425 | public licenses. 426 | 427 | Creative Commons may be contacted at creativecommons.org. 428 | --------------------------------------------------------------------------------