├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── docs ├── css │ └── default.css ├── fixpoint.core.html ├── fixpoint.datasource.elastic.html ├── fixpoint.datasource.hikari.html ├── fixpoint.datasource.jdbc.html ├── fixpoint.datasource.mysql.html ├── fixpoint.datasource.postgresql.html ├── highlight │ ├── highlight.min.js │ └── solarized-light.css ├── index.html └── js │ ├── jquery.min.js │ └── page_effects.js ├── project.clj ├── resources └── fixpoint │ └── amqp │ └── broker-config.json ├── src └── fixpoint │ ├── core.clj │ └── datasource │ ├── amqp.clj │ ├── elastic.clj │ ├── file_utils.clj │ ├── hikari.clj │ ├── jdbc.clj │ ├── mysql.clj │ └── postgresql.clj └── test └── fixpoint ├── core_test.clj └── datasource ├── amqp_test.clj ├── elastic_test.clj ├── hikari_test.clj ├── mysql_test.clj └── postgresql_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | derby.log 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | sudo: required 3 | lein: lein2 4 | script: lein2 test :all 5 | jdk: 6 | - oraclejdk8 7 | services: 8 | - postgresql 9 | - mysql 10 | - elasticsearch 11 | before_script: 12 | - psql -c 'create database fix_test;' -U postgres 13 | - mysql -e 'create database fix_test;' 14 | env: 15 | global: 16 | - FIXPOINT_POSTGRESQL_URI="jdbc:postgresql://localhost:5432/fix_test?user=postgres&password=" 17 | - FIXPOINT_MYSQL_URI="jdbc:mysql://localhost:3306/fix_test?user=travis&password=&useSSL=false" 18 | - FIXPOINT_ELASTIC_HOST="http://localhost:9200" 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DOC_DIR="./docs" 2 | TMP_DOC_DIR=$(DOC_DIR).tmp 3 | COMMIT?=HEAD 4 | BRANCH?=master 5 | SHORT_COMMIT=$(shell git rev-parse --short $(COMMIT)) 6 | 7 | docs: $(DOC_DIR) 8 | git add $(DOC_DIR) 9 | git commit -m "update documentation ($(SHORT_COMMIT))." 10 | 11 | $(DOC_DIR): $(TMP_DOC_DIR) 12 | git checkout $(BRANCH) 13 | mv $(DOC_DIR).tmp $(DOC_DIR) 14 | 15 | $(TMP_DOC_DIR): 16 | git checkout $(COMMIT) 17 | rm -rf $(DOC_DIR) 18 | lein codox 19 | mv $(DOC_DIR) $(DOC_DIR).tmp 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fixpoint 2 | 3 | __fixpoint__ is a library offering a simple and powerful way of setting up 4 | test datastores and data. 5 | 6 | [![Build Status](https://travis-ci.org/stylefruits/fixpoint.svg?branch=master)](https://travis-ci.org/stylefruits/fixpoint) 7 | [![Clojars Artifact](https://img.shields.io/clojars/v/stylefruits/fixpoint.svg)](https://clojars.org/stylefruits/fixpoint) 8 | 9 | Ready-to-use components for [PostgreSQL][postgres], [MySQL][mysql] and 10 | [ElasticSearch][elastic], as well as an [AMQP Broker][qpid] are already included. 11 | 12 | [postgres]: https://www.postgresql.org/ 13 | [mysql]: https://www.mysql.com/ 14 | [elastic]: https://www.elastic.co/products/elasticsearch 15 | [qpid]: https://qpid.apache.org/ 16 | 17 | ## Usage 18 | 19 | ```clojure 20 | (require '[fixpoint.core :as fix] 21 | '[fixpoint.datasource.postgresql :as pg]) 22 | ``` 23 | 24 | Set up a datasource and give it an ID to be used later, e.g. to set up a 25 | PostgreSQL database and call it `:test-db`: 26 | 27 | 28 | ```clojure 29 | (def test-db 30 | (pg/make-datasource 31 | :test-db 32 | {:connection-uri "jdbc:postgresql://..."})) 33 | ``` 34 | 35 | Set up test fixture functions. Use `fix/as` to specify a name for a specific 36 | fixture document that can be used in other fixture documents to refer to it. 37 | Use `fix/on-datasource` to specify which datasource the fixture should 38 | get inserted in, e.g.: 39 | 40 | ```clojure 41 | (defn- person 42 | [reference name age] 43 | (-> {:db/table :people 44 | :name name 45 | :age age 46 | :active true} 47 | (fix/as reference) 48 | (fix/on-datasource :test-db))) 49 | 50 | (defn- post 51 | [reference person-reference text] 52 | (-> {:db/table :posts 53 | :text text 54 | :author-id person-reference} 55 | (fix/as reference) 56 | (fix/on-datasource :test-db))) 57 | ``` 58 | 59 | Note that you can also return a seq (or nested ones) of fixtures, allowing 60 | you to cover multiple datapoints within one fixture function. Check out 61 | instantiation of those fixtures: 62 | 63 | ```clojure 64 | (def +fixtures+ 65 | [(person :person/me "me" 27) 66 | (person :person/you "you" 29) 67 | (post :post/happy :person/me "Awesome.") 68 | (post :post/meh :person/you "Meh.")]) 69 | ``` 70 | 71 | Note the cross-references between entities, using namespaced keywords. They, 72 | by default, resolve to the `:id` field of the respective inserted data. You can 73 | also do a lookup within, e.g. to create a post with the same author as another: 74 | 75 | ```clojure 76 | (post :post/question [:post/happy :author-id] "Do you really think so?") 77 | ``` 78 | 79 | Of course, so far nothing has happened since we haven't brought our datasource 80 | and fixtures together. Let's start up the datasource, ensuring rollback after 81 | we're done, insert our fixtures and check out the inserted data. 82 | 83 | ```clojure 84 | (fix/with-rollback-datasource [_ test-db] 85 | (fix/with-data +fixtures+ 86 | (vector 87 | (fix/property :person/me) 88 | (fix/property :person/you :created-at) 89 | (fix/property :post/happy :author-id) 90 | (fix/id :post/meh)))) 91 | ;; => [{:id 3, 92 | ;; :name "me", 93 | ;; :age 27, 94 | ;; :active true, 95 | ;; :created-at #inst "2017-03-10T11:13:06.452505000-00:00"}, 96 | ;; #inst "2017-03-10T11:13:06.452505000-00:00" 97 | ;; 3 98 | ;; 4] 99 | ``` 100 | 101 | Cool, eh? 102 | 103 | ## Integration with Clojure Tests 104 | 105 | fixpoint functionality can be used as part of `clojure.test` fixtures. Note that 106 | the datasource fixture has to be applied before the data fixture: 107 | 108 | ```clojure 109 | (require '[clojure.test :refer :all] 110 | '[clojure.java.jdbc :as jdbc]) 111 | 112 | (use-fixtures 113 | :once 114 | (fix/use-datasources db) 115 | (fix/use-data +fixtures+)) 116 | 117 | (deftest t-people-query 118 | (let [db (fix/raw-datasource :test-db)] 119 | (is (= #{"me" "you" "someone"} 120 | (->> (jdbc/query db ["select name from people"]) 121 | (map :name) 122 | (set)))))) 123 | ``` 124 | 125 | This, as you might have noticed, should fail: 126 | 127 | ```clojure 128 | (run-tests) 129 | ;; FAIL in (t-people-query) (b88f62883cbaa1a3f26472a814829fe3c5933107-init.clj:3) 130 | ;; expected: (= #{"someone" "you" "me"} (->> (db/query db ["select name from people"]) (map :name) (set))) 131 | ;; actual: (not (= #{"someone" "you" "me"} #{"you" "me"})) 132 | ;; 133 | ;; Ran 1 tests containing 1 assertions. 134 | ;; 1 failures, 0 errors. 135 | ;; => {:test 1, :pass 0, :fail 1, :error 0, :type :summary} 136 | ``` 137 | 138 | ## License 139 | 140 | Copyright © 2017 stylefruits GmbH 141 | 142 | This project is licensed under the [Apache License 2.0][license]. 143 | 144 | [license]: http://www.apache.org/licenses/LICENSE-2.0.html 145 | -------------------------------------------------------------------------------- /docs/css/default.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=PT+Sans'); 2 | 3 | body { 4 | font-family: 'PT Sans', Helvetica, sans-serif; 5 | font-size: 14px; 6 | } 7 | 8 | a { 9 | color: #337ab7; 10 | text-decoration: none; 11 | } 12 | 13 | a:hover { 14 | color: #30426a; 15 | text-decoration: underline; 16 | } 17 | 18 | pre, code { 19 | font-family: Monaco, DejaVu Sans Mono, Consolas, monospace; 20 | font-size: 9pt; 21 | margin: 15px 0; 22 | } 23 | 24 | h1 { 25 | font-weight: normal; 26 | font-size: 29px; 27 | margin: 10px 0 2px 0; 28 | padding: 0; 29 | } 30 | 31 | h2 { 32 | font-weight: normal; 33 | font-size: 25px; 34 | } 35 | 36 | h3 > a:hover { 37 | text-decoration: none; 38 | } 39 | 40 | .document h1, .namespace-index h1 { 41 | font-size: 32px; 42 | margin-top: 12px; 43 | } 44 | 45 | #header, #content, .sidebar { 46 | position: fixed; 47 | } 48 | 49 | #header { 50 | top: 0; 51 | left: 0; 52 | right: 0; 53 | height: 22px; 54 | color: #f5f5f5; 55 | padding: 5px 7px; 56 | } 57 | 58 | #content { 59 | top: 32px; 60 | right: 0; 61 | bottom: 0; 62 | overflow: auto; 63 | background: #fff; 64 | color: #333; 65 | padding: 0 18px; 66 | } 67 | 68 | .sidebar { 69 | position: fixed; 70 | top: 32px; 71 | bottom: 0; 72 | overflow: auto; 73 | } 74 | 75 | .sidebar.primary { 76 | background: #30426a; 77 | border-right: solid 1px #cccccc; 78 | left: 0; 79 | width: 250px; 80 | color: white; 81 | font-size: 110%; 82 | } 83 | 84 | .sidebar.secondary { 85 | background: #f2f2f2; 86 | border-right: solid 1px #d7d7d7; 87 | left: 251px; 88 | width: 200px; 89 | font-size: 110%; 90 | } 91 | 92 | #content.namespace-index, #content.document { 93 | left: 251px; 94 | } 95 | 96 | #content.namespace-docs { 97 | left: 452px; 98 | } 99 | 100 | #content.document { 101 | padding-bottom: 10%; 102 | } 103 | 104 | #header { 105 | background: #2d3e63; 106 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.4); 107 | z-index: 100; 108 | } 109 | 110 | #header h1 { 111 | margin: 0; 112 | padding: 0; 113 | font-size: 18px; 114 | font-weight: lighter; 115 | text-shadow: -1px -1px 0px #333; 116 | } 117 | 118 | #header h1 .project-version { 119 | font-weight: normal; 120 | } 121 | 122 | .project-version { 123 | padding-left: 0.15em; 124 | } 125 | 126 | #header a, .sidebar a { 127 | display: block; 128 | text-decoration: none; 129 | } 130 | 131 | #header a { 132 | color: #f5f5f5; 133 | } 134 | 135 | .sidebar.primary, .sidebar.primary a { 136 | color: #b2bfdc; 137 | } 138 | 139 | .sidebar.primary a:hover { 140 | color: white; 141 | } 142 | 143 | .sidebar.secondary, .sidebar.secondary a { 144 | color: #738bc0; 145 | } 146 | 147 | .sidebar.secondary a:hover { 148 | color: #2d3e63; 149 | } 150 | 151 | #header h2 { 152 | float: right; 153 | font-size: 9pt; 154 | font-weight: normal; 155 | margin: 4px 3px; 156 | padding: 0; 157 | color: #bbb; 158 | } 159 | 160 | #header h2 a { 161 | display: inline; 162 | } 163 | 164 | .sidebar h3 { 165 | margin: 0; 166 | padding: 10px 13px 0 13px; 167 | font-size: 19px; 168 | font-weight: lighter; 169 | } 170 | 171 | .sidebar.primary h3.no-link { 172 | text-transform: uppercase; 173 | font-size: 12px; 174 | color: #738bc0; 175 | } 176 | 177 | .sidebar.secondary h3 a { 178 | text-transform: uppercase; 179 | font-size: 12px; 180 | color: #2d3e63; 181 | } 182 | 183 | .sidebar ul { 184 | padding: 7px 0 6px 0; 185 | margin: 0; 186 | } 187 | 188 | .sidebar ul.index-link { 189 | padding-bottom: 4px; 190 | } 191 | 192 | .sidebar li { 193 | display: block; 194 | vertical-align: middle; 195 | } 196 | 197 | .sidebar li a, .sidebar li .no-link { 198 | border-left: 3px solid transparent; 199 | padding: 0 10px; 200 | white-space: nowrap; 201 | } 202 | 203 | .sidebar li .inner { 204 | display: inline-block; 205 | padding-top: 7px; 206 | height: 24px; 207 | } 208 | 209 | .sidebar li a, .sidebar li .tree { 210 | height: 31px; 211 | } 212 | 213 | .depth-1 .inner { padding-left: 2px; } 214 | .depth-2 .inner { padding-left: 6px; } 215 | .depth-3 .inner { padding-left: 20px; } 216 | .depth-4 .inner { padding-left: 34px; } 217 | .depth-5 .inner { padding-left: 48px; } 218 | .depth-6 .inner { padding-left: 62px; } 219 | 220 | .sidebar li .tree { 221 | display: block; 222 | float: left; 223 | position: relative; 224 | top: -10px; 225 | margin: 0 4px 0 0; 226 | padding: 0; 227 | } 228 | 229 | .sidebar li.depth-1 .tree { 230 | display: none; 231 | } 232 | 233 | .sidebar li .tree .top, .sidebar li .tree .bottom { 234 | display: block; 235 | margin: 0; 236 | padding: 0; 237 | width: 7px; 238 | } 239 | 240 | .sidebar li .tree .top { 241 | border-left: 1px solid #aaa; 242 | border-bottom: 1px solid #aaa; 243 | height: 19px; 244 | } 245 | 246 | .sidebar li .tree .bottom { 247 | height: 22px; 248 | } 249 | 250 | .sidebar li.branch .tree .bottom { 251 | border-left: 1px solid #aaa; 252 | } 253 | 254 | .sidebar.primary li.current a { 255 | border-left: 3px solid #e99d1a; 256 | color: white; 257 | } 258 | 259 | .sidebar.secondary li.current a { 260 | border-left: 3px solid #2d3e63; 261 | color: #33a; 262 | } 263 | 264 | .namespace-index h2 { 265 | margin: 30px 0 0 0; 266 | } 267 | 268 | .namespace-index h3 { 269 | font-size: 16px; 270 | font-weight: bold; 271 | margin-bottom: 0; 272 | letter-spacing: 0.05em; 273 | border-bottom: solid 1px #ddd; 274 | max-width: 680px; 275 | background-color: #fafafa; 276 | padding: 0.5em; 277 | } 278 | 279 | .namespace-index .topics { 280 | padding-left: 30px; 281 | margin: 11px 0 0 0; 282 | } 283 | 284 | .namespace-index .topics li { 285 | padding: 5px 0; 286 | } 287 | 288 | .namespace-docs h3 { 289 | font-size: 18px; 290 | font-weight: bold; 291 | } 292 | 293 | .public h3 { 294 | margin: 0; 295 | float: left; 296 | } 297 | 298 | .usage { 299 | clear: both; 300 | } 301 | 302 | .public { 303 | margin: 0; 304 | border-top: 1px solid #e0e0e0; 305 | padding-top: 14px; 306 | padding-bottom: 6px; 307 | } 308 | 309 | .public:last-child { 310 | margin-bottom: 20%; 311 | } 312 | 313 | .members .public:last-child { 314 | margin-bottom: 0; 315 | } 316 | 317 | .members { 318 | margin: 15px 0; 319 | } 320 | 321 | .members h4 { 322 | color: #555; 323 | font-weight: normal; 324 | font-variant: small-caps; 325 | margin: 0 0 5px 0; 326 | } 327 | 328 | .members .inner { 329 | padding-top: 5px; 330 | padding-left: 12px; 331 | margin-top: 2px; 332 | margin-left: 7px; 333 | border-left: 1px solid #bbb; 334 | } 335 | 336 | #content .members .inner h3 { 337 | font-size: 12pt; 338 | } 339 | 340 | .members .public { 341 | border-top: none; 342 | margin-top: 0; 343 | padding-top: 6px; 344 | padding-bottom: 0; 345 | } 346 | 347 | .members .public:first-child { 348 | padding-top: 0; 349 | } 350 | 351 | h4.type, 352 | h4.dynamic, 353 | h4.added, 354 | h4.deprecated { 355 | float: left; 356 | margin: 3px 10px 15px 0; 357 | font-size: 15px; 358 | font-weight: bold; 359 | font-variant: small-caps; 360 | } 361 | 362 | .public h4.type, 363 | .public h4.dynamic, 364 | .public h4.added, 365 | .public h4.deprecated { 366 | font-size: 13px; 367 | font-weight: bold; 368 | margin: 3px 0 0 10px; 369 | } 370 | 371 | .members h4.type, 372 | .members h4.added, 373 | .members h4.deprecated { 374 | margin-top: 1px; 375 | } 376 | 377 | h4.type { 378 | color: #717171; 379 | } 380 | 381 | h4.dynamic { 382 | color: #9933aa; 383 | } 384 | 385 | h4.added { 386 | color: #508820; 387 | } 388 | 389 | h4.deprecated { 390 | color: #880000; 391 | } 392 | 393 | .namespace { 394 | margin-bottom: 30px; 395 | } 396 | 397 | .namespace:last-child { 398 | margin-bottom: 10%; 399 | } 400 | 401 | .index { 402 | padding: 0; 403 | font-size: 80%; 404 | margin: 15px 0; 405 | line-height: 1.6em; 406 | } 407 | 408 | .index * { 409 | display: inline; 410 | } 411 | 412 | .index p { 413 | padding-right: 3px; 414 | } 415 | 416 | .index li { 417 | padding-right: 5px; 418 | } 419 | 420 | .index ul { 421 | padding-left: 0; 422 | } 423 | 424 | .type-sig { 425 | clear: both; 426 | color: #088; 427 | } 428 | 429 | .type-sig pre { 430 | padding-top: 10px; 431 | margin: 0; 432 | } 433 | 434 | .usage code { 435 | display: block; 436 | color: #008; 437 | margin: 2px 0; 438 | } 439 | 440 | .usage code:first-child { 441 | padding-top: 10px; 442 | } 443 | 444 | p { 445 | margin: 15px 0; 446 | } 447 | 448 | .public p:first-child, .public pre.plaintext { 449 | margin-top: 12px; 450 | } 451 | 452 | .doc { 453 | margin: 0 0 26px 0; 454 | clear: both; 455 | } 456 | 457 | .public .doc { 458 | margin: 0; 459 | } 460 | 461 | .namespace-index { 462 | font-size: 120%; 463 | } 464 | 465 | .namespace-index .doc { 466 | margin-bottom: 20px; 467 | } 468 | 469 | .namespace-index .namespace .doc { 470 | margin-bottom: 10px; 471 | } 472 | 473 | .markdown p, .markdown li, .markdown dt, .markdown dd, .markdown td { 474 | line-height: 1.6em; 475 | } 476 | 477 | .markdown h2 { 478 | font-weight: normal; 479 | font-size: 25px; 480 | } 481 | 482 | #content .markdown h3 { 483 | font-size: 20px; 484 | } 485 | 486 | .markdown h4 { 487 | font-size: 15px; 488 | } 489 | 490 | .doc, .public, .namespace .index { 491 | max-width: 680px; 492 | overflow-x: visible; 493 | } 494 | 495 | .markdown pre > code { 496 | display: block; 497 | padding: 10px; 498 | } 499 | 500 | .markdown pre > code, .src-link a { 501 | border: 1px solid #e4e4e4; 502 | border-radius: 2px; 503 | } 504 | 505 | .src-link a { 506 | background: #f6f6f6; 507 | } 508 | 509 | .markdown code:not(.hljs) { 510 | color: #c7254e; 511 | background-color: #f9f2f4; 512 | border-radius: 4px; 513 | font-size: 90%; 514 | padding: 2px 4px; 515 | } 516 | 517 | pre.deps { 518 | display: inline-block; 519 | margin: 0 10px; 520 | border: 1px solid #e4e4e4; 521 | border-radius: 2px; 522 | padding: 10px; 523 | background-color: #f6f6f6; 524 | } 525 | 526 | .markdown hr { 527 | border-style: solid; 528 | border-top: none; 529 | color: #ccc; 530 | } 531 | 532 | .doc ul, .doc ol { 533 | padding-left: 30px; 534 | } 535 | 536 | .doc table { 537 | border-collapse: collapse; 538 | margin: 0 10px; 539 | } 540 | 541 | .doc table td, .doc table th { 542 | border: 1px solid #dddddd; 543 | padding: 4px 6px; 544 | } 545 | 546 | .doc table th { 547 | background: #f2f2f2; 548 | } 549 | 550 | .doc dl { 551 | margin: 0 10px 20px 10px; 552 | } 553 | 554 | .doc dl dt { 555 | font-weight: bold; 556 | margin: 0; 557 | padding: 3px 0; 558 | border-bottom: 1px solid #ddd; 559 | } 560 | 561 | .doc dl dd { 562 | padding: 5px 0; 563 | margin: 0 0 5px 10px; 564 | } 565 | 566 | .doc abbr { 567 | border-bottom: 1px dotted #333; 568 | font-variant: none; 569 | cursor: help; 570 | } 571 | 572 | .src-link { 573 | margin-bottom: 15px; 574 | } 575 | 576 | .src-link a { 577 | font-size: 70%; 578 | padding: 1px 4px; 579 | text-decoration: none; 580 | color: #5555bb; 581 | background-color: #f6f6f6; 582 | } 583 | 584 | blockquote { 585 | opacity: 0.6; 586 | border-left: solid 2px #ddd; 587 | margin-left: 0; 588 | padding-left: 1em; 589 | } 590 | 591 | /* Responsivene Theme */ 592 | 593 | @media (max-width: 860px) { 594 | .sidebar { 595 | display:none; 596 | } 597 | 598 | #content { 599 | position: relative; 600 | left: initial !important; 601 | top: 66px; 602 | padding: 0 3px; 603 | } 604 | 605 | #header { 606 | display: flex; 607 | flex-direction: column-reverse; 608 | height: 58px; 609 | } 610 | 611 | #header > h1 { 612 | font-size: 32px; 613 | } 614 | 615 | .namespace-index > h1 { 616 | display: none; 617 | } 618 | 619 | #header h2 { 620 | float: none; 621 | } 622 | } 623 | -------------------------------------------------------------------------------- /docs/fixpoint.core.html: -------------------------------------------------------------------------------- 1 | 3 | fixpoint.core documentation

fixpoint.core

Fixture Functions and Protocols

as

(as data document-id)

Declare the given fixture’s reference ID, which can be used from within other fixtures.

4 |
(defn person
 5 |   [reference name]
 6 |   (-> {:db/table :people
 7 |        :name     name}
 8 |       (as reference)
 9 |       (on-datasource :db)))
10 | 
11 | (defn post
12 |   [reference author-reference text]
13 |   (-> {:db/table :posts
14 |        :author-id author-reference
15 |        :text      text}
16 |       (as reference)
17 |       (on-datasource :db)))
18 | 
19 |

A simple set of fixtures could be:

20 |
[(person :person/me "me")
21 |  (post   :post/happy :person/me "yay!")]
22 | 
23 |

You can also reference specific fields of other documents using a vector notation, e.g. to declare a post with the same author as :post/happy:

24 |
(post :post/again [:post/happy :author-id] "still yay!")
25 | 
26 |

Note that every reference ID can be used exactly once.

by-namespace

(by-namespace nspace & path)

Retrieve a property for each entity whose reference (attached using as) has the given namespace.

27 |
(with-datasource [ds (pg/make-datasource :db ...)]
28 |   (with-data [(person :person/me  "me")
29 |               (person :person/you "you")
30 |               (post   :post/happy :person/me "yay!")]
31 |     (by-namespace :person :id)))
32 | ;; => {:person/me 1, :person/you 2}
33 | 
34 |

Returns a map associating the reference (see as) with the queried property.

Datasource

protocol

Protocol for fixture-capable test datasources.

members

as-raw-datasource

(as-raw-datasource this)

Retrieve the underlying raw datasource, e.g. a clojure.java.jdbc database spec, or some raw connection object.

35 |

This should fail on non-started datasources.

datasource-id

(datasource-id this)

Return an ID for this datasource.

insert-document!

(insert-document! this document)

Insert the given data into the current datasource. Return a map of :id (the reference ID), :data (the actual data inserted) and optionally :tags (a seq of tags for entity filtering).

36 |

Reference IDs must be namespaced keywords.

run-with-rollback

(run-with-rollback this f)

Run (f datasource), where datasource is a transacted version of the current datasource. Ensure that changes made via datasource are rolled back afterwards.

start-datasource

(start-datasource this)

Start the datasource.

stop-datasource

(stop-datasource this)

Stop the datasource.

datasource

(datasource id)

Get the Datasource registered under the given ID within the current scope. Throws an AssertionError if there is none.

Fixture

protocol

Protocol for datasource fixtures.

members

fixture-documents

(fixture-documents this)

Generate a seq of fixture document maps, each one belonging to a single datasource identified by the :fixpoint/datasource key.

id

(id document-id)

Retrieve the :id property for the given document.

ids

(ids document-ids)

Retrieve the :id property for each of the given documents.

match

(match tags & transformations)

Look up a fixture document’s property for every entity that matches all of the given tags.

maybe-datasource

(maybe-datasource id)

Get the Datasource registered with the given ID within the current scope, or nil if there is none.

on-datasource

(on-datasource data datasource-id)

Declare the given fixture’s target datasource, corresponding to one to-be-instantiated within with-datasource or with-rollback-datasource.

37 |
(defn person
38 |   [reference name]
39 |   (-> {:db/table :people
40 |        :name     name}
41 |       (as reference)
42 |       (on-datasource :db)))
43 | 
44 | (with-datasource [ds (pg/make-datasource :db ...)]
45 |   (with-data [(person :person/me  "me") ...]
46 |     ...))
47 | 
48 |

The result can be passed to with-data to be run against the actual datasource.

properties

(properties document-ids & transformations)

See property. Performs a lookup in multiple fixture documents, returning values in an order corresponding to document-ids.

property

(property document-id & transformations)

Look up a single fixture document’s property using a reference attached to a fixture using as.

49 |
(defn person
50 |   [reference name]
51 |   (-> {:db/table :people
52 |        :name     name}
53 |       (as reference)
54 |       (on-datasource :db)))
55 | 
56 | (with-datasource [ds (pg/make-datasource :db ...)]
57 |   (with-data [(person :person/me  "me")
58 |               (person :person/you "you")]
59 |     (println (property :person/me))
60 |     (println (property :person/you :id))))
61 | 
62 |

transformations can be given to apply a sequence of functions, in order, to the fixture map. Has to be used within a with-data block.

raw-datasource

(raw-datasource id)

Get the raw datasource value for the Datasource registered under the given ID within the current scope.

63 |

See datasource and as-raw-datasource.

use-data

(use-data & fixtures)

A clojure.test fixture that will insert the given fixtures into their respective datasources.

64 |

Needs to be applied after use-datasources.

use-datasources

(use-datasources & datasources)

A clojure.test fixture that will wrap test runs with startup/shutdown of the given datasources. After the tests have run, a rollback will be initiated to cleanup the database.

with-data

macro

(with-data fixtures & body)

Given a Fixture (or a seq of them), run them against their respective datasources, then execute body.

65 |
(defn person
66 |   [name]
67 |   (-> {:db/table :people
68 |        :name     name}
69 |       (on-datasource :db)))
70 | 
71 | (with-datasource [ds (pg/make-datasource :db ...)]
72 |   (with-data [(person "me") (person "you")]
73 |     ...))
74 | 
75 |

This has to be wrapped by with-datasource or with-rollback-datasource since otherwise there is nothing to insert into.

with-datasource

macro

(with-datasource [sym datasource] & body)

Start datasource and bind it to sym, then run body in its scope.

76 |
(with-datasource [ds (pg/make-datasource ...)]
77 |   ...)
78 | 

with-rollback

macro

(with-rollback [sym datasource] & body)

Run the given body within a ‘transacted’ version of the given datasource, rolling back after the run has finished.

79 |
(with-datasource [ds (pg/make-datasource ...)]
80 |   (with-rollback [tx ds]
81 |     (let [db (as-jdbc-datasource tx)]
82 |       (jdbc/execute! db ["INSERT INTO ..." ...]))))
83 | 

with-rollback-datasource

macro

(with-rollback-datasource [sym datasource] & body)

Start a ‘transacted’ version of datasource, rolling back any changed made after the run has finished.

84 |
(with-rollback-datasource [ds (pg/make-datasource ...)]
85 |   (let [db (as-jdbc-datasource ds)]
86 |     (jdbc/execute! db ["INSERT INTO ..." ...])))
87 | 
88 |

This is a convenience function combining with-datasource and with-rollback.

-------------------------------------------------------------------------------- /docs/fixpoint.datasource.elastic.html: -------------------------------------------------------------------------------- 1 | 3 | fixpoint.datasource.elastic documentation

fixpoint.datasource.elastic

ElasticSearch Datasource Component

4 |

Make sure the cc.qbits/spandec dependency is available on your classpath.

index

(index datasource-id index-key)

Retrieve the actual name of the index that was declared using index-key as :elastic/index.

5 |
(with-datasource [es (elastic/make-datasource :elastic ...)]
 6 |   (with-data [{:elastic/index :people, :elastic/mapping ...}]
 7 |     (index :elastic :people)))
 8 | ;; => "people-ec0796d7-c1b6-49de-a2d8-e60f262b608d"
 9 | 

make-datasource

(make-datasource id hosts)

Create an ElasticSearch datasource. Rollback capability is achieved by only allowing index declaration through the datasource, tracking and deleting any created ones.

10 |

An index creation document has to contain both the :elastic/index and the :elastic/mapping key:

11 |
{:elastic/index   :people
12 |  :elastic/mapping {:person
13 |                    {:properties
14 |                     {:name {:type :string}
15 |                      :age  {:type :long}}}}}
16 | 
17 |

The index name will be randomly generated and can be accessed by passing the value given in :elastic/index to index.

18 |

If :elastic/mapping is set to false, the index will not be created, but a name will be reserved and, on rollback, cleanup initiated.

19 |

Actual documents have to reference their respective :elastic/index and specify an :elastic/type pointing at the mapping they should conform to. Optionally, an explicit ID can be set using :elastic/id.

20 |
{:elastic/index :people
21 |  :elastic/type  :person
22 |  :name          "Me"
23 |  :age           27}
24 | 
-------------------------------------------------------------------------------- /docs/fixpoint.datasource.hikari.html: -------------------------------------------------------------------------------- 1 | 3 | fixpoint.datasource.hikari documentation

fixpoint.datasource.hikari

HikariCP wrapper for any JDBCDatasource.

wrap-jdbc-datasource

(wrap-jdbc-datasource jdbc-datasource & [pool-options])

Wrap the given JDBCDatasource to use a Hikari connection pool.

-------------------------------------------------------------------------------- /docs/fixpoint.datasource.jdbc.html: -------------------------------------------------------------------------------- 1 | 3 | fixpoint.datasource.jdbc documentation

fixpoint.datasource.jdbc

Generic JDBC datasource component.

JDBCDatasource

protocol

Protocol for JDBC datasources.

members

get-db-spec

(get-db-spec this)

Retrieve the JDBC datasource’s database spec.

set-db-spec

(set-db-spec this new-db-spec)

Set the JDBC datasource’s database spec.

make-datasource

(make-datasource id db-spec & [overrides])

Create a JDBC datasource with the given id. overrides can contain:

4 |
    5 |
  • :pre-fn: to be applied to each document before insertion,
  • 6 |
  • :post-fn: to be applied to the db-spec and each insertion result, transforming the data returned by a fixture.
  • 7 |
8 |

Both can be useful for JDBC drivers that need special handling, e.g. MySQL which does not return inserted data, but a map with :generated_key only.

9 |

Documents passed to this datasource need to contain the :db/table key pointing at the database table they should be inserted into. Every other key within the document will be interpreted as a database column and its value.

-------------------------------------------------------------------------------- /docs/fixpoint.datasource.mysql.html: -------------------------------------------------------------------------------- 1 | 3 | fixpoint.datasource.mysql documentation

fixpoint.datasource.mysql

MySQL JDBC Datasource Component

4 |

Make sure the mysql/mysql-connector-java dependency is available on your classpath.

make-datasource

(make-datasource id db-spec)

Create a MySQL JDBC datasource. Documents passed to this datasource need to have the format described in fixpoint.datasource.jdbc/make-datasource.

5 |
{:db/table :people
 6 |  :name     "me"
 7 |  :age      28}
 8 | 
9 |

If the respective table’s primary key column is not :id, it has to additionally be specified using a :db/primary-key value.

10 |
{:db/table       :people
11 |  :db/primary-key :uuid
12 |  :name           "me"
13 |  :age            28}
14 | 
15 |

(The reason for this is that, for MySQL, we need to perform a SELECT statement after insertion, since all we get back is the :generated_key.)

-------------------------------------------------------------------------------- /docs/fixpoint.datasource.postgresql.html: -------------------------------------------------------------------------------- 1 | 3 | fixpoint.datasource.postgresql documentation

fixpoint.datasource.postgresql

PostgreSQL Datasource Component

4 |

Make sure the org.postgresql/postgresql dependency is available on your classpath.

make-datasource

(make-datasource id db-spec)

Create a PostgreSQL JDBC datasource. Documents passed to this datasource need to have the format described in fixpoint.datasource.jdbc/make-datasource.

5 |
{:db/table :people
6 |  :name     "me"
7 |  :age      28}
8 | 
-------------------------------------------------------------------------------- /docs/highlight/highlight.min.js: -------------------------------------------------------------------------------- 1 | /*! highlight.js v9.6.0 | BSD3 License | git.io/hljslicense */ 2 | !function(e){var n="object"==typeof window&&window||"object"==typeof self&&self;"undefined"!=typeof exports?e(exports):n&&(n.hljs=e({}),"function"==typeof define&&define.amd&&define([],function(){return n.hljs}))}(function(e){function n(e){return e.replace(/[&<>]/gm,function(e){return I[e]})}function t(e){return e.nodeName.toLowerCase()}function r(e,n){var t=e&&e.exec(n);return t&&0===t.index}function a(e){return k.test(e)}function i(e){var n,t,r,i,o=e.className+" ";if(o+=e.parentNode?e.parentNode.className:"",t=B.exec(o))return R(t[1])?t[1]:"no-highlight";for(o=o.split(/\s+/),n=0,r=o.length;r>n;n++)if(i=o[n],a(i)||R(i))return i}function o(e,n){var t,r={};for(t in e)r[t]=e[t];if(n)for(t in n)r[t]=n[t];return r}function u(e){var n=[];return function r(e,a){for(var i=e.firstChild;i;i=i.nextSibling)3===i.nodeType?a+=i.nodeValue.length:1===i.nodeType&&(n.push({event:"start",offset:a,node:i}),a=r(i,a),t(i).match(/br|hr|img|input/)||n.push({event:"stop",offset:a,node:i}));return a}(e,0),n}function c(e,r,a){function i(){return e.length&&r.length?e[0].offset!==r[0].offset?e[0].offset"}function u(e){l+=""}function c(e){("start"===e.event?o:u)(e.node)}for(var s=0,l="",f=[];e.length||r.length;){var g=i();if(l+=n(a.substr(s,g[0].offset-s)),s=g[0].offset,g===e){f.reverse().forEach(u);do c(g.splice(0,1)[0]),g=i();while(g===e&&g.length&&g[0].offset===s);f.reverse().forEach(o)}else"start"===g[0].event?f.push(g[0].node):f.pop(),c(g.splice(0,1)[0])}return l+n(a.substr(s))}function s(e){function n(e){return e&&e.source||e}function t(t,r){return new RegExp(n(t),"m"+(e.cI?"i":"")+(r?"g":""))}function r(a,i){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var u={},c=function(n,t){e.cI&&(t=t.toLowerCase()),t.split(" ").forEach(function(e){var t=e.split("|");u[t[0]]=[n,t[1]?Number(t[1]):1]})};"string"==typeof a.k?c("keyword",a.k):E(a.k).forEach(function(e){c(e,a.k[e])}),a.k=u}a.lR=t(a.l||/\w+/,!0),i&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=t(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=t(a.e)),a.tE=n(a.e)||"",a.eW&&i.tE&&(a.tE+=(a.e?"|":"")+i.tE)),a.i&&(a.iR=t(a.i)),null==a.r&&(a.r=1),a.c||(a.c=[]);var s=[];a.c.forEach(function(e){e.v?e.v.forEach(function(n){s.push(o(e,n))}):s.push("self"===e?a:e)}),a.c=s,a.c.forEach(function(e){r(e,a)}),a.starts&&r(a.starts,i);var l=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(n).filter(Boolean);a.t=l.length?t(l.join("|"),!0):{exec:function(){return null}}}}r(e)}function l(e,t,a,i){function o(e,n){var t,a;for(t=0,a=n.c.length;a>t;t++)if(r(n.c[t].bR,e))return n.c[t]}function u(e,n){if(r(e.eR,n)){for(;e.endsParent&&e.parent;)e=e.parent;return e}return e.eW?u(e.parent,n):void 0}function c(e,n){return!a&&r(n.iR,e)}function g(e,n){var t=N.cI?n[0].toLowerCase():n[0];return e.k.hasOwnProperty(t)&&e.k[t]}function h(e,n,t,r){var a=r?"":y.classPrefix,i='',i+n+o}function p(){var e,t,r,a;if(!E.k)return n(B);for(a="",t=0,E.lR.lastIndex=0,r=E.lR.exec(B);r;)a+=n(B.substr(t,r.index-t)),e=g(E,r),e?(M+=e[1],a+=h(e[0],n(r[0]))):a+=n(r[0]),t=E.lR.lastIndex,r=E.lR.exec(B);return a+n(B.substr(t))}function d(){var e="string"==typeof E.sL;if(e&&!x[E.sL])return n(B);var t=e?l(E.sL,B,!0,L[E.sL]):f(B,E.sL.length?E.sL:void 0);return E.r>0&&(M+=t.r),e&&(L[E.sL]=t.top),h(t.language,t.value,!1,!0)}function b(){k+=null!=E.sL?d():p(),B=""}function v(e){k+=e.cN?h(e.cN,"",!0):"",E=Object.create(e,{parent:{value:E}})}function m(e,n){if(B+=e,null==n)return b(),0;var t=o(n,E);if(t)return t.skip?B+=n:(t.eB&&(B+=n),b(),t.rB||t.eB||(B=n)),v(t,n),t.rB?0:n.length;var r=u(E,n);if(r){var a=E;a.skip?B+=n:(a.rE||a.eE||(B+=n),b(),a.eE&&(B=n));do E.cN&&(k+=C),E.skip||(M+=E.r),E=E.parent;while(E!==r.parent);return r.starts&&v(r.starts,""),a.rE?0:n.length}if(c(n,E))throw new Error('Illegal lexeme "'+n+'" for mode "'+(E.cN||"")+'"');return B+=n,n.length||1}var N=R(e);if(!N)throw new Error('Unknown language: "'+e+'"');s(N);var w,E=i||N,L={},k="";for(w=E;w!==N;w=w.parent)w.cN&&(k=h(w.cN,"",!0)+k);var B="",M=0;try{for(var I,j,O=0;;){if(E.t.lastIndex=O,I=E.t.exec(t),!I)break;j=m(t.substr(O,I.index-O),I[0]),O=I.index+j}for(m(t.substr(O)),w=E;w.parent;w=w.parent)w.cN&&(k+=C);return{r:M,value:k,language:e,top:E}}catch(T){if(T.message&&-1!==T.message.indexOf("Illegal"))return{r:0,value:n(t)};throw T}}function f(e,t){t=t||y.languages||E(x);var r={r:0,value:n(e)},a=r;return t.filter(R).forEach(function(n){var t=l(n,e,!1);t.language=n,t.r>a.r&&(a=t),t.r>r.r&&(a=r,r=t)}),a.language&&(r.second_best=a),r}function g(e){return y.tabReplace||y.useBR?e.replace(M,function(e,n){return y.useBR&&"\n"===e?"
":y.tabReplace?n.replace(/\t/g,y.tabReplace):void 0}):e}function h(e,n,t){var r=n?L[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),-1===e.indexOf(r)&&a.push(r),a.join(" ").trim()}function p(e){var n,t,r,o,s,p=i(e);a(p)||(y.useBR?(n=document.createElementNS("http://www.w3.org/1999/xhtml","div"),n.innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n")):n=e,s=n.textContent,r=p?l(p,s,!0):f(s),t=u(n),t.length&&(o=document.createElementNS("http://www.w3.org/1999/xhtml","div"),o.innerHTML=r.value,r.value=c(t,u(o),s)),r.value=g(r.value),e.innerHTML=r.value,e.className=h(e.className,p,r.language),e.result={language:r.language,re:r.r},r.second_best&&(e.second_best={language:r.second_best.language,re:r.second_best.r}))}function d(e){y=o(y,e)}function b(){if(!b.called){b.called=!0;var e=document.querySelectorAll("pre code");w.forEach.call(e,p)}}function v(){addEventListener("DOMContentLoaded",b,!1),addEventListener("load",b,!1)}function m(n,t){var r=x[n]=t(e);r.aliases&&r.aliases.forEach(function(e){L[e]=n})}function N(){return E(x)}function R(e){return e=(e||"").toLowerCase(),x[e]||x[L[e]]}var w=[],E=Object.keys,x={},L={},k=/^(no-?highlight|plain|text)$/i,B=/\blang(?:uage)?-([\w-]+)\b/i,M=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,C="
",y={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0},I={"&":"&","<":"<",">":">"};return e.highlight=l,e.highlightAuto=f,e.fixMarkup=g,e.highlightBlock=p,e.configure=d,e.initHighlighting=b,e.initHighlightingOnLoad=v,e.registerLanguage=m,e.listLanguages=N,e.getLanguage=R,e.inherit=o,e.IR="[a-zA-Z]\\w*",e.UIR="[a-zA-Z_]\\w*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|like)\b/},e.C=function(n,t,r){var a=e.inherit({cN:"comment",b:n,e:t,c:[]},r||{});return a.c.push(e.PWM),a.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),a},e.CLCM=e.C("//","$"),e.CBCM=e.C("/\\*","\\*/"),e.HCM=e.C("#","$"),e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e.METHOD_GUARD={b:"\\.\\s*"+e.UIR,r:0},e});hljs.registerLanguage("clojure",function(e){var t={"builtin-name":"def defonce cond apply if-not if-let if not not= = < > <= >= == + / * - rem quot neg? pos? delay? symbol? keyword? true? false? integer? empty? coll? list? set? ifn? fn? associative? sequential? sorted? counted? reversible? number? decimal? class? distinct? isa? float? rational? reduced? ratio? odd? even? char? seq? vector? string? map? nil? contains? zero? instance? not-every? not-any? libspec? -> ->> .. . inc compare do dotimes mapcat take remove take-while drop letfn drop-last take-last drop-while while intern condp case reduced cycle split-at split-with repeat replicate iterate range merge zipmap declare line-seq sort comparator sort-by dorun doall nthnext nthrest partition eval doseq await await-for let agent atom send send-off release-pending-sends add-watch mapv filterv remove-watch agent-error restart-agent set-error-handler error-handler set-error-mode! error-mode shutdown-agents quote var fn loop recur throw try monitor-enter monitor-exit defmacro defn defn- macroexpand macroexpand-1 for dosync and or when when-not when-let comp juxt partial sequence memoize constantly complement identity assert peek pop doto proxy defstruct first rest cons defprotocol cast coll deftype defrecord last butlast sigs reify second ffirst fnext nfirst nnext defmulti defmethod meta with-meta ns in-ns create-ns import refer keys select-keys vals key val rseq name namespace promise into transient persistent! conj! assoc! dissoc! pop! disj! use class type num float double short byte boolean bigint biginteger bigdec print-method print-dup throw-if printf format load compile get-in update-in pr pr-on newline flush read slurp read-line subvec with-open memfn time re-find re-groups rand-int rand mod locking assert-valid-fdecl alias resolve ref deref refset swap! reset! set-validator! compare-and-set! alter-meta! reset-meta! commute get-validator alter ref-set ref-history-count ref-min-history ref-max-history ensure sync io! new next conj set! to-array future future-call into-array aset gen-class reduce map filter find empty hash-map hash-set sorted-map sorted-map-by sorted-set sorted-set-by vec vector seq flatten reverse assoc dissoc list disj get union difference intersection extend extend-type extend-protocol int nth delay count concat chunk chunk-buffer chunk-append chunk-first chunk-rest max min dec unchecked-inc-int unchecked-inc unchecked-dec-inc unchecked-dec unchecked-negate unchecked-add-int unchecked-add unchecked-subtract-int unchecked-subtract chunk-next chunk-cons chunked-seq? prn vary-meta lazy-seq spread list* str find-keyword keyword symbol gensym force rationalize"},r="a-zA-Z_\\-!.?+*=<>&#'",n="["+r+"]["+r+"0-9/;:]*",a="[-+]?\\d+(\\.\\d+)?",o={b:n,r:0},s={cN:"number",b:a,r:0},i=e.inherit(e.QSM,{i:null}),c=e.C(";","$",{r:0}),d={cN:"literal",b:/\b(true|false|nil)\b/},l={b:"[\\[\\{]",e:"[\\]\\}]"},m={cN:"comment",b:"\\^"+n},p=e.C("\\^\\{","\\}"),u={cN:"symbol",b:"[:]{1,2}"+n},f={b:"\\(",e:"\\)"},h={eW:!0,r:0},y={k:t,l:n,cN:"name",b:n,starts:h},b=[f,i,m,p,c,u,l,s,d,o];return f.c=[e.C("comment",""),y,h],h.c=b,l.c=b,{aliases:["clj"],i:/\S/,c:[f,i,m,p,c,u,l,s,d]}});hljs.registerLanguage("clojure-repl",function(e){return{c:[{cN:"meta",b:/^([\w.-]+|\s*#_)=>/,starts:{e:/$/,sL:"clojure"}}]}}); -------------------------------------------------------------------------------- /docs/highlight/solarized-light.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Orginal Style from ethanschoonover.com/solarized (c) Jeremy Hull 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | background: #fdf6e3; 12 | color: #657b83; 13 | } 14 | 15 | .hljs-comment, 16 | .hljs-quote { 17 | color: #93a1a1; 18 | } 19 | 20 | /* Solarized Green */ 21 | .hljs-keyword, 22 | .hljs-selector-tag, 23 | .hljs-addition { 24 | color: #859900; 25 | } 26 | 27 | /* Solarized Cyan */ 28 | .hljs-number, 29 | .hljs-string, 30 | .hljs-meta .hljs-meta-string, 31 | .hljs-literal, 32 | .hljs-doctag, 33 | .hljs-regexp { 34 | color: #2aa198; 35 | } 36 | 37 | /* Solarized Blue */ 38 | .hljs-title, 39 | .hljs-section, 40 | .hljs-name, 41 | .hljs-selector-id, 42 | .hljs-selector-class { 43 | color: #268bd2; 44 | } 45 | 46 | /* Solarized Yellow */ 47 | .hljs-attribute, 48 | .hljs-attr, 49 | .hljs-variable, 50 | .hljs-template-variable, 51 | .hljs-class .hljs-title, 52 | .hljs-type { 53 | color: #b58900; 54 | } 55 | 56 | /* Solarized Orange */ 57 | .hljs-symbol, 58 | .hljs-bullet, 59 | .hljs-subst, 60 | .hljs-meta, 61 | .hljs-meta .hljs-keyword, 62 | .hljs-selector-attr, 63 | .hljs-selector-pseudo, 64 | .hljs-link { 65 | color: #cb4b16; 66 | } 67 | 68 | /* Solarized Red */ 69 | .hljs-built_in, 70 | .hljs-deletion { 71 | color: #dc322f; 72 | } 73 | 74 | .hljs-formula { 75 | background: #eee8d5; 76 | } 77 | 78 | .hljs-emphasis { 79 | font-style: italic; 80 | } 81 | 82 | .hljs-strong { 83 | font-weight: bold; 84 | } 85 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 3 | fixpoint 0.1.2

fixpoint 0.1.2

Released under the Apache License 2.0

Simple & Powerful Test Fixtures/Datasources for Clojure.

Installation

To install, add the following dependency to your project or build file:

[stylefruits/fixpoint "0.1.2"]

Namespaces

fixpoint.datasource.elastic

ElasticSearch Datasource Component

Public variables and functions:

fixpoint.datasource.hikari

HikariCP wrapper for any JDBCDatasource.

Public variables and functions:

fixpoint.datasource.jdbc

Generic JDBC datasource component.

Public variables and functions:

fixpoint.datasource.mysql

MySQL JDBC Datasource Component

Public variables and functions:

fixpoint.datasource.postgresql

PostgreSQL Datasource Component

Public variables and functions:

-------------------------------------------------------------------------------- /docs/js/page_effects.js: -------------------------------------------------------------------------------- 1 | function visibleInParent(element) { 2 | var position = $(element).position().top 3 | return position > -50 && position < ($(element).offsetParent().height() - 50) 4 | } 5 | 6 | function hasFragment(link, fragment) { 7 | return $(link).attr("href").indexOf("#" + fragment) != -1 8 | } 9 | 10 | function findLinkByFragment(elements, fragment) { 11 | return $(elements).filter(function(i, e) { return hasFragment(e, fragment)}).first() 12 | } 13 | 14 | function scrollToCurrentVarLink(elements) { 15 | var elements = $(elements); 16 | var parent = elements.offsetParent(); 17 | 18 | if (elements.length == 0) return; 19 | 20 | var top = elements.first().position().top; 21 | var bottom = elements.last().position().top + elements.last().height(); 22 | 23 | if (top >= 0 && bottom <= parent.height()) return; 24 | 25 | if (top < 0) { 26 | parent.scrollTop(parent.scrollTop() + top); 27 | } 28 | else if (bottom > parent.height()) { 29 | parent.scrollTop(parent.scrollTop() + bottom - parent.height()); 30 | } 31 | } 32 | 33 | function setCurrentVarLink() { 34 | $('.secondary a').parent().removeClass('current') 35 | $('.anchor'). 36 | filter(function(index) { return visibleInParent(this) }). 37 | each(function(index, element) { 38 | findLinkByFragment(".secondary a", element.id). 39 | parent(). 40 | addClass('current') 41 | }); 42 | scrollToCurrentVarLink('.secondary .current'); 43 | } 44 | 45 | var hasStorage = (function() { try { return localStorage.getItem } catch(e) {} }()) 46 | 47 | function scrollPositionId(element) { 48 | var directory = window.location.href.replace(/[^\/]+\.html$/, '') 49 | return 'scroll::' + $(element).attr('id') + '::' + directory 50 | } 51 | 52 | function storeScrollPosition(element) { 53 | if (!hasStorage) return; 54 | localStorage.setItem(scrollPositionId(element) + "::x", $(element).scrollLeft()) 55 | localStorage.setItem(scrollPositionId(element) + "::y", $(element).scrollTop()) 56 | } 57 | 58 | function recallScrollPosition(element) { 59 | if (!hasStorage) return; 60 | $(element).scrollLeft(localStorage.getItem(scrollPositionId(element) + "::x")) 61 | $(element).scrollTop(localStorage.getItem(scrollPositionId(element) + "::y")) 62 | } 63 | 64 | function persistScrollPosition(element) { 65 | recallScrollPosition(element) 66 | $(element).scroll(function() { storeScrollPosition(element) }) 67 | } 68 | 69 | function sidebarContentWidth(element) { 70 | var widths = $(element).find('.inner').map(function() { return $(this).innerWidth() }) 71 | return Math.max.apply(Math, widths) 72 | } 73 | 74 | function calculateSize(width, snap, margin, minimum) { 75 | if (width == 0) { 76 | return 0 77 | } 78 | else { 79 | return Math.max(minimum, (Math.ceil(width / snap) * snap) + (margin * 2)) 80 | } 81 | } 82 | 83 | function resizeSidebars() { 84 | var primaryWidth = sidebarContentWidth('.primary') 85 | var secondaryWidth = 0 86 | 87 | if ($('.secondary').length != 0) { 88 | secondaryWidth = sidebarContentWidth('.secondary') 89 | } 90 | 91 | // snap to grid 92 | primaryWidth = calculateSize(primaryWidth, 32, 13, 160) 93 | secondaryWidth = calculateSize(secondaryWidth, 32, 13, 160) 94 | 95 | $('.primary').css('width', primaryWidth) 96 | $('.secondary').css('width', secondaryWidth).css('left', primaryWidth + 1) 97 | 98 | if (secondaryWidth > 0) { 99 | $('#content').css('left', primaryWidth + secondaryWidth + 2) 100 | } 101 | else { 102 | $('#content').css('left', primaryWidth + 1) 103 | } 104 | } 105 | 106 | $(window).ready(resizeSidebars) 107 | $(window).ready(setCurrentVarLink) 108 | $(window).ready(function() { persistScrollPosition('.primary')}) 109 | $(window).ready(function() { 110 | $('#content').scroll(setCurrentVarLink) 111 | $(window).resize(setCurrentVarLink) 112 | $(window).resize(resizeSidebars) 113 | }) 114 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject stylefruits/fixpoint "0.1.4-SNAPSHOT" 2 | :description "Simple & Powerful Test Fixtures/Datasources for Clojure" 3 | :url "https://github.com/stylefruits/fixpoint" 4 | :license {:name "Apache License 2.0" 5 | :url "http://www.apache.org/licenses/LICENSE-2.0.html" 6 | :author "stylefruits GmbH" 7 | :year 2017 8 | :key "apache-2.0"} 9 | :dependencies [[org.clojure/clojure "1.8.0" :scope "provided"] 10 | [org.clojure/java.jdbc "0.6.1" :scope "provided"] 11 | [camel-snake-kebab "0.4.0"]] 12 | :profiles 13 | {:dev {:dependencies 14 | [[hikari-cp "1.7.5"] 15 | [org.postgresql/postgresql "9.4.1212"] 16 | [mysql/mysql-connector-java "5.1.41"] 17 | [cc.qbits/spandex "0.3.4" 18 | :exclusions [org.clojure/clojure]] 19 | [org.apache.qpid/qpid-broker "6.1.3" 20 | :exclusions [org.webjars.bower/dstore 21 | org.slf4j/slf4j-api]] 22 | [kithara "0.1.8"]]} 23 | :codox {:dependencies [[org.clojure/tools.reader "1.0.0"] 24 | [codox-theme-rdash "0.1.2"]] 25 | :plugins [[lein-codox "0.10.3"]] 26 | :codox {:project {:name "fixpoint"} 27 | :metadata {:doc/format :markdown} 28 | :themes [:rdash] 29 | :source-paths ["src"] 30 | :output-path "docs" 31 | 32 | :source-uri "https://github.com/stylefruits/fixpoint/blob/master/{filepath}#L{line}" 33 | :namespaces [fixpoint.core #"^fixpoint\.datasource\..*"]}}} 34 | :test-selectors {:default #(not-any? % [:elastic :mysql :postgresql]) 35 | :elastic :elastic 36 | :mysql :mysql 37 | :postgresql :postgresql} 38 | :aliases {"codox" ["with-profile" "codox,dev" "codox"]} 39 | :pedantic? :abort) 40 | -------------------------------------------------------------------------------- /resources/fixpoint/amqp/broker-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "${broker.name}", 3 | "modelVersion": "6.0", 4 | "authenticationproviders" : [ { 5 | "name" : "plain", 6 | "type" : "Plain", 7 | "secureOnlyMechanisms": [], 8 | "users" : [ { 9 | "name" : "${fixpoint.username}", 10 | "type" : "managed", 11 | "password" : "${fixpoint.password}" 12 | } ] 13 | } ], 14 | "brokerloggers" : [ { 15 | "name" : "stdout", 16 | "type" : "Console", 17 | "brokerloginclusionrules" : [ { 18 | "name" : "Root", 19 | "type" : "NameAndLevel", 20 | "level" : "WARN", 21 | "loggerName" : "ROOT" 22 | }, { 23 | "name" : "Qpid", 24 | "type" : "NameAndLevel", 25 | "level" : "${fixpoint.log_level}", 26 | "loggerName" : "org.apache.qpid.*" 27 | }, { 28 | "name" : "Operational", 29 | "type" : "NameAndLevel", 30 | "level" : "${fixpoint.log_level}", 31 | "loggerName" : "qpid.message.*" 32 | } ] 33 | } ], 34 | "ports" : [ { 35 | "name" : "AMQP", 36 | "port" : "${qpid.amqp_port}", 37 | "authenticationProvider" : "plain", 38 | "virtualhostaliases": [ { 39 | "name": "${fixpoint.vhost}", 40 | "type": "nameAlias" 41 | } ] 42 | } ], 43 | "virtualhostnodes": [ { 44 | "name": "${fixpoint.vhost}", 45 | "type": "JSON", 46 | "defaultVirtualHostNode": true, 47 | "virtualHostInitialConfiguration": "${qpid.initial_config_virtualhost_config}" 48 | } ] 49 | } 50 | -------------------------------------------------------------------------------- /src/fixpoint/core.clj: -------------------------------------------------------------------------------- 1 | (ns fixpoint.core 2 | "Fixture Functions and Protocols" 3 | (:refer-clojure :exclude [ref]) 4 | (:require [clojure.set :as set] 5 | [clojure.walk :as walk])) 6 | 7 | ;; ## Protocols 8 | 9 | (defprotocol Datasource 10 | "Protocol for fixture-capable test datasources." 11 | (datasource-id [this] 12 | "Return an ID for this datasource.") 13 | (start-datasource [this] 14 | "Start the datasource.") 15 | (stop-datasource [this] 16 | "Stop the datasource.") 17 | (run-with-rollback [this f] 18 | "Run `(f datasource)`, where `datasource` is a transacted version of the 19 | current datasource. Ensure that changes made via `datasource` are rolled 20 | back afterwards.") 21 | (insert-document! [this document] 22 | "Insert the given `data` into the current datasource. Return a map of 23 | `:id` (the reference ID), `:data` (the actual data inserted) and 24 | optionally `:tags` (a seq of tags for entity filtering). 25 | 26 | Reference IDs __must__ be namespaced keywords.") 27 | (as-raw-datasource [this] 28 | "Retrieve the underlying raw datasource, e.g. a `clojure.java.jdbc` database 29 | spec, or some raw connection object. 30 | 31 | This should fail on non-started datasources.")) 32 | 33 | (defprotocol Fixture 34 | "Protocol for datasource fixtures." 35 | (fixture-documents [this] 36 | "Generate a seq of fixture document maps, each one belonging to a single 37 | datasource identified by the `:fixpoint/datasource` key.")) 38 | 39 | (extend-protocol Fixture 40 | clojure.lang.Sequential 41 | (fixture-documents [sq] 42 | (mapcat fixture-documents sq)) 43 | 44 | clojure.lang.IPersistentMap 45 | (fixture-documents [m] 46 | [m]) 47 | 48 | nil 49 | (fixture-documents [_] 50 | [])) 51 | 52 | ;; ## Datasource Registry 53 | 54 | (def ^:private ^:dynamic *datasources* 55 | {}) 56 | 57 | (defn- run-with-datasource 58 | [datasource f] 59 | (let [id (datasource-id datasource)] 60 | (binding [*datasources* (assoc *datasources* id datasource)] 61 | (f datasource)))) 62 | 63 | (defn maybe-datasource 64 | "Get the [[Datasource]] registered with the given ID within the current scope, 65 | or `nil` if there is none." 66 | [id] 67 | (get *datasources* id)) 68 | 69 | (defn datasource 70 | "Get the [[Datasource]] registered under the given ID within the current scope. 71 | Throws an `AssertionError` if there is none." 72 | [id] 73 | (let [ds (maybe-datasource id)] 74 | (assert ds (format "no datasource registered as: %s" (pr-str id))) 75 | ds)) 76 | 77 | (defn raw-datasource 78 | "Get the raw datasource value for the [[Datasource]] registered under the 79 | given ID within the current scope. 80 | 81 | See [[datasource]] and [[as-raw-datasource]]." 82 | [id] 83 | (-> (datasource id) 84 | (as-raw-datasource))) 85 | 86 | ;; ## Rollback 87 | 88 | (defn ^:no-doc with-rollback* 89 | "See [[with-rollback]]." 90 | [ds f] 91 | (let [id (datasource-id ds) 92 | ds (datasource id)] 93 | (->> (fn [ds'] 94 | (binding [*datasources* (assoc *datasources* id ds')] 95 | (f ds'))) 96 | (run-with-rollback ds)))) 97 | 98 | (defmacro with-rollback 99 | "Run the given body within a 'transacted' version of the given datasource, 100 | rolling back after the run has finished. 101 | 102 | ```clojure 103 | (with-datasource [ds (pg/make-datasource ...)] 104 | (with-rollback [tx ds] 105 | (let [db (as-jdbc-datasource tx)] 106 | (jdbc/execute! db [\"INSERT INTO ...\" ...])))) 107 | ``` 108 | " 109 | [[sym datasource] & body] 110 | `(with-rollback* ~datasource 111 | (fn [~sym] ~@body))) 112 | 113 | ;; ## Startup/Shutdown 114 | 115 | (defn ^:no-doc with-datasource* 116 | "See [[with-datasource]]." 117 | [datasource f] 118 | (let [started-datasource (start-datasource datasource)] 119 | (try 120 | (run-with-datasource started-datasource f) 121 | (finally 122 | (stop-datasource started-datasource))))) 123 | 124 | (defmacro with-datasource 125 | "Start `datasource` and bind it to `sym`, then run `body` in its scope. 126 | 127 | ```clojure 128 | (with-datasource [ds (pg/make-datasource ...)] 129 | ...) 130 | ``` 131 | " 132 | [[sym datasource] & body] 133 | `(with-datasource* ~datasource 134 | (fn [~sym] ~@body))) 135 | 136 | (defmacro with-rollback-datasource 137 | "Start a 'transacted' version of `datasource`, rolling back any changed made 138 | after the run has finished. 139 | 140 | ```clojure 141 | (with-rollback-datasource [ds (pg/make-datasource ...)] 142 | (let [db (as-jdbc-datasource ds)] 143 | (jdbc/execute! db [\"INSERT INTO ...\" ...]))) 144 | ``` 145 | 146 | This is a convenience function combining [[with-datasource]] and 147 | [[with-rollback]]." 148 | [[sym datasource] & body] 149 | `(with-datasource [ds# ~datasource] 150 | (with-rollback [~sym ds#] 151 | ~@body))) 152 | 153 | ;; ## References 154 | 155 | (defn- reference? 156 | [value] 157 | (if (sequential? value) 158 | (recur (first value)) 159 | (and (keyword? value) 160 | (namespace value)))) 161 | 162 | (defn- parse-reference 163 | [value] 164 | (cond (vector? value) 165 | (let [[value' & path'] value] 166 | (when-let [[document-id path] (parse-reference value')] 167 | [document-id (concat path path')])) 168 | 169 | (reference? value) 170 | [value []])) 171 | 172 | ;; ## Fixture Insertion 173 | 174 | (defn- lookup-reference 175 | [entities document-id transformations] 176 | (assert 177 | (contains? entities document-id) 178 | (str "no such document available within the current fixture scope: " 179 | (pr-str document-id))) 180 | (if (every? keyword? transformations) 181 | (let [result (get-in entities (cons document-id transformations) ::none)] 182 | (assert (not= result ::none) 183 | (format "document '%s' does not contain property '%s': %s" 184 | (pr-str document-id) 185 | (pr-str (vec transformations)) 186 | (pr-str (get entities document-id)))) 187 | result) 188 | (let [document (get entities document-id)] 189 | (reduce 190 | (fn [value transformation] 191 | (transformation value)) 192 | document 193 | transformations)))) 194 | 195 | (defn- resolve-references* 196 | [entities value] 197 | (cond (reference? value) 198 | (if-let [[document-id path] (parse-reference value)] 199 | (->> (if (empty? path) [:id] path) 200 | (lookup-reference entities document-id)) 201 | value) 202 | 203 | (sequential? value) 204 | (cond-> (map #(resolve-references* entities %) value) 205 | (vector? value) (vec)) 206 | 207 | (map? value) 208 | (->> (for [[k v] value] 209 | [k (resolve-references* entities v)]) 210 | (into {})) 211 | 212 | :else value)) 213 | 214 | (defn- resolve-references 215 | [entities document] 216 | (->> (dissoc document :fixpoint/datasource :fixpoint/id) 217 | (resolve-references* entities))) 218 | 219 | (defn- throw-document-exception! 220 | [entities document ^Throwable throwable] 221 | (throw 222 | (ex-info 223 | (format "insertion failed for document: %s%n(%s)" 224 | (pr-str document) 225 | (.getMessage throwable)) 226 | {:document document 227 | :resolved-document (resolve-references entities document)} 228 | throwable))) 229 | 230 | (defn- assert-id-reference 231 | [{:keys [fixpoint/id] :as document}] 232 | (when id 233 | (assert (reference? id) 234 | (str 235 | "':fixpoint/id' needs to be a namespaced keyword: " 236 | (pr-str document))))) 237 | 238 | (defn- assert-insertion-result 239 | [document result] 240 | (when result 241 | (assert (:data result) 242 | (format 243 | (str "insertion result needs ':data' key.%n" 244 | "document: %s%n" 245 | "result: %s") 246 | (pr-str document) 247 | (pr-str result))))) 248 | 249 | (defn- update-entities 250 | [entities {:keys [fixpoint/id] :as document} result] 251 | (or (when id 252 | (when-let [{:keys [data tags]} result] 253 | (assert (not (contains? entities id)) 254 | (format 255 | (str "duplicate document ':id': %s%n" 256 | "document: %s%n" 257 | "result: %s") 258 | (pr-str id) 259 | (pr-str document) 260 | (pr-str result))) 261 | (reduce 262 | (fn [entities tag] 263 | (update-in entities [::index tag] (fnil conj #{}) id)) 264 | (assoc entities id data) 265 | tags))) 266 | entities)) 267 | 268 | (defn- insert-document-and-update-entities! 269 | [entities datasource-id document] 270 | (try 271 | (assert-id-reference document) 272 | (let [ds (datasource datasource-id) 273 | document' (resolve-references entities document) 274 | result (insert-document! ds document')] 275 | (assert-insertion-result document result) 276 | (update-entities entities document result)) 277 | (catch Throwable t 278 | (throw-document-exception! entities document t)))) 279 | 280 | (defn- insert-fixtures! 281 | [entities fixtures] 282 | (->> fixtures 283 | (mapcat fixture-documents) 284 | (reduce 285 | (fn [entities document] 286 | (let [datasource-id (:fixpoint/datasource document)] 287 | (assert datasource-id 288 | (format "document is missing ':fixpoint/datasource': %s" 289 | (pr-str document))) 290 | (insert-document-and-update-entities! 291 | entities 292 | datasource-id 293 | document))) 294 | entities))) 295 | 296 | ;; ## Fixture Access 297 | 298 | (def ^:private ^:dynamic *entities* 299 | {}) 300 | 301 | (defn ^:no-doc with-data* 302 | "See [[with-data]]." 303 | [fixtures f] 304 | (binding [*entities* (insert-fixtures! *entities* fixtures)] 305 | (f))) 306 | 307 | (defmacro with-data 308 | "Given a [[Fixture]] (or a seq of them), run them against their respective 309 | datasources, then execute `body`. 310 | 311 | ```clojure 312 | (defn person 313 | [name] 314 | (-> {:db/table :people 315 | :name name} 316 | (on-datasource :db))) 317 | 318 | (with-datasource [ds (pg/make-datasource :db ...)] 319 | (with-data [(person \"me\") (person \"you\")] 320 | ...)) 321 | ``` 322 | 323 | This has to be wrapped by [[with-datasource]] or [[with-rollback-datasource]] 324 | since otherwise there is nothing to insert into." 325 | [fixtures & body] 326 | `(with-data* ~fixtures 327 | (fn [] ~@body))) 328 | 329 | (defn property 330 | "Look up a single fixture document's property using a reference attached to a 331 | fixture using [[as]]. 332 | 333 | ```clojure 334 | (defn person 335 | [reference name] 336 | (-> {:db/table :people 337 | :name name} 338 | (as reference) 339 | (on-datasource :db))) 340 | 341 | (with-datasource [ds (pg/make-datasource :db ...)] 342 | (with-data [(person :person/me \"me\") 343 | (person :person/you \"you\")] 344 | (println (property :person/me)) 345 | (println (property :person/you :id)))) 346 | ``` 347 | 348 | `transformations` can be given to apply a sequence of functions, in order, 349 | to the fixture map. Has to be used within a [[with-data]] block." 350 | [document-id & transformations] 351 | (lookup-reference *entities* document-id transformations)) 352 | 353 | (defn properties 354 | "See [[property]]. Performs a lookup in multiple fixture documents, returning 355 | values in an order corresponding to `document-ids`." 356 | [document-ids & transformations] 357 | (map #(apply property % transformations) document-ids)) 358 | 359 | (defn match 360 | "Look up a fixture document's property for every entity that matches all 361 | of the given tags." 362 | [tags & transformations] 363 | (if-let [index-matches (seq (keep #(get-in *entities* [::index %]) tags))] 364 | (->> index-matches 365 | (reduce set/intersection) 366 | (map #(apply property % transformations))))) 367 | 368 | (defn id 369 | "Retrieve the `:id` [[property]] for the given document." 370 | [document-id] 371 | (property document-id :id)) 372 | 373 | (defn ids 374 | "Retrieve the `:id` [[property]] for each of the given documents." 375 | [document-ids] 376 | (properties document-ids :id)) 377 | 378 | (defn by-namespace 379 | "Retrieve a [[property]] for each entity whose reference (attached using 380 | [[as]]) has the given namespace. 381 | 382 | ```clojure 383 | (with-datasource [ds (pg/make-datasource :db ...)] 384 | (with-data [(person :person/me \"me\") 385 | (person :person/you \"you\") 386 | (post :post/happy :person/me \"yay!\")] 387 | (by-namespace :person :id))) 388 | ;; => {:person/me 1, :person/you 2} 389 | ``` 390 | 391 | Returns a map associating the reference (see [[as]]) with the queried 392 | property." 393 | [nspace & path] 394 | (let [n (name nspace)] 395 | (->> (for [[document-id value] *entities* 396 | :when (= (namespace document-id) n)] 397 | [document-id (lookup-reference *entities* document-id path)]) 398 | (into {})))) 399 | 400 | ;; ## Clojure Test Integration 401 | 402 | (defn use-datasources 403 | "A clojure.test fixture that will wrap test runs with startup/shutdown 404 | of the given datasources. After the tests have run, a rollback will 405 | be initiated to cleanup the database." 406 | [& datasources] 407 | (fn [f] 408 | (let [f' (reduce 409 | (fn [f datasource] 410 | (fn [] 411 | (with-rollback-datasource [_ datasource] 412 | (f)))) 413 | f datasources)] 414 | (f')))) 415 | 416 | (defn use-data 417 | "A clojure.test fixture that will insert the given fixtures into 418 | their respective datasources. 419 | 420 | Needs to be applied after [[use-datasources]]." 421 | [& fixtures] 422 | (fn [f] 423 | (with-data* fixtures f))) 424 | 425 | ;; ## Helper 426 | 427 | (defn on-datasource 428 | "Declare the given fixture's target datasource, corresponding to one 429 | to-be-instantiated within [[with-datasource]] or 430 | [[with-rollback-datasource]]. 431 | 432 | ```clojure 433 | (defn person 434 | [reference name] 435 | (-> {:db/table :people 436 | :name name} 437 | (as reference) 438 | (on-datasource :db))) 439 | 440 | (with-datasource [ds (pg/make-datasource :db ...)] 441 | (with-data [(person :person/me \"me\") ...] 442 | ...)) 443 | ``` 444 | 445 | The result can be passed to [[with-data]] to be run against the actual 446 | datasource." 447 | [data datasource-id] 448 | {:pre [(map? data)]} 449 | (assoc data :fixpoint/datasource datasource-id)) 450 | 451 | (defn as 452 | "Declare the given fixture's reference ID, which can be used from within other 453 | fixtures. 454 | 455 | ```clojure 456 | (defn person 457 | [reference name] 458 | (-> {:db/table :people 459 | :name name} 460 | (as reference) 461 | (on-datasource :db))) 462 | 463 | (defn post 464 | [reference author-reference text] 465 | (-> {:db/table :posts 466 | :author-id author-reference 467 | :text text} 468 | (as reference) 469 | (on-datasource :db))) 470 | ``` 471 | 472 | A simple set of fixtures could be: 473 | 474 | ```clojure 475 | [(person :person/me \"me\") 476 | (post :post/happy :person/me \"yay!\")] 477 | ``` 478 | 479 | You can also reference specific fields of other documents using a vector 480 | notation, e.g. to declare a post with the same author as `:post/happy`: 481 | 482 | ```clojure 483 | (post :post/again [:post/happy :author-id] \"still yay!\") 484 | ``` 485 | 486 | Note that every reference ID can be used _exactly once_. " 487 | [data document-id] 488 | {:pre [(map? data) 489 | (reference? document-id)]} 490 | (assoc data :fixpoint/id document-id)) 491 | -------------------------------------------------------------------------------- /src/fixpoint/datasource/amqp.clj: -------------------------------------------------------------------------------- 1 | (ns fixpoint.datasource.amqp 2 | "An AMQP broker datasource compatible with AMQP 0.9.1. 3 | 4 | Make sure the `org.apache.qpid/qpid-broker` dependency is available on your 5 | classpath." 6 | (:require [fixpoint.datasource.file-utils :as f] 7 | [fixpoint.core :as fix] 8 | [clojure.java.io :as io]) 9 | (:import [org.apache.qpid.server Broker BrokerOptions])) 10 | 11 | ;; ## Helper 12 | 13 | (defmacro ^:private let-cleanup 14 | [[sym start-fn stop-fn & more] & body] 15 | (if (seq more) 16 | `(let-cleanup [~sym ~start-fn ~stop-fn] 17 | (let-cleanup [~@more] 18 | ~@body)) 19 | `(let [val# ~start-fn 20 | ~sym val#] 21 | (try 22 | (do ~@body) 23 | (catch Throwable t# 24 | (~stop-fn val#) 25 | (throw t#)))))) 26 | 27 | ;; ## Metadata 28 | 29 | (def ^:private default-configuration-file 30 | (io/resource "fixpoint/amqp/broker-config.json")) 31 | 32 | (defn- random-string 33 | [] 34 | (str (java.util.UUID/randomUUID))) 35 | 36 | (defn- prepare-broker-data 37 | [{:keys [log-level port configuration-file username password vhost]}] 38 | {:workdir (f/create-temporary-directory!) 39 | :port (or port 57622) 40 | :log-level (or log-level :error) 41 | :username (or username (random-string)) 42 | :password (or password (random-string)) 43 | :vhost (or vhost "default") 44 | :config (or configuration-file default-configuration-file)}) 45 | 46 | (defn- cleanup-broker-data 47 | [{:keys [workdir]}] 48 | (f/delete-directory-recursively! workdir) 49 | nil) 50 | 51 | ;; ## Broker Logic 52 | 53 | (defn- prop 54 | [^BrokerOptions options k v] 55 | (.setConfigProperty options (name k) (str v))) 56 | 57 | (defn- start-broker! 58 | [{:keys [config log-level port username password vhost workdir]}] 59 | (let [options (doto (BrokerOptions.) 60 | (prop :broker.name "fixpoint-embedded-amqp") 61 | (prop :qpid.amqp_port port) 62 | (prop :qpid.http_port (inc port)) 63 | (prop :qpid.work_dir (f/path->string workdir)) 64 | (prop :fixpoint.log_level (-> log-level name (.toUpperCase))) 65 | (prop :fixpoint.username username) 66 | (prop :fixpoint.password password) 67 | (prop :fixpoint.vhost vhost) 68 | (.setInitialConfigurationLocation (str config)))] 69 | (doto (Broker.) 70 | (.startup options)))) 71 | 72 | (defn- stop-broker! 73 | [^Broker broker] 74 | (.shutdown broker)) 75 | 76 | ;; ## Datasource 77 | 78 | (defrecord AmqpDatasource [id options metadata broker] 79 | fix/Datasource 80 | (datasource-id [this] 81 | id) 82 | (start-datasource [this] 83 | (let-cleanup [metadata (prepare-broker-data options) cleanup-broker-data 84 | broker (start-broker! metadata) stop-broker!] 85 | (assoc this 86 | :broker broker 87 | :metadata metadata))) 88 | (stop-datasource [this] 89 | (stop-broker! broker) 90 | (cleanup-broker-data metadata) 91 | (assoc this :metadata nil, :broker nil)) 92 | (run-with-rollback [this f] 93 | (f this)) 94 | (insert-document! [this document] 95 | (throw 96 | (IllegalArgumentException. 97 | "AmqpDatasource does not accept any documents."))) 98 | (as-raw-datasource [_] 99 | (-> metadata 100 | (select-keys [:port :username :password :vhost]) 101 | (update :vhost #(str "/" %))))) 102 | 103 | (defn make-datasource 104 | "Create a datasource corresponding to an AMQP 0.9.1 broker. Options include: 105 | 106 | - `:port`: the port to run the broker on (default: 57622), 107 | - `log-level`: the broker's log level (`:debug`, `:info`, `:warn`, `:error`), 108 | - `:username`: the username for authentication (default: random), 109 | - `:password`: the password for authentication (default: random), 110 | - `:vhost`: the name of the default vhost to create (default: \"default\"). 111 | 112 | This datasource doesn't accept any documents, it just sets up an AMQP broker 113 | and exposes the port, vhost and credentials using [[raw-datasource]]." 114 | ([id] 115 | (make-datasource id {})) 116 | ([id options] 117 | (map->AmqpDatasource 118 | {:id id 119 | :options options}))) 120 | -------------------------------------------------------------------------------- /src/fixpoint/datasource/elastic.clj: -------------------------------------------------------------------------------- 1 | (ns fixpoint.datasource.elastic 2 | "ElasticSearch Datasource Component 3 | 4 | Make sure the `cc.qbits/spandec` dependency is available on your 5 | classpath." 6 | (:require [fixpoint.core :as fix] 7 | [qbits.spandex :as s] 8 | [qbits.spandex.utils :as utils]) 9 | (:import [java.util UUID])) 10 | 11 | ;; ## Client 12 | 13 | (defn- start-client 14 | [hosts] 15 | (s/client 16 | {:hosts hosts 17 | :request {:connect-timeout 10000 18 | :socket-timeout 10000} 19 | :http-client {:max-conn-per-route 16 20 | :max-conn-total (* (count hosts) 16)}})) 21 | 22 | (defn- stop-client 23 | [client] 24 | (s/close! client) 25 | nil) 26 | 27 | ;; ## Queries 28 | 29 | (defn- request 30 | [{:keys [client]} method url body & [opts]] 31 | (if (and client (not= client ::none)) 32 | (->> {:url (utils/url url) 33 | :method method 34 | :body body} 35 | (merge opts) 36 | (s/request client)))) 37 | 38 | (defn- return-body-on-success 39 | [{:keys [status body]} & [path]] 40 | (when (<= 200 status 204) 41 | (if path 42 | (get-in body path) 43 | body))) 44 | 45 | (defn- put 46 | [es index type id doc] 47 | {:pre [index type id (map? doc)]} 48 | (-> (request es :put [index type id] doc) 49 | (return-body-on-success))) 50 | 51 | ;; ## Indices 52 | 53 | (def ^:private default-index-settings 54 | {:number_of_shards 6}) 55 | 56 | (defn- create-index! 57 | [es index mapping & [settings]] 58 | (->> {:settings (merge default-index-settings settings) 59 | :mappings mapping} 60 | (request es :put [index]) 61 | (return-body-on-success))) 62 | 63 | (defn- delete-index! 64 | [es index] 65 | (-> (request es :delete [index] nil) 66 | (return-body-on-success))) 67 | 68 | (defn- refresh-index! 69 | [es index] 70 | (-> (request es :post [index :_refresh] nil) 71 | (return-body-on-success))) 72 | 73 | ;; ## Helpers 74 | 75 | (defn- random-id 76 | [& [prefix]] 77 | (str (some-> prefix name (str "-")) 78 | (UUID/randomUUID))) 79 | 80 | (defn- assert-name-key 81 | [document key] 82 | (let [value (get document key)] 83 | (assert (or (string? value) (keyword? value)) 84 | (str "ES fixture requires `" key "` key, and it has to be " 85 | "a keyword or string: " 86 | (pr-str document))))) 87 | 88 | (defn- assert-optional-name-key 89 | [document key] 90 | (let [value (get document key)] 91 | (assert (or (nil? value) (string? value) (keyword? value)) 92 | (str "ES fixture allows `" key "` key, but it has to be " 93 | "a keyword or string: " 94 | (pr-str document))))) 95 | 96 | (defn- assert-map-key 97 | [document key] 98 | (let [value (get document key)] 99 | (assert 100 | (map? value) 101 | (str "ES fixture requires `" key "` key, and it has to be a map: " 102 | (pr-str document))))) 103 | 104 | ;; ## Datasource 105 | ;; 106 | ;; This datasource takes two kinds of documents: 107 | ;; 108 | ;; - insert documents (w/ `:elastic/index`) 109 | ;; - index setup documents (w/ additional `:elastic/create?` and 110 | ;; `:elastic/mapping`) 111 | 112 | (defn- create-index-name! 113 | [{:keys [indices]} {:keys [elastic/index] :as document}] 114 | (let [index-name (random-id index) 115 | index-key (name index)] 116 | (swap! indices 117 | (fn [indices] 118 | (assert (not (contains? indices index-key)) 119 | (str 120 | "ES fixture contains the already used index key `" 121 | index "`: " (pr-str document))) 122 | (assoc indices index-key index-name))) 123 | index-name)) 124 | 125 | (defn- lookup-index-name 126 | [{:keys [indices]} {:keys [elastic/index] :as document}] 127 | (let [index-name (get @indices (name index))] 128 | (assert index-name 129 | (str "ES fixture requires index `" index "`: " (pr-str document))) 130 | index-name)) 131 | 132 | (defn- handle-create-index! 133 | [es {:keys [elastic/mapping] :as fixture}] 134 | (assert-name-key fixture :elastic/index) 135 | (assert-map-key fixture :elastic/mapping) 136 | (let [index-name (create-index-name! es fixture) 137 | [success? result] 138 | (try 139 | [true (create-index! es index-name mapping)] 140 | (catch clojure.lang.ExceptionInfo ex 141 | [false (:body (ex-data ex))]))] 142 | (assert success? 143 | (str "creation of index failed for ES fixture: " 144 | (pr-str fixture) "\n" 145 | "status: " (:status result) "\n" 146 | "error: " (:error result))) 147 | {:data {:id index-name}})) 148 | 149 | (defn- handle-declare-index! 150 | [es fixture] 151 | (assert-name-key fixture :elastic/index) 152 | (let [index-name (create-index-name! es fixture)] 153 | {:data {:id index-name}})) 154 | 155 | (defn- handle-put! 156 | [es {:keys [elastic/index elastic/type elastic/id] :as document}] 157 | (assert-name-key document :elastic/index) 158 | (assert-name-key document :elastic/type) 159 | (assert-optional-name-key document :elastic/id) 160 | (let [document' (dissoc document :elastic/index :elastic/type :elastic/id) 161 | id (or id (random-id (str (name index) "-" (name type)))) 162 | index-name (lookup-index-name es document) 163 | [success? result] 164 | (try 165 | [true (put es index-name type id document')] 166 | (catch clojure.lang.ExceptionInfo ex 167 | [false (:body (ex-data ex))]))] 168 | (assert success? 169 | (str "insertion of document failed for ES fixture: " 170 | (pr-str document) "\n" 171 | "status: " (:status result) "\n" 172 | "error: " (:error result))) 173 | (refresh-index! es index-name) 174 | {:data (-> document 175 | (update :elastic/type name) 176 | (assoc :elastic/index index-name) 177 | (assoc :elastic/id id))})) 178 | 179 | (defn- rollback-indices! 180 | [es indices] 181 | (doseq [[index-key index] indices] 182 | (try 183 | (delete-index! es index) 184 | (catch clojure.lang.ExceptionInfo ex 185 | (let [{:keys [status body]} (ex-data ex)] 186 | (when (not= status 404) 187 | (println 188 | (format "WARN: could not deleted index (%d): %s%n%s" 189 | status 190 | index 191 | (pr-str body)))))) 192 | (catch Throwable t 193 | (println 194 | (format "WARN: could not delete index (unexpected error): %s%n%s" 195 | index 196 | (pr-str t))))))) 197 | 198 | (defrecord ElasticDatasource [id indices hosts client] 199 | fix/Datasource 200 | (datasource-id [_] 201 | id) 202 | (start-datasource [this] 203 | (-> this 204 | (assoc :client (start-client hosts)))) 205 | (stop-datasource [this] 206 | (-> this 207 | (update :client stop-client))) 208 | (run-with-rollback [this f] 209 | (let [old-indices (if indices (keys @indices) #{}) 210 | indices (or indices (atom {}))] 211 | (try 212 | (f (assoc this :indices indices)) 213 | (finally 214 | (rollback-indices! this (reduce disj @indices old-indices)))))) 215 | (insert-document! [this {:keys [elastic/mapping] :as document}] 216 | (cond (false? mapping) (handle-declare-index! this document) 217 | mapping (handle-create-index! this document) 218 | :else (handle-put! this document))) 219 | (as-raw-datasource [_] 220 | client)) 221 | 222 | (defn make-datasource 223 | "Create an ElasticSearch datasource. Rollback capability is achieved by 224 | only allowing index declaration through the datasource, tracking and deleting 225 | any created ones. 226 | 227 | An index creation document has to contain both the `:elastic/index` and 228 | the `:elastic/mapping` key: 229 | 230 | ```clojure 231 | {:elastic/index :people 232 | :elastic/mapping {:person 233 | {:properties 234 | {:name {:type :string} 235 | :age {:type :long}}}}} 236 | ``` 237 | 238 | The index name will be randomly generated and can be accessed by passing the 239 | value given in `:elastic/index` to [[index]]. 240 | 241 | If `:elastic/mapping` is set to `false`, the index will not be created, but 242 | a name will be reserved and, on rollback, cleanup initiated. 243 | 244 | Actual documents have to reference their respective `:elastic/index` and 245 | specify an `:elastic/type` pointing at the mapping they should conform to. 246 | Optionally, an explicit ID can be set using `:elastic/id`. 247 | 248 | ```clojure 249 | {:elastic/index :people 250 | :elastic/type :person 251 | :name \"Me\" 252 | :age 27} 253 | ```" 254 | [id hosts] 255 | (map->ElasticDatasource 256 | {:hosts (if (string? hosts) 257 | [hosts] 258 | hosts) 259 | :id id})) 260 | 261 | ;; ## Helpers 262 | 263 | (defn index 264 | "Retrieve the actual name of the index that was declared using `index-key` 265 | as `:elastic/index`. 266 | 267 | ```clojure 268 | (with-datasource [es (elastic/make-datasource :elastic ...)] 269 | (with-data [{:elastic/index :people, :elastic/mapping ...}] 270 | (index :elastic :people))) 271 | ;; => \"people-ec0796d7-c1b6-49de-a2d8-e60f262b608d\" 272 | ``` 273 | " 274 | [datasource-id index-key] 275 | (let [{:keys [indices]} (fix/datasource datasource-id) 276 | index-name (some-> indices deref (get (name index-key)))] 277 | (assert index-name 278 | (str "no such index within ES fixture context: " 279 | index-key)) 280 | index-name)) 281 | -------------------------------------------------------------------------------- /src/fixpoint/datasource/file_utils.clj: -------------------------------------------------------------------------------- 1 | (ns fixpoint.datasource.file-utils 2 | (:import [java.nio.file Files Path SimpleFileVisitor FileVisitResult] 3 | [java.nio.file.attribute FileAttribute])) 4 | 5 | ;; ## Directory Handling 6 | 7 | (let [empty-array (into-array FileAttribute [])] 8 | (defn create-temporary-directory! 9 | ^Path [] 10 | (Files/createTempDirectory "fixpoint-embedded-amqp" empty-array))) 11 | 12 | (let [delete-visitor (proxy [SimpleFileVisitor] [] 13 | (visitFile [file _] 14 | (Files/delete file) 15 | FileVisitResult/CONTINUE) 16 | (postVisitDirectory [directory exception] 17 | (when-not exception 18 | (Files/delete directory)) 19 | FileVisitResult/CONTINUE))] 20 | (defn delete-directory-recursively! 21 | [^Path directory] 22 | (Files/walkFileTree directory delete-visitor))) 23 | 24 | (defn path->string 25 | [^Path path] 26 | (-> path 27 | (.toAbsolutePath) 28 | (str))) 29 | -------------------------------------------------------------------------------- /src/fixpoint/datasource/hikari.clj: -------------------------------------------------------------------------------- 1 | (ns fixpoint.datasource.hikari 2 | "HikariCP wrapper for any [[JDBCDatasource]]." 3 | (:require [fixpoint.core :as fix] 4 | [fixpoint.datasource.jdbc :as fix-jdbc] 5 | [hikari-cp.core :as hikari] 6 | [clojure.set :as set] 7 | [clojure.java.jdbc :as jdbc])) 8 | 9 | ;; ## Pool 10 | 11 | (defn- make-hikari-config 12 | [db-spec pool-options] 13 | {:pre [(map? db-spec)]} 14 | (-> db-spec 15 | (set/rename-keys 16 | {:connection-uri :jdbc-url 17 | :subname :jdbc-url 18 | :host :server-name 19 | :port :port-number 20 | :subprotocol :adapter}) 21 | (merge pool-options))) 22 | 23 | (defn- start-pool! 24 | [db-spec pool-options] 25 | (-> (make-hikari-config db-spec pool-options) 26 | (hikari/make-datasource))) 27 | 28 | (defn- stop-pool! 29 | [pool] 30 | (hikari/close-datasource pool)) 31 | 32 | ;; ## Datasource Startup/Shutdown Logic 33 | 34 | (defn- cache-db-spec 35 | [{:keys [jdbc-datasource] :as this}] 36 | (assoc this 37 | :cached-db-spec 38 | (fix-jdbc/get-db-spec jdbc-datasource))) 39 | 40 | (defn- clear-db-spec 41 | [this] 42 | (assoc this :cached-db-spec nil)) 43 | 44 | (defn- instantiate-pool 45 | [{:keys [pool-options cached-db-spec] :as this}] 46 | (let [pool (start-pool! cached-db-spec pool-options)] 47 | (-> this 48 | (assoc :pool pool) 49 | (update :jdbc-datasource fix-jdbc/set-db-spec {:datasource pool})))) 50 | 51 | (defn- cleanup-pool 52 | [{:keys [cached-db-spec pool] :as this}] 53 | (-> this 54 | (update :jdbc-datasource 55 | fix-jdbc/set-db-spec 56 | cached-db-spec) 57 | (update :pool stop-pool!))) 58 | 59 | (defn- start-jdbc-datasource 60 | [this] 61 | (update this :jdbc-datasource fix/start-datasource)) 62 | 63 | (defn- stop-jdbc-datasource 64 | [this] 65 | (update this :jdbc-datasource fix/stop-datasource)) 66 | 67 | ;; ## Rollback Logic 68 | 69 | (defn- run-with-jdbc-datasource-rollback 70 | [{:keys [jdbc-datasource] :as this} f] 71 | (->> (fn [datasource'] 72 | (f (assoc this :jdbc-datasource datasource'))) 73 | (fix/run-with-rollback jdbc-datasource))) 74 | 75 | ;; ## Component 76 | 77 | (defrecord HikariDatasource [jdbc-datasource 78 | cached-db-spec 79 | pool-options 80 | pool] 81 | fix/Datasource 82 | (datasource-id [_] 83 | (fix/datasource-id jdbc-datasource)) 84 | (start-datasource [this] 85 | (-> this 86 | (cache-db-spec) 87 | (instantiate-pool) 88 | (start-jdbc-datasource))) 89 | (stop-datasource [this] 90 | (-> this 91 | (stop-jdbc-datasource) 92 | (cleanup-pool) 93 | (clear-db-spec))) 94 | (run-with-rollback [this f] 95 | (run-with-jdbc-datasource-rollback this f)) 96 | (insert-document! [_ document] 97 | (fix/insert-document! jdbc-datasource document)) 98 | (as-raw-datasource [_] 99 | (fix/as-raw-datasource jdbc-datasource))) 100 | 101 | (defn wrap-jdbc-datasource 102 | "Wrap the given [[JDBCDatasource]] to use a Hikari connection pool." 103 | [jdbc-datasource & [pool-options]] 104 | {:pre (satisfies? fix-jdbc/JDBCDatasource jdbc-datasource)} 105 | (map->HikariDatasource 106 | {:jdbc-datasource jdbc-datasource 107 | :pool-options pool-options})) 108 | -------------------------------------------------------------------------------- /src/fixpoint/datasource/jdbc.clj: -------------------------------------------------------------------------------- 1 | (ns fixpoint.datasource.jdbc 2 | "Generic JDBC datasource component." 3 | (:require [fixpoint.core :as fix] 4 | [camel-snake-kebab 5 | [core :as csk] 6 | [extras :refer [transform-keys]]] 7 | [clojure.java.jdbc :as jdbc])) 8 | 9 | ;; ## Logic 10 | 11 | (defn- run-with-transaction-rollback 12 | [{:keys [db] :as this} f] 13 | (jdbc/with-db-transaction [tx db] 14 | (jdbc/db-set-rollback-only! tx) 15 | (f (assoc this :db tx)))) 16 | 17 | (defn- table-for 18 | [{:keys [db/table] :as document}] 19 | (assert table 20 | (str "no ':db/table' key given in JDBC fixture: " 21 | (pr-str document))) 22 | (csk/->snake_case_string table)) 23 | 24 | (defn- prepare-for-insert 25 | [document] 26 | (->> (dissoc document :db/table) 27 | (transform-keys csk/->snake_case_string))) 28 | 29 | (defn- read-after-insert 30 | [{:keys [db/tags]} result] 31 | (let [result' (transform-keys csk/->kebab-case-keyword result)] 32 | {:data result' 33 | :tags (vec tags)})) 34 | 35 | (defn- insert! 36 | [{:keys [db pre-fn post-fn]} document] 37 | (let [table (table-for document) 38 | document' (prepare-for-insert document)] 39 | (if-not (empty? document') 40 | (->> document' 41 | (pre-fn) 42 | (jdbc/insert! db table) 43 | (first) 44 | (post-fn db document) 45 | (read-after-insert document))))) 46 | 47 | ;; ## Protocol 48 | 49 | (defprotocol JDBCDatasource 50 | "Protocol for JDBC datasources." 51 | (get-db-spec [this] 52 | "Retrieve the JDBC datasource's database spec.") 53 | (set-db-spec [this new-db-spec] 54 | "Set the JDBC datasource's database spec.")) 55 | 56 | ;; ## Datasource 57 | 58 | (defrecord Database [id db pre-fn post-fn] 59 | JDBCDatasource 60 | (get-db-spec [_] 61 | db) 62 | (set-db-spec [this new-db-spec] 63 | (assoc this :db new-db-spec)) 64 | 65 | fix/Datasource 66 | (datasource-id [_] 67 | id) 68 | (start-datasource [this] 69 | this) 70 | (stop-datasource [this] 71 | this) 72 | (run-with-rollback [this f] 73 | (run-with-transaction-rollback this f)) 74 | (insert-document! [this document] 75 | (insert! this document)) 76 | (as-raw-datasource [_] 77 | db)) 78 | 79 | (defn make-datasource 80 | "Create a JDBC datasource with the given `id`. `overrides` can contain: 81 | 82 | - `:pre-fn`: to be applied to each document before insertion, 83 | - `:post-fn`: to be applied to the `db-spec` and each insertion result, 84 | transforming the data returned by a fixture. 85 | 86 | Both can be useful for JDBC drivers that need special handling, e.g. MySQL 87 | which does not return inserted data, but a map with `:generated_key` only. 88 | 89 | Documents passed to this datasource need to contain the `:db/table` key 90 | pointing at the database table they should be inserted into. Every other 91 | key within the document will be interpreted as a database column and its 92 | value." 93 | [id db-spec & [overrides]] 94 | (map->Database 95 | (merge 96 | {:id id 97 | :db db-spec 98 | :pre-fn identity 99 | :post-fn #(do %3)} 100 | overrides))) 101 | -------------------------------------------------------------------------------- /src/fixpoint/datasource/mysql.clj: -------------------------------------------------------------------------------- 1 | (ns fixpoint.datasource.mysql 2 | "MySQL JDBC Datasource Component 3 | 4 | Make sure the `mysql/mysql-connector-java` dependency is available on your 5 | classpath." 6 | (:require [fixpoint.datasource.jdbc :as fix-jdbc] 7 | [camel-snake-kebab.core :as csk] 8 | [clojure.java.jdbc :as jdbc])) 9 | 10 | ;; ## MySQL Adjustments 11 | ;; 12 | ;; We need to do an additional query to fetch the inserted row. For this, we 13 | ;; need to know the primary key column and the generated key. 14 | 15 | (defn- verify-inserted-row! 16 | [table-name column document-id row] 17 | (when-not row 18 | (throw 19 | (IllegalStateException. 20 | (format 21 | "Could not fetch inserted row using column '%s.%s' and value: %s" 22 | table-name 23 | column 24 | document-id))))) 25 | 26 | (defn- query-inserted-row 27 | [db 28 | {:keys [db/table db/primary-key] 29 | :or {db/primary-key :id} 30 | :as document} 31 | {:keys [generated_key]}] 32 | (when-let [document-id (or generated_key (get document primary-key))] 33 | (let [table-name (csk/->snake_case_string table) 34 | column (csk/->snake_case_string primary-key) 35 | [row] (->> [(format "select * from %s where %s = ? limit 1" 36 | table-name 37 | column) 38 | document-id] 39 | (jdbc/query db))] 40 | (verify-inserted-row! table-name column document-id row) 41 | row))) 42 | 43 | (defn- remove-primary-key-field 44 | [document] 45 | (dissoc document :db/primary-key)) 46 | 47 | ;; ## Datasource 48 | 49 | (defn make-datasource 50 | "Create a MySQL JDBC datasource. Documents passed to this datasource need 51 | to have the format described in [[fixpoint.datasource.jdbc/make-datasource]]. 52 | 53 | ```clojure 54 | {:db/table :people 55 | :name \"me\" 56 | :age 28} 57 | ``` 58 | 59 | If the respective table's primary key column is not `:id`, it has to 60 | additionally be specified using a `:db/primary-key` value. 61 | 62 | ```clojure 63 | {:db/table :people 64 | :db/primary-key :uuid 65 | :name \"me\" 66 | :age 28} 67 | ``` 68 | 69 | (The reason for this is that, for MySQL, we need to perform a `SELECT` 70 | statement after insertion, since all we get back is the `:generated_key`.)" 71 | [id db-spec] 72 | (fix-jdbc/make-datasource 73 | id 74 | db-spec 75 | {:pre-fn remove-primary-key-field 76 | :post-fn query-inserted-row})) 77 | -------------------------------------------------------------------------------- /src/fixpoint/datasource/postgresql.clj: -------------------------------------------------------------------------------- 1 | (ns fixpoint.datasource.postgresql 2 | "PostgreSQL Datasource Component 3 | 4 | Make sure the `org.postgresql/postgresql` dependency is available on your 5 | classpath." 6 | (:require [fixpoint.datasource.jdbc :as fix-jdbc])) 7 | 8 | ;; This is identical to the JDBC datasource, but just in case, we alias it 9 | ;; for future extensibility. 10 | 11 | (defn make-datasource 12 | "Create a PostgreSQL JDBC datasource. Documents passed to this datasource need 13 | to have the format described in [[fixpoint.datasource.jdbc/make-datasource]]. 14 | 15 | ```clojure 16 | {:db/table :people 17 | :name \"me\" 18 | :age 28} 19 | ``` 20 | " 21 | [id db-spec] 22 | (fix-jdbc/make-datasource id db-spec)) 23 | -------------------------------------------------------------------------------- /test/fixpoint/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns fixpoint.core-test 2 | (:require [clojure.test :refer :all] 3 | [fixpoint.core :as fix])) 4 | 5 | ;; ## Datasource 6 | 7 | (defrecord DummySource [data state] 8 | fix/Datasource 9 | (datasource-id [_] 10 | :db) 11 | (start-datasource [this] 12 | (swap! state conj :started) 13 | (assoc this :data (atom {}))) 14 | (stop-datasource [this] 15 | (swap! state conj :stopped) 16 | (dissoc this :data)) 17 | (run-with-rollback [this f] 18 | (let [tx (atom @data)] 19 | (try 20 | (f (assoc this :data tx)) 21 | (finally 22 | (swap! state conj :rolled-back))))) 23 | (insert-document! [_ {:keys [id] :as row}] 24 | (let [generic-id (gensym) 25 | row' (assoc row :id generic-id)] 26 | (swap! data assoc generic-id row') 27 | {:data row'}))) 28 | 29 | (defn- dummy-datasource 30 | [& [state]] 31 | (map->DummySource 32 | {:state (or state (atom []))})) 33 | 34 | ;; ## Test Data 35 | 36 | (def ^:private test-docs 37 | [:doc/a :doc/b :doc/c]) 38 | 39 | (def ^:private test-fixtures 40 | (->> test-docs 41 | (map-indexed #(hash-map :index %1 :fixpoint/id %2)) 42 | (map #(assoc % :fixpoint/datasource :db)))) 43 | 44 | ;; ## Tests 45 | 46 | (deftest t-with-datasource 47 | (let [state (atom [])] 48 | (fix/with-datasource [ds (dummy-datasource state)] 49 | (testing "datasource was started." 50 | (is (instance? clojure.lang.Atom (:data ds))) 51 | (is (= @state [:started]))) 52 | (testing "datasource is registered in scope." 53 | (is (identical? (fix/datasource :db) ds)))) 54 | (is (= @state [:started :stopped])))) 55 | 56 | (deftest t-with-data 57 | (fix/with-datasource [_ (dummy-datasource)] 58 | (fix/with-data test-fixtures 59 | (is (every? (comp symbol? fix/id) test-docs)) 60 | (is (every? symbol? (fix/ids test-docs))) 61 | (is (= [0 1 2] (map #(fix/property % :index) test-docs))) 62 | (is (= [0 1 2] (fix/properties test-docs :index)))))) 63 | 64 | (deftest t-with-rollback 65 | (fix/with-datasource [ds (dummy-datasource)] 66 | (is (empty? @(:data ds))) 67 | (fix/with-rollback [tx ds] 68 | (is (empty? @(:data tx))) 69 | (fix/with-data test-fixtures 70 | (is (every? (comp symbol? fix/id) test-docs)) 71 | (is (every? symbol? (fix/ids test-docs))) 72 | (is (= [0 1 2] (map #(fix/property % :index) test-docs))) 73 | (is (= [0 1 2] (fix/properties test-docs :index)))) 74 | (is (= 3 (count @(:data tx))))) 75 | (is (empty? @(:data ds))))) 76 | 77 | (deftest t-references 78 | (fix/with-datasource [_ (dummy-datasource)] 79 | (fix/with-data (->> [(-> {:name "me"} 80 | (fix/as :doc/me)) 81 | (-> {:name "you" 82 | :friend-name [:doc/me :name] 83 | :friend-id :doc/me 84 | :first-char [:doc/me :name first str]} 85 | (fix/as :doc/you)) 86 | (-> {:name "someone"} 87 | (fix/as :other/them))] 88 | (map #(fix/on-datasource % :db))) 89 | (is (= {:doc/me "me", :doc/you "you"} 90 | (fix/by-namespace :doc :name))) 91 | (is (= {:other/them "someone"} 92 | (fix/by-namespace :other :name))) 93 | (is (= "me" 94 | (fix/property :doc/you :friend-name))) 95 | (is (= "prefixed-me" 96 | (fix/property :doc/you :friend-name #(str "prefixed-" %)))) 97 | (is (= "m" 98 | (fix/property :doc/you :first-char))) 99 | (is (= (fix/id :doc/me) 100 | (fix/property :doc/you :friend-id)))))) 101 | 102 | (deftest t-use-datasources 103 | (let [state (atom []) 104 | datasource (dummy-datasource state) 105 | fixture-fn (fix/use-datasources datasource)] 106 | (->> (fn [] 107 | (let [ds (fix/datasource :db)] 108 | (testing "datasource was started." 109 | (is (instance? clojure.lang.Atom (:data ds))) 110 | (is (= @state [:started]))))) 111 | (fixture-fn)) 112 | (testing "datasource was rolled back." 113 | (is (= @state [:started :rolled-back :stopped]))))) 114 | 115 | (deftest t-use-data 116 | (let [datasource (dummy-datasource) 117 | fixture-fn (compose-fixtures 118 | (fix/use-datasources datasource) 119 | (fix/use-data test-fixtures))] 120 | (->> (fn [] 121 | (is (every? (comp symbol? fix/id) test-docs)) 122 | (is (every? symbol? (fix/ids test-docs))) 123 | (is (= [0 1 2] (map #(fix/property % :index) test-docs))) 124 | (is (= [0 1 2] (fix/properties test-docs :index)))) 125 | (fixture-fn)))) 126 | -------------------------------------------------------------------------------- /test/fixpoint/datasource/amqp_test.clj: -------------------------------------------------------------------------------- 1 | (ns fixpoint.datasource.amqp-test 2 | (:require [clojure.test :refer :all] 3 | [kithara.rabbitmq 4 | [channel :as rch] 5 | [connection :as rc] 6 | [exchange :as re] 7 | [queue :as rq] 8 | [publish :refer [publish]]] 9 | [fixpoint.datasource.amqp :as amqp] 10 | [fixpoint.core :as fix])) 11 | 12 | ;; ## Test Datasource 13 | 14 | (def test-amqp 15 | (amqp/make-datasource :amqp {:log-level :error})) 16 | 17 | ;; ## Fixtures 18 | 19 | (use-fixtures 20 | :once 21 | (fix/use-datasources test-amqp)) 22 | 23 | ;; ## Helpers 24 | 25 | (defmacro with-connection 26 | [[sym options] & body] 27 | `(let [connection# (rc/open ~options) 28 | ~sym connection#] 29 | (try 30 | (do ~@body) 31 | (finally 32 | (rc/close connection#))))) 33 | 34 | (defn- setup-amqp! 35 | [connection] 36 | (let [ch (is (rch/open connection)) 37 | ex (is (re/declare ch "test-exchange" :topic)) 38 | qa (is (rq/declare ch "test-queue-a")) 39 | qb (is (rq/declare ch "test-queue-b"))] 40 | (rq/bind qa {:exchange "test-exchange", :routing-keys ["a"]}) 41 | (rq/bind qb {:exchange "test-exchange", :routing-keys ["b" "a"]}) 42 | {:channel ch :qa qa :qb qb})) 43 | 44 | (defn- publish-message! 45 | [channel routing-key] 46 | (->> {:exchange "test-exchange" 47 | :routing-key routing-key 48 | :body (.getBytes routing-key "UTF-8")} 49 | (publish channel)) 50 | true) 51 | 52 | (defn- get-messages! 53 | [q] 54 | (->> #(rq/get q {:auto-ack? true, :as :string}) 55 | (repeatedly 2) 56 | (mapv (juxt :routing-key :body)))) 57 | 58 | ;; ## Tests 59 | 60 | (deftest t-amqp 61 | (with-connection [connection (is (fix/raw-datasource :amqp))] 62 | (let [{:keys [channel qa qb]} (setup-amqp! connection)] 63 | (is (publish-message! channel "a")) 64 | (is (publish-message! channel "b")) 65 | (is (publish-message! channel "c")) 66 | (is (= [["a" "a"] [nil nil]] (get-messages! qa))) 67 | (is (= [["a" "a"] ["b" "b"]] (get-messages! qb)))))) 68 | -------------------------------------------------------------------------------- /test/fixpoint/datasource/elastic_test.clj: -------------------------------------------------------------------------------- 1 | (ns ^:elastic fixpoint.datasource.elastic-test 2 | (:require [clojure.test :refer :all] 3 | [qbits.spandex :as s] 4 | [fixpoint.datasource.elastic :as elastic] 5 | [fixpoint.core :as fix])) 6 | 7 | ;; ## Test Datasource 8 | 9 | (def test-es 10 | (elastic/make-datasource 11 | :es 12 | (or (System/getenv "FIXPOINT_ELASTIC_HOST") 13 | "http://docker:9200"))) 14 | 15 | ;; ## Fixtures 16 | 17 | (def ^:private +indices+ 18 | (->> [{:elastic/index :people 19 | :elastic/mapping 20 | {:person 21 | {:properties 22 | {:id {:type :long} 23 | :name {:type :string} 24 | :age {:type :long}}}}} 25 | {:elastic/index :posts 26 | :elastic/mapping 27 | {:post 28 | {:properties 29 | {:id {:type :long} 30 | :text {:type :string} 31 | :author-id {:type :string, :index :not_analyzed}}}}} 32 | {:elastic/index :facets 33 | :elastic/mapping false}] 34 | (map #(fix/on-datasource % :es)))) 35 | 36 | (defn- person 37 | [reference name age] 38 | (-> {:elastic/index :people 39 | :elastic/type :person 40 | :id (rand-int 100000) 41 | :name name 42 | :age age} 43 | (fix/as reference) 44 | (fix/on-datasource :es))) 45 | 46 | (defn- post 47 | [reference person-reference text] 48 | (-> {:elastic/index :posts 49 | :elastic/type :post 50 | :id (rand-int 100000) 51 | :text text 52 | :author-id person-reference} 53 | (fix/as reference) 54 | (fix/on-datasource :es))) 55 | 56 | (def +fixtures+ 57 | [(person :person/me "me" 27) 58 | (person :person/you "you" 29) 59 | (post :post/happy :person/me "Awesome.") 60 | (post :post/meh :person/you "Meh.") 61 | (post :post/question [:post/happy :author-id] "Do you really think so?")]) 62 | 63 | (use-fixtures 64 | :once 65 | (fix/use-datasources test-es) 66 | (fix/use-data [+indices+ +fixtures+])) 67 | 68 | ;; ## Tests 69 | 70 | (deftest t-elastic 71 | (testing "insertion data." 72 | (let [person (is (fix/property :person/me)) 73 | post (is (fix/property :post/happy))] 74 | (is (integer? (:id person))) 75 | (is (integer? (:id post))))) 76 | 77 | (testing "references." 78 | (are [post person] (= (fix/id person) (fix/property post :author-id)) 79 | :post/happy :person/me 80 | :post/meh :person/you 81 | :post/question :person/me)) 82 | 83 | (testing "datasource access." 84 | (let [es (fix/raw-datasource :es) 85 | index-name (is (elastic/index :es :people)) 86 | url (str "/" index-name "/person/_search") 87 | response (->> {:url url 88 | :method :post 89 | :body {:query {:match_all {}}}} 90 | (s/request es)) 91 | ids (->> (get-in response [:body :hits :hits]) 92 | (map :_id) 93 | (set))] 94 | (is (= (set (fix/properties [:person/me :person/you] :elastic/id)) 95 | ids)))) 96 | 97 | (testing "index declaration only." 98 | (let [index-name (is (elastic/index :es :facets)) 99 | url (str "/" index-name "/_mapping") 100 | es (fix/raw-datasource :es) 101 | response (->> {:url url 102 | :method :get 103 | :exception-handler (comp ex-data s/decode-exception)} 104 | (s/request es))] 105 | (is (= 404 (:status response)))))) 106 | -------------------------------------------------------------------------------- /test/fixpoint/datasource/hikari_test.clj: -------------------------------------------------------------------------------- 1 | (ns ^:postgresql fixpoint.datasource.hikari-test 2 | (:require [clojure.test :refer :all] 3 | [clojure.java.jdbc :as jdbc] 4 | [fixpoint.datasource 5 | [hikari :as hikari] 6 | [postgresql :as pg]] 7 | [fixpoint.core :as fix])) 8 | 9 | ;; ## Test Datasource 10 | 11 | (def test-db 12 | (-> (pg/make-datasource 13 | :test-db 14 | {:connection-uri (or (System/getenv "FIXPOINT_POSTGRESQL_URI") 15 | "jdbc:postgresql://localhost:5432/test")}) 16 | (hikari/wrap-jdbc-datasource))) 17 | 18 | ;; ## Fixtures 19 | 20 | (defn- person 21 | [reference name age] 22 | (-> {:db/table :people 23 | :name name 24 | :age age 25 | :active true} 26 | (fix/as reference) 27 | (fix/on-datasource :test-db))) 28 | 29 | (defn- post 30 | [reference person-reference text] 31 | (-> {:db/table :posts 32 | :text text 33 | :author-id person-reference} 34 | (fix/as reference) 35 | (fix/on-datasource :test-db))) 36 | 37 | (def +fixtures+ 38 | [(person :person/me "me" 27) 39 | (person :person/you "you" 29) 40 | (post :post/happy :person/me "Awesome.") 41 | (post :post/meh :person/you "Meh.") 42 | (post :post/question [:post/happy :author-id] "Do you really think so?")]) 43 | 44 | (defn- use-postgresql-setup 45 | [] 46 | (fn [f] 47 | (let [db (fix/raw-datasource :test-db)] 48 | (->> (str "create table people (" 49 | " id SERIAL PRIMARY KEY," 50 | " name VARCHAR NOT NULL," 51 | " age INT NOT NULL," 52 | " active BOOLEAN NOT NULL DEFAULT TRUE," 53 | " created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP" 54 | ")") 55 | (jdbc/execute! db)) 56 | (->> (str "create table posts (" 57 | " id SERIAL PRIMARY KEY," 58 | " author_id INTEGER NOT NULL REFERENCES people (id)," 59 | " text VARCHAR NOT NULL," 60 | " created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP" 61 | ")") 62 | (jdbc/execute! db)) 63 | (f)))) 64 | 65 | (use-fixtures 66 | :once 67 | (fix/use-datasources test-db) 68 | (use-postgresql-setup) 69 | (fix/use-data +fixtures+)) 70 | 71 | ;; ## Tests 72 | 73 | (deftest t-hikari 74 | (testing "insertion data." 75 | (let [person (is (fix/property :person/me)) 76 | post (is (fix/property :post/happy))] 77 | (is (integer? (:id person))) 78 | (is (integer? (:id post))) 79 | (is (:created-at person)) 80 | (is (:created-at post)))) 81 | 82 | (testing "references." 83 | (are [post person] (= (fix/id person) (fix/property post :author-id)) 84 | :post/happy :person/me 85 | :post/meh :person/you 86 | :post/question :person/me)) 87 | 88 | (testing "datasource access." 89 | (let [db (fix/raw-datasource :test-db) 90 | ids (->> ["select id from people order by name asc"] 91 | (jdbc/query db) 92 | (map :id))] 93 | (is (= (fix/ids [:person/me :person/you]) ids))))) 94 | -------------------------------------------------------------------------------- /test/fixpoint/datasource/mysql_test.clj: -------------------------------------------------------------------------------- 1 | (ns ^:mysql fixpoint.datasource.mysql-test 2 | (:require [clojure.test :refer :all] 3 | [clojure.java.jdbc :as jdbc] 4 | [fixpoint.datasource.mysql :as mysql] 5 | [fixpoint.core :as fix])) 6 | 7 | ;; ## Test Datasource 8 | 9 | (def test-db 10 | (mysql/make-datasource 11 | :test-db 12 | {:connection-uri (or (System/getenv "FIXPOINT_MYSQL_URI") 13 | "jdbc:mysql://localhost:3306/test?useSSL=false")})) 14 | 15 | ;; ## Fixtures 16 | 17 | (defn- person 18 | [reference name age] 19 | (-> {:db/table :people 20 | :name name 21 | :age age 22 | :active true} 23 | (fix/as reference) 24 | (fix/on-datasource :test-db))) 25 | 26 | (defn- post 27 | [reference person-reference text] 28 | (-> {:db/table :posts 29 | :text text 30 | :author-id person-reference} 31 | (fix/as reference) 32 | (fix/on-datasource :test-db))) 33 | 34 | (defn- post-with-explicit-id 35 | [reference id person-reference text] 36 | (-> {:db/table :posts 37 | :id id 38 | :text text 39 | :author-id person-reference} 40 | (fix/as reference) 41 | (fix/on-datasource :test-db))) 42 | 43 | (def +explicit-post-id+ 123456789) 44 | 45 | (def +fixtures+ 46 | [(person :person/me "me" 27) 47 | (person :person/you "you" 29) 48 | (post :post/happy :person/me "Awesome.") 49 | (post :post/question [:post/happy :author-id] "Do you really think so?") 50 | (post-with-explicit-id :post/meh +explicit-post-id+ :person/you "Meh.")]) 51 | 52 | (defn- use-mysql-setup 53 | [] 54 | (fn [f] 55 | (let [db (fix/raw-datasource :test-db)] 56 | (try 57 | (->> (str "create table people (" 58 | " id INT AUTO_INCREMENT PRIMARY KEY," 59 | " name VARCHAR(255) NOT NULL," 60 | " age INT NOT NULL," 61 | " active TINYINT NOT NULL DEFAULT TRUE," 62 | " created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP" 63 | ")") 64 | (jdbc/execute! db)) 65 | (->> (str "create table posts (" 66 | " id INT AUTO_INCREMENT PRIMARY KEY," 67 | " author_id INT NOT NULL REFERENCES people (id)," 68 | " text VARCHAR(255) NOT NULL," 69 | " created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP" 70 | ")") 71 | (jdbc/execute! db)) 72 | (f) 73 | (finally 74 | (jdbc/execute! db "drop table if exists posts") 75 | (jdbc/execute! db "drop table if exists people")))))) 76 | 77 | (use-fixtures 78 | :once 79 | (fix/use-datasources test-db) 80 | (use-mysql-setup) 81 | (fix/use-data +fixtures+)) 82 | 83 | ;; ## Tests 84 | 85 | (deftest t-mysql 86 | (testing "insertion data." 87 | (let [person (is (fix/property :person/me)) 88 | post (is (fix/property :post/happy))] 89 | (is (integer? (:id person))) 90 | (is (integer? (:id post))) 91 | (is (:created-at person)) 92 | (is (:created-at post)))) 93 | 94 | (testing "references." 95 | (are [post person] (= (fix/id person) (fix/property post :author-id)) 96 | :post/happy :person/me 97 | :post/meh :person/you 98 | :post/question :person/me)) 99 | 100 | (testing "explicitly set primary key." 101 | (is (= +explicit-post-id+ (fix/id :post/meh)))) 102 | 103 | (testing "datasource access." 104 | (let [db (fix/raw-datasource :test-db) 105 | ids (->> ["select id from people order by name asc"] 106 | (jdbc/query db) 107 | (map :id))] 108 | (is (= (fix/ids [:person/me :person/you]) ids))))) 109 | -------------------------------------------------------------------------------- /test/fixpoint/datasource/postgresql_test.clj: -------------------------------------------------------------------------------- 1 | (ns ^:postgresql fixpoint.datasource.postgresql-test 2 | (:require [clojure.test :refer :all] 3 | [clojure.java.jdbc :as jdbc] 4 | [fixpoint.datasource.postgresql :as pg] 5 | [fixpoint.core :as fix])) 6 | 7 | ;; ## Test Datasource 8 | 9 | (def test-db 10 | (pg/make-datasource 11 | :test-db 12 | {:connection-uri (or (System/getenv "FIXPOINT_POSTGRESQL_URI") 13 | "jdbc:postgresql://localhost:5432/test")})) 14 | 15 | ;; ## Fixtures 16 | 17 | (defn- person 18 | [reference name age] 19 | (-> {:db/table :people 20 | :name name 21 | :age age 22 | :active true} 23 | (fix/as reference) 24 | (fix/on-datasource :test-db))) 25 | 26 | (defn- post 27 | [reference person-reference text] 28 | (-> {:db/table :posts 29 | :text text 30 | :author-id person-reference} 31 | (fix/as reference) 32 | (fix/on-datasource :test-db))) 33 | 34 | (def +fixtures+ 35 | [(person :person/me "me" 27) 36 | (person :person/you "you" 29) 37 | (post :post/happy :person/me "Awesome.") 38 | (post :post/meh :person/you "Meh.") 39 | (post :post/question [:post/happy :author-id] "Do you really think so?")]) 40 | 41 | (defn- use-postgresql-setup 42 | [] 43 | (fn [f] 44 | (let [db (fix/raw-datasource :test-db)] 45 | (->> (str "create table people (" 46 | " id SERIAL PRIMARY KEY," 47 | " name VARCHAR NOT NULL," 48 | " age INT NOT NULL," 49 | " active BOOLEAN NOT NULL DEFAULT TRUE," 50 | " created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP" 51 | ")") 52 | (jdbc/execute! db)) 53 | (->> (str "create table posts (" 54 | " id SERIAL PRIMARY KEY," 55 | " author_id INTEGER NOT NULL REFERENCES people (id)," 56 | " text VARCHAR NOT NULL," 57 | " created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP" 58 | ")") 59 | (jdbc/execute! db)) 60 | (f)))) 61 | 62 | (use-fixtures 63 | :once 64 | (fix/use-datasources test-db) 65 | (use-postgresql-setup) 66 | (fix/use-data +fixtures+)) 67 | 68 | ;; ## Tests 69 | 70 | (deftest t-postgresql 71 | (testing "insertion data." 72 | (let [person (is (fix/property :person/me)) 73 | post (is (fix/property :post/happy))] 74 | (is (integer? (:id person))) 75 | (is (integer? (:id post))) 76 | (is (:created-at person)) 77 | (is (:created-at post)))) 78 | 79 | (testing "references." 80 | (are [post person] (= (fix/id person) (fix/property post :author-id)) 81 | :post/happy :person/me 82 | :post/meh :person/you 83 | :post/question :person/me)) 84 | 85 | (testing "datasource access." 86 | (let [db (fix/raw-datasource :test-db) 87 | ids (->> ["select id from people order by name asc"] 88 | (jdbc/query db) 89 | (map :id))] 90 | (is (= (fix/ids [:person/me :person/you]) ids))))) 91 | --------------------------------------------------------------------------------