├── bin └── kaocha ├── test.edn ├── .dir-locals.el ├── .gitignore ├── src └── practicalli │ ├── banking_on_clojure.clj │ ├── specifications.cljc │ └── banking_specifications.clj ├── deps.edn ├── .circleci └── config.yml ├── README.md └── test └── practicalli └── banking_on_clojure_test.clj /bin/kaocha: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## Script to run the kaocha test runner 4 | clojure -A:test:runner-kaocha "$@" 5 | -------------------------------------------------------------------------------- /test.edn: -------------------------------------------------------------------------------- 1 | #kaocha/v1 2 | {:tests [{:type :kaocha.type/spec.test.check 3 | :id :generative-fdef-checks}]} 4 | -------------------------------------------------------------------------------- /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ((clojure-mode . ((cider-preferred-build-tool . "clojure-cli") 2 | (cider-clojure-cli-aliases . ":dev/reloaded")))) 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | *.jar 5 | *.class 6 | /.cpcache 7 | /.lein-* 8 | /.nrepl-history 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | -------------------------------------------------------------------------------- /src/practicalli/banking_on_clojure.clj: -------------------------------------------------------------------------------- 1 | (ns practicalli.banking-on-clojure 2 | (:gen-class)) 3 | 4 | ;; failing function 5 | #_(defn register-account-holder 6 | "Register a new customer with the bank 7 | Arguments: 8 | - hash-map of customer-details 9 | Return: 10 | - hash-map of an account-holder (adds account id)" 11 | 12 | [customer-details] 13 | 14 | customer-details) 15 | 16 | ;; passing function 17 | 18 | (defn register-account-holder 19 | "Register a new customer with the bank 20 | Arguments: 21 | - hash-map of customer-details 22 | Return: 23 | - hash-map of an account-holder (adds account id)" 24 | [customer-details] 25 | 26 | (assoc customer-details 27 | :practicalli.banking-specifications/account-id 28 | (java.util.UUID/randomUUID))) 29 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths 2 | ["src" "resources"] 3 | 4 | :deps 5 | {org.clojure/clojure {:mvn/version "1.10.1"} 6 | 7 | ;; move to a :spec-check alias so its not included in deployment 8 | org.clojure/test.check {:mvn/version "1.0.0"}} 9 | 10 | :aliases 11 | 12 | {:test {:extra-paths ["test"] 13 | :extra-deps {org.clojure/test.check {:mvn/version "1.0.0"}}} 14 | 15 | :runner 16 | {:extra-deps {com.cognitect/test-runner 17 | {:git/url "https://github.com/cognitect-labs/test-runner" 18 | :sha "f7ef16dc3b8332b0d77bc0274578ad5270fbfedd"}} 19 | :main-opts ["-m" "cognitect.test-runner" 20 | "-d" "test"]} 21 | 22 | :runner-kaocha 23 | {:extra-paths ["test"] 24 | :extra-deps {lambdaisland/kaocha {:mvn/version "1.0-612"}} 25 | :main-opts ["-m" "kaocha.runner"]} 26 | 27 | 28 | :uberjar {:extra-deps {seancorfield/depstar {:mvn/version "1.0.94"}} 29 | :main-opts ["-m" "hf.depstar.uberjar" "banking-on-clojure.jar" 30 | "-C" "-m" "practicalli.banking-on-clojure"]}}} 31 | -------------------------------------------------------------------------------- /.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 | 6 | jobs: # basic units of work in a run 7 | build: # runs not using Workflows must have a `build` job as entry point 8 | working_directory: ~/build # directory where steps will run 9 | docker: # run the steps with Docker 10 | - image: circleci/clojure:openjdk-11-tools-deps-1.10.1.536 # image is primary container where `steps` are run 11 | environment: # environment variables for primary container 12 | JVM_OPTS: -Xmx3200m # limit the maximum heap size to prevent out of memory errors 13 | steps: # commands that comprise the `build` job 14 | - checkout # check out source code to working directory 15 | - restore_cache: # restores saved cache if checksum hasn't changed since the last run 16 | key: random-clojure-function-{{ checksum "deps.edn" }} 17 | - run: clojure -R:test:runner -Spath 18 | - save_cache: # generate and store cache in the .m2 directory using a key template 19 | paths: 20 | - ~/.m2 21 | - ~/.gitlibs 22 | key: random-clojure-function-{{ checksum "deps.edn" }} 23 | - run: bin/kaocha --reporter kaocha.report/documentation --no-randomize --no-color --plugin kaocha.plugin.alpha/spec-test-check 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # banking-on-clojure 2 | Developing a Clojure application using a test driven development approach (TDD). Includes the use of `clojure.spec` libraries for defining data and function contracts to be used for generative testing. 3 | 4 | A [guide to the development of this project](http://practicalli.github.io/clojure/clojure-spec/projects/bank-account/) is on the Practicalli Clojure website. 5 | 6 | [![CircleCI](https://circleci.com/gh/circleci/circleci-docs.svg?style=svg)](https://circleci.com/gh/practicalli/banking-on-clojure) 7 | 8 | ## Installation 9 | 10 | Download from https://github.com/practicalli/banking-on-clojure. 11 | 12 | ## Development 13 | Open the code in a Clojure aware editor and start a REPL session. 14 | 15 | ```shell 16 | clojure -M:repl/reloaded 17 | ``` 18 | 19 | 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. 20 | 21 | ```shell 22 | clojure -A:test:runner 23 | ``` 24 | 25 | Or continually run test with the kaocha test runner from the Practicalli Clojure CLI Config 26 | ``` 27 | clojure -X:test/watch 28 | ``` 29 | 30 | 31 | ## Running the code 32 | Use the `-m` option to set the main namespace to inform Clojure where it can find the `-main` function to start the code running. 33 | 34 | ```shell 35 | clojure -m practicalli.banking-on-clojure 36 | ``` 37 | 38 | ## Packaging / Deployment 39 | 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. 40 | 41 | 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. 42 | 43 | ```shell 44 | clojure -A:uberjar 45 | ``` 46 | 47 | The code can be run from the uberjar on the command line 48 | 49 | ```shell 50 | java -jar banking-on-clojure.jar 51 | ``` 52 | 53 | 54 | ## License 55 | 56 | Copyright © 2020 Practicalli 57 | 58 | Distributed under the Creative Commons Attribution Share-Alike 4.0 International 59 | -------------------------------------------------------------------------------- /src/practicalli/specifications.cljc: -------------------------------------------------------------------------------- 1 | (ns practicalli.specifications 2 | (:require [clojure.spec.alpha :as spec])) 3 | 4 | (spec/def ::countries-of-the-world 5 | #{ 6 | "Afghanistan" 7 | "Albania" 8 | "Algeria" 9 | "Andorra" 10 | "Angola" 11 | "Antigua & Deps" 12 | "Argentina" 13 | "Armenia" 14 | "Australia" 15 | "Austria" 16 | "Azerbaijan" 17 | "Bahamas" 18 | "Bahrain" 19 | "Bangladesh" 20 | "Barbados" 21 | "Belarus" 22 | "Belgium" 23 | "Belize" 24 | "Benin" 25 | "Bhutan" 26 | "Bolivia" 27 | "Bosnia Herzegovina" 28 | "Botswana" 29 | "Brazil" 30 | "Brunei" 31 | "Bulgaria" 32 | "Burkina" 33 | "Burundi" 34 | "Cambodia" 35 | "Cameroon" 36 | "Canada" 37 | "Cape Verde" 38 | "Central African Rep" 39 | "Chad" 40 | "Chile" 41 | "China" 42 | "Colombia" 43 | "Comoros" 44 | "Congo" 45 | "Congo {Democratic Rep}" 46 | "Costa Rica" 47 | "Croatia" 48 | "Cuba" 49 | "Cyprus" 50 | "Czech Republic" 51 | "Denmark" 52 | "Djibouti" 53 | "Dominica" 54 | "Dominican Republic" 55 | "East Timor" 56 | "Ecuador" 57 | "Egypt" 58 | "El Salvador" 59 | "Equatorial Guinea" 60 | "Eritrea" 61 | "Estonia" 62 | "Ethiopia" 63 | "Fiji" 64 | "Finland" 65 | "France" 66 | "Gabon" 67 | "Gambia" 68 | "Georgia" 69 | "Germany" 70 | "Ghana" 71 | "Greece" 72 | "Grenada" 73 | "Guatemala" 74 | "Guinea" 75 | "Guinea-Bissau" 76 | "Guyana" 77 | "Haiti" 78 | "Honduras" 79 | "Hungary" 80 | "Iceland" 81 | "India" 82 | "Indonesia" 83 | "Iran" 84 | "Iraq" 85 | "Ireland {Republic}" 86 | "Israel" 87 | "Italy" 88 | "Ivory Coast" 89 | "Jamaica" 90 | "Japan" 91 | "Jordan" 92 | "Kazakhstan" 93 | "Kenya" 94 | "Kiribati" 95 | "Korea North" 96 | "Korea South" 97 | "Kosovo" 98 | "Kuwait" 99 | "Kyrgyzstan" 100 | "Laos" 101 | "Latvia" 102 | "Lebanon" 103 | "Lesotho" 104 | "Liberia" 105 | "Libya" 106 | "Liechtenstein" 107 | "Lithuania" 108 | "Luxembourg" 109 | "Macedonia" 110 | "Madagascar" 111 | "Malawi" 112 | "Malaysia" 113 | "Maldives" 114 | "Mali" 115 | "Malta" 116 | "Marshall Islands" 117 | "Mauritania" 118 | "Mauritius" 119 | "Mexico" 120 | "Micronesia" 121 | "Moldova" 122 | "Monaco" 123 | "Mongolia" 124 | "Montenegro" 125 | "Morocco" 126 | "Mozambique" 127 | "Myanmar, {Burma}" 128 | "Namibia" 129 | "Nauru" 130 | "Nepal" 131 | "Netherlands" 132 | "New Zealand" 133 | "Nicaragua" 134 | "Niger" 135 | "Nigeria" 136 | "Norway" 137 | "Oman" 138 | "Pakistan" 139 | "Palau" 140 | "Panama" 141 | "Papua New Guinea" 142 | "Paraguay" 143 | "Peru" 144 | "Philippines" 145 | "Poland" 146 | "Portugal" 147 | "Qatar" 148 | "Romania" 149 | "Russian Federation" 150 | "Rwanda" 151 | "St Kitts & Nevis" 152 | "St Lucia" 153 | "Saint Vincent & the Grenadines" 154 | "Samoa" 155 | "San Marino" 156 | "Sao Tome & Principe" 157 | "Saudi Arabia" 158 | "Senegal" 159 | "Serbia" 160 | "Seychelles" 161 | "Sierra Leone" 162 | "Singapore" 163 | "Slovakia" 164 | "Slovenia" 165 | "Solomon Islands" 166 | "Somalia" 167 | "South Africa" 168 | "South Sudan" 169 | "Spain" 170 | "Sri Lanka" 171 | "Sudan" 172 | "Suriname" 173 | "Swaziland" 174 | "Sweden" 175 | "Switzerland" 176 | "Syria" 177 | "Taiwan" 178 | "Tajikistan" 179 | "Tanzania" 180 | "Thailand" 181 | "Togo" 182 | "Tonga" 183 | "Trinidad & Tobago" 184 | "Tunisia" 185 | "Turkey" 186 | "Turkmenistan" 187 | "Tuvalu" 188 | "Uganda" 189 | "Ukraine" 190 | "United Arab Emirates" 191 | "United Kingdom" 192 | "United States" 193 | "Uruguay" 194 | "Uzbekistan" 195 | "Vanuatu" 196 | "Vatican City" 197 | "Venezuela" 198 | "Vietnam" 199 | "Yemen" 200 | "Zambia" 201 | "Zimbabwe" 202 | }) 203 | -------------------------------------------------------------------------------- /test/practicalli/banking_on_clojure_test.clj: -------------------------------------------------------------------------------- 1 | (ns practicalli.banking-on-clojure-test 2 | (:require 3 | [clojure.test :refer [deftest is testing]] 4 | 5 | [clojure.spec.alpha :as spec] 6 | [clojure.spec.test.alpha :as spec-test] 7 | [clojure.spec.gen.alpha :as spec-gen] 8 | 9 | [practicalli.banking-on-clojure :as SUT] 10 | [practicalli.banking-specifications])) 11 | 12 | 13 | ;; Generative data 14 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 15 | 16 | ;; requires custom generators as custom predicates are used? 17 | (def customer-mock 18 | (spec-gen/generate (spec/gen :practicalli.banking-specifications/customer-details))) 19 | 20 | 21 | (def account-holder-mock 22 | (spec-gen/generate (spec/gen :practicalli.banking-specifications/account-holder))) 23 | 24 | 25 | (deftest register-account-holder-test 26 | (testing "Basic registration - happy path" 27 | 28 | (is (= (set (keys (SUT/register-account-holder customer-mock))) 29 | (set (keys account-holder-mock)))) 30 | 31 | (is (spec/valid? :practicalli.banking-specifications/account-holder 32 | (SUT/register-account-holder customer-mock))) 33 | 34 | )) ;; End of register-account-holder-test 35 | 36 | 37 | 38 | 39 | ;; REPL experiments 40 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 41 | 42 | 43 | (comment 44 | 45 | (spec-gen/generate (spec/gen :practicalli.banking-specifications/customer-details)) 46 | ;; => #:practicalli.banking-specifications{:first-name "r7q9RFB202v7a69z", :last-name "6N5", :email-address "L6dsud946p680P0pIYZ33CGZd0", :residential-address #:practicalli.banking-specifications{:house-name-number "gCuRMe0C8", :street-name "5", :post-code "VN"}, :social-security-id "a7P0xfBNPv6"} 47 | 48 | 49 | (spec-gen/sample (spec/gen :practicalli.banking-specifications/customer-details)) 50 | 51 | (spec/exercise (spec/cat :practicalli.banking-specifications/first-name :practicalli.banking-specifications/last-name)) 52 | ;; => ([("") #:practicalli.banking-specifications{:first-name ""}] [("6") #:practicalli.banking-specifications{:first-name "6"}] [("") #:practicalli.banking-specifications{:first-name ""}] [("6") #:practicalli.banking-specifications{:first-name "6"}] [("W") #:practicalli.banking-specifications{:first-name "W"}] [("ljooD") #:practicalli.banking-specifications{:first-name "ljooD"}] [("704d5x") #:practicalli.banking-specifications{:first-name "704d5x"}] [("EZyBT") #:practicalli.banking-specifications{:first-name "EZyBT"}] [("1e6") #:practicalli.banking-specifications{:first-name "1e6"}] [("v") #:practicalli.banking-specifications{:first-name "v"}]) 53 | 54 | 55 | (def customer-mock 56 | {:first-name (spec-gen/generate (spec/gen :practicalli.banking-specifications/first-name)) 57 | :last-name (spec-gen/generate (spec/gen :practicalli.banking-specifications/last-name)) 58 | :email-address (spec-gen/generate (spec/gen :practicalli.banking-specifications/email-address)) ;; needs a custom generator 59 | :residential-address (spec-gen/generate (spec/gen :practicalli.banking-specifications/residential-address)) 60 | :postal-code (spec-gen/generate (spec/gen :practicalli.banking-specifications/post-code)) 61 | :social-security-id (spec-gen/generate (spec/gen :practicalli.banking-specifications/social-security-id-uk))}) 62 | 63 | (spec/gen :practicalli.banking-specifications/email-address) 64 | 65 | 66 | ;; original mock data - manually created 67 | 68 | (def customer-mock 69 | {:first-name "Jenny" 70 | :last-name "Jetpack" 71 | :email-address "jenny@jetpack.org" 72 | :residential-address "42 meaning of life street, Earth" 73 | :postal-code "AB3 0EF" 74 | :social-security-id "123456789"}) 75 | ;; => #'practicalli.banking-on-clojure-test/customer-mock 76 | 77 | #_(def account-holder-mock 78 | {:acount-id #uuid "97bda55b-6175-4c39-9e04-7c0205c709dc" 79 | :first-name "Jenny" 80 | :last-name "Jetpack" 81 | :email-address "jenny@jetpack.org" 82 | :residential-address "42 meaning of life street, Earth" 83 | :postal-code "AB3 0EF" 84 | :social-security-id "123456789"}) 85 | 86 | ;; initial clojure.test style unit test 87 | 88 | (deftest register-account-holder-test 89 | (testing "Basic registration - happy path" 90 | (is (= (set (keys (SUT/register-account-holder customer-mock))) 91 | (set (keys account-holder-mock)))) 92 | )) 93 | 94 | 95 | (spec/valid? :practicalli.banking-specifications/account-holder 96 | (spec-gen/generate (spec/gen :practicalli.banking-specifications/account-holder)) ) 97 | 98 | 99 | (spec/valid? :practicalli.banking-specifications/account-holder 100 | (SUT/register-account-holder 101 | (spec-gen/sample (spec/gen :practicalli.banking-specifications/customer-details)))) 102 | 103 | 104 | ) 105 | -------------------------------------------------------------------------------- /src/practicalli/banking_specifications.clj: -------------------------------------------------------------------------------- 1 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 2 | ;; Specifications for banking on clojure application 3 | ;; 4 | ;; Author: practicalli 5 | ;; 6 | ;; Description: 7 | ;; Data and function specifications using clojure.spec.alpha 8 | ;; and instrumentation helper functions 9 | ;; 10 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 11 | 12 | (ns practicalli.banking-specifications 13 | (:require [clojure.spec.alpha :as spec] 14 | [clojure.spec.gen.alpha :as spec-gen] 15 | [clojure.spec.test.alpha :as spec-test] 16 | 17 | [clojure.string] 18 | 19 | [practicalli.specifications] 20 | [practicalli.banking-on-clojure :as SUT])) 21 | 22 | 23 | 24 | ;; Banking data specifications 25 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 26 | 27 | (spec/def ::first-name string?) 28 | (spec/def ::last-name string?) 29 | (spec/def ::email-address string?) 30 | 31 | ;; Requires a custom generator - to be simplified 32 | #_(spec/def ::email-address (spec/and string? #(clojure.string/includes? % "@"))) 33 | #_(spec/def ::email-address 34 | (spec/and string? 35 | #(re-matches #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$" 36 | %))) 37 | 38 | 39 | ;; residential address values 40 | (spec/def ::house-name-number (spec/or :string string? 41 | :number int?)) 42 | (spec/def ::street-name string?) 43 | (spec/def ::post-code string?) 44 | (spec/def ::county string?) 45 | 46 | ;; countries of the world as a set, 47 | ;; containing a string for each country 48 | ;; defined in the practicalli.specifications namespace 49 | (spec/def ::country :practicalli.specifications/countries-of-the-world) 50 | 51 | (spec/def ::residential-address (spec/keys :req [::house-name-number ::street-name ::post-code] 52 | :opt [::county ::country])) 53 | 54 | 55 | 56 | ;; Social security id specifications - not working reliably with built in generators 57 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 58 | #_(defn social-security-number-usa? [value] (= 9 (count value))) 59 | #_(defn social-security-number-uk? [value] (= 11 (count value))) 60 | 61 | ;; Social Security number - one of the regional specifications 62 | #_(spec/def ::social-security-id-usa (spec/and string? social-security-number-usa?)) 63 | 64 | #_(spec/def ::social-security-id-uk (spec/and string? social-security-number-uk?)) 65 | 66 | 67 | (spec/def ::social-security-id-uk string?) 68 | (spec/def ::social-security-id-usa string?) 69 | 70 | (spec/def ::social-security-id (spec/or ::social-security-id-uk 71 | ::social-security-id-usa)) 72 | 73 | ;; composite customer details specification 74 | (spec/def ::customer-details 75 | (spec/keys 76 | :req [::first-name ::last-name ::email-address ::residential-address ::social-security-id])) 77 | 78 | 79 | ;; Account holder values 80 | (spec/def ::account-id uuid?) 81 | 82 | ;; Account holder - composite specification 83 | (spec/def ::account-holder 84 | (spec/keys 85 | :req [::account-id 86 | ::first-name 87 | ::last-name 88 | ::email-address 89 | ::residential-address 90 | ::social-security-id])) 91 | 92 | 93 | ;; Generating data from specifications 94 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 95 | 96 | ;; Test data specifications by generating sample data from those specifications. 97 | ;; Identifies specifications that may require custom generators. 98 | ;; If individual specifications do not generate consistent data, 99 | ;; then incorrect results may occur during function specification checking. 100 | 101 | (comment 102 | 103 | (spec-gen/sample (spec/gen ::first-name)) 104 | (spec-gen/sample (spec/gen ::last-name)) 105 | (spec-gen/sample (spec/gen ::email-address)) 106 | (spec-gen/sample (spec/gen ::house-name-number)) 107 | (spec-gen/sample (spec/gen ::street-name)) 108 | (spec-gen/sample (spec/gen ::post-code)) 109 | (spec-gen/sample (spec/gen ::county)) 110 | (spec-gen/sample (spec/gen ::country)) 111 | (spec-gen/sample (spec/gen ::residential-address)) 112 | (spec-gen/sample (spec/gen ::social-security-id-uk)) 113 | (spec-gen/sample (spec/gen ::social-security-id-usa)) 114 | (spec-gen/sample (spec/gen ::social-security-id)) 115 | (spec-gen/sample (spec/gen ::customer-details)) 116 | (spec-gen/sample (spec/gen ::account-holder)) 117 | 118 | ) 119 | 120 | 121 | 122 | ;; Banking function specifications 123 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 124 | 125 | (spec/fdef SUT/register-account-holder 126 | :args (spec/cat :customer ::customer-details) 127 | :ret ::account-holder) 128 | 129 | 130 | ;; Banking function instrumentation 131 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 132 | 133 | (spec-test/instrument `SUT/register-account-holder) 134 | 135 | 136 | ;; test the instrumentation of the function 137 | ;; use bad data, should return spec error 138 | (comment 139 | 140 | (SUT/register-account-holder {}) 141 | 142 | ;; Use specs to generate test data, should evaluate correctly 143 | (SUT/register-account-holder 144 | (spec-gen/generate 145 | (spec/gen ::customer-details))) 146 | 147 | 148 | (spec-test/unstrument `register-account-holder) 149 | 150 | ) 151 | 152 | ;; spec check 153 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 154 | ;; Generative testing from specifications 155 | ;; Generate 1000 data points from the arguments to a function spec 156 | ;; Use that data to check the evaluation result against the return specification 157 | 158 | (comment 159 | 160 | (spec-test/check `SUT/register-account-holder) 161 | 162 | ) 163 | --------------------------------------------------------------------------------