├── .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 | [](https://travis-ci.org/stylefruits/fixpoint)
7 | [](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)
stop-datasource (stop-datasource this)
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)
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 wrap-jdbc-datasource (wrap-jdbc-datasource jdbc-datasource & [pool-options])
--------------------------------------------------------------------------------
/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+=""+t(e)+">"}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 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 Fixture Functions and Protocols
Public variables and functions:
ElasticSearch Datasource Component
Public variables and functions:
Public variables and functions:
Generic JDBC datasource component.
Public variables and functions:
MySQL JDBC Datasource Component
Public variables and functions:
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 |
--------------------------------------------------------------------------------