boltless 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +8 -0
  3. data/Guardfile +44 -0
  4. data/Makefile +138 -0
  5. data/Rakefile +26 -0
  6. data/docker-compose.yml +19 -0
  7. data/lib/boltless/configuration.rb +69 -0
  8. data/lib/boltless/errors/invalid_json_error.rb +9 -0
  9. data/lib/boltless/errors/request_error.rb +24 -0
  10. data/lib/boltless/errors/response_error.rb +30 -0
  11. data/lib/boltless/errors/transaction_begin_error.rb +9 -0
  12. data/lib/boltless/errors/transaction_in_bad_state_error.rb +11 -0
  13. data/lib/boltless/errors/transaction_not_found_error.rb +11 -0
  14. data/lib/boltless/errors/transaction_rollback_error.rb +26 -0
  15. data/lib/boltless/extensions/configuration_handling.rb +37 -0
  16. data/lib/boltless/extensions/connection_pool.rb +127 -0
  17. data/lib/boltless/extensions/operations.rb +175 -0
  18. data/lib/boltless/extensions/transactions.rb +301 -0
  19. data/lib/boltless/extensions/utilities.rb +187 -0
  20. data/lib/boltless/request.rb +386 -0
  21. data/lib/boltless/result.rb +98 -0
  22. data/lib/boltless/result_row.rb +90 -0
  23. data/lib/boltless/statement_collector.rb +40 -0
  24. data/lib/boltless/transaction.rb +234 -0
  25. data/lib/boltless/version.rb +23 -0
  26. data/lib/boltless.rb +36 -0
  27. data/spec/benchmark/transfer.rb +57 -0
  28. data/spec/boltless/extensions/configuration_handling_spec.rb +39 -0
  29. data/spec/boltless/extensions/connection_pool_spec.rb +131 -0
  30. data/spec/boltless/extensions/operations_spec.rb +189 -0
  31. data/spec/boltless/extensions/transactions_spec.rb +418 -0
  32. data/spec/boltless/extensions/utilities_spec.rb +546 -0
  33. data/spec/boltless/request_spec.rb +946 -0
  34. data/spec/boltless/result_row_spec.rb +161 -0
  35. data/spec/boltless/result_spec.rb +127 -0
  36. data/spec/boltless/statement_collector_spec.rb +45 -0
  37. data/spec/boltless/transaction_spec.rb +601 -0
  38. data/spec/boltless_spec.rb +11 -0
  39. data/spec/fixtures/files/raw_result.yml +21 -0
  40. data/spec/fixtures/files/raw_result_with_graph_result.yml +48 -0
  41. data/spec/fixtures/files/raw_result_with_meta.yml +11 -0
  42. data/spec/fixtures/files/raw_result_with_stats.yml +26 -0
  43. data/spec/spec_helper.rb +89 -0
  44. metadata +384 -0
@@ -0,0 +1,546 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Boltless::Extensions::Utilities do
6
+ let(:described_class) { Boltless }
7
+
8
+ describe '.build_cypher' do
9
+ let(:action) { described_class.build_cypher(**replacements) { cypher } }
10
+
11
+ context 'without replacements' do
12
+ let(:cypher) { 'CREATE (n:User)' }
13
+ let(:replacements) { {} }
14
+
15
+ it 'returns the untouched input string' do
16
+ expect(action).to be_eql(cypher)
17
+ end
18
+ end
19
+
20
+ context 'with replacements (unbalanced)' do
21
+ let(:cypher) { 'CREATE (n:%<subject>s)' }
22
+ let(:replacements) { { object: 'test' } }
23
+
24
+ it 'raises a KeyError' do
25
+ expect { action }.to raise_error(KeyError, /subject.*not found/)
26
+ end
27
+ end
28
+
29
+ context 'with replacements (balanced, untouched string)' do
30
+ let(:cypher) { 'CREATE (n:%<subject>s)' }
31
+ let(:replacements) { { subject: 'User' } }
32
+
33
+ it 'returns the processed input string' do
34
+ expect(action).to be_eql('CREATE (n:User)')
35
+ end
36
+ end
37
+
38
+ context 'with replacements (balanced, single label symbol, +label+)' do
39
+ let(:cypher) { 'CREATE (n:%<label>s)' }
40
+ let(:replacements) { { label: :user } }
41
+
42
+ it 'returns the processed input string' do
43
+ expect(action).to be_eql('CREATE (n:User)')
44
+ end
45
+ end
46
+
47
+ context 'with replacements (balanced, single label symbol)' do
48
+ let(:cypher) { 'CREATE (n:%<subject_label>s)' }
49
+ let(:replacements) { { subject_label: :user } }
50
+
51
+ it 'returns the processed input string' do
52
+ expect(action).to be_eql('CREATE (n:User)')
53
+ end
54
+ end
55
+
56
+ context 'with replacements (balanced, multiple label symbols)' do
57
+ let(:cypher) { 'CREATE (n:%<subject_labels>s)' }
58
+ let(:replacements) { { subject_labels: %i[user customer] } }
59
+
60
+ it 'returns the processed input string' do
61
+ expect(action).to be_eql('CREATE (n:Customer:User)')
62
+ end
63
+ end
64
+
65
+ context 'with replacements (balanced, single type symbol, +type+)' do
66
+ let(:cypher) { 'CREATE (a)-[:%<type>s]->(b)' }
67
+ let(:replacements) { { type: :read } }
68
+
69
+ it 'returns the processed input string' do
70
+ expect(action).to be_eql('CREATE (a)-[:READ]->(b)')
71
+ end
72
+ end
73
+
74
+ context 'with replacements (balanced, single type symbol)' do
75
+ let(:cypher) { 'CREATE (a)-[:%<predicate_type>s]->(b)' }
76
+ let(:replacements) { { predicate_type: :read } }
77
+
78
+ it 'returns the processed input string' do
79
+ expect(action).to be_eql('CREATE (a)-[:READ]->(b)')
80
+ end
81
+ end
82
+
83
+ context 'with replacements (balanced, multiple type symbols)' do
84
+ let(:cypher) { 'CREATE (a)-[:%<predicate_types>s]->(b)' }
85
+ let(:replacements) { { predicate_types: %i[read write] } }
86
+
87
+ it 'returns the processed input string' do
88
+ expect(action).to be_eql('CREATE (a)-[:READ|WRITE]->(b)')
89
+ end
90
+ end
91
+
92
+ context 'with replacements (balanced, single string)' do
93
+ let(:cypher) { 'CREATE (n:User { name: %<name_str>s })' }
94
+ let(:replacements) { { name_str: 'Klaus' } }
95
+
96
+ it 'returns the processed input string' do
97
+ expect(action).to be_eql('CREATE (n:User { name: "Klaus" })')
98
+ end
99
+ end
100
+
101
+ context 'with replacements (balanced, multiple strings)' do
102
+ let(:cypher) { 'CREATE (n:User { states: [%<state_strs>s] })' }
103
+ let(:replacements) { { state_strs: %w[active locked] } }
104
+
105
+ it 'returns the processed input string' do
106
+ expect(action).to \
107
+ be_eql('CREATE (n:User { states: ["active", "locked"] })')
108
+ end
109
+ end
110
+
111
+ context 'without replacements, with comments' do
112
+ let(:cypher) do
113
+ <<~CYPHER
114
+ // Nice comment!
115
+ CREATE (n:User) // Yay.
116
+ // Other comment
117
+ CYPHER
118
+ end
119
+ let(:replacements) { {} }
120
+
121
+ it 'returns the processed input string' do
122
+ expect(action).to \
123
+ be_eql('CREATE (n:User)')
124
+ end
125
+ end
126
+ end
127
+
128
+ describe '.prepare_label' do
129
+ context 'with a single string' do
130
+ it 'returns the escaped string' do
131
+ expect(described_class.prepare_label('user')).to \
132
+ be_eql('User')
133
+ end
134
+ end
135
+
136
+ context 'with multiple strings' do
137
+ it 'returns the escaped string' do
138
+ expect(described_class.prepare_label('user', 'payment')).to \
139
+ be_eql('Payment:User')
140
+ end
141
+ end
142
+
143
+ context 'with nested strings' do
144
+ it 'returns the escaped string' do
145
+ res = described_class.prepare_label(['user'], [['payment']], 'session')
146
+ expect(res).to be_eql('Payment:Session:User')
147
+ end
148
+ end
149
+
150
+ context 'with some nasty strings' do
151
+ it 'returns the escaped string' do
152
+ expect(described_class.prepare_label('userv2', 'user+v2')).to \
153
+ be_eql('Userv2:`User+v2`')
154
+ end
155
+ end
156
+
157
+ context 'with an underscored string' do
158
+ it 'returns the escaped string' do
159
+ expect(described_class.prepare_label('payment_mandate')).to \
160
+ be_eql('PaymentMandate')
161
+ end
162
+ end
163
+
164
+ context 'with an camel-cased string' do
165
+ it 'returns the escaped string' do
166
+ expect(described_class.prepare_label('paymentMandate')).to \
167
+ be_eql('PaymentMandate')
168
+ end
169
+ end
170
+
171
+ context 'with an pascal-cased string' do
172
+ it 'returns the escaped string' do
173
+ expect(described_class.prepare_label('PaymentMandate')).to \
174
+ be_eql('PaymentMandate')
175
+ end
176
+ end
177
+
178
+ context 'with an underscored symbol' do
179
+ it 'returns the escaped string' do
180
+ expect(described_class.prepare_label(:payment_mandate)).to \
181
+ be_eql('PaymentMandate')
182
+ end
183
+ end
184
+
185
+ context 'with an kabab-cased string' do
186
+ it 'returns the escaped string' do
187
+ expect(described_class.prepare_label('payment-mandate')).to \
188
+ be_eql('PaymentMandate')
189
+ end
190
+ end
191
+
192
+ context 'with an boolean (false)' do
193
+ it 'returns the escaped string' do
194
+ expect(described_class.prepare_label(false)).to be_eql('False')
195
+ end
196
+ end
197
+
198
+ context 'with nil' do
199
+ it 'raises an ArgumentError' do
200
+ expect { described_class.prepare_label(nil) }.to \
201
+ raise_error(ArgumentError, /Bad labels: \[nil\]/)
202
+ end
203
+ end
204
+
205
+ context 'with mixed values' do
206
+ it 'returns the escaped string' do
207
+ res = described_class.prepare_label(
208
+ [:a], [true, [false, ['"nice"', "o'neil"], nil]]
209
+ )
210
+ expect(res).to be_eql("A:False:True:`\"nice\"`:`O\'neil`")
211
+ end
212
+ end
213
+
214
+ context 'with a set' do
215
+ it 'returns the escaped string' do
216
+ set = Set[:user, :payment, :session, true]
217
+ expect(described_class.prepare_label(set)).to \
218
+ be_eql('Payment:Session:True:User')
219
+ end
220
+ end
221
+ end
222
+
223
+ describe '.prepare_type' do
224
+ context 'with a single string' do
225
+ it 'returns the escaped string' do
226
+ expect(described_class.prepare_type('read')).to \
227
+ be_eql('READ')
228
+ end
229
+ end
230
+
231
+ context 'with multiple strings' do
232
+ it 'returns the escaped string' do
233
+ expect(described_class.prepare_type('read', 'write')).to \
234
+ be_eql('READ|WRITE')
235
+ end
236
+ end
237
+
238
+ context 'with nested strings' do
239
+ it 'returns the escaped string' do
240
+ res = described_class.prepare_type(['read'], [['write']], 'delete')
241
+ expect(res).to be_eql('DELETE|READ|WRITE')
242
+ end
243
+ end
244
+
245
+ context 'with some nasty strings' do
246
+ it 'returns the escaped string' do
247
+ expect(described_class.prepare_type('userv2', 'user+v2')).to \
248
+ be_eql('USERV2|`USER+V2`')
249
+ end
250
+ end
251
+
252
+ context 'with an underscored string' do
253
+ it 'returns the escaped string' do
254
+ expect(described_class.prepare_type('read_write')).to \
255
+ be_eql('READ_WRITE')
256
+ end
257
+ end
258
+
259
+ context 'with an camel-cased string' do
260
+ it 'returns the escaped string' do
261
+ expect(described_class.prepare_type('readWrite')).to \
262
+ be_eql('READ_WRITE')
263
+ end
264
+ end
265
+
266
+ context 'with an pascal-cased string' do
267
+ it 'returns the escaped string' do
268
+ expect(described_class.prepare_type('ReadWrite')).to \
269
+ be_eql('READ_WRITE')
270
+ end
271
+ end
272
+
273
+ context 'with an underscored symbol' do
274
+ it 'returns the escaped string' do
275
+ expect(described_class.prepare_type(:read_write)).to \
276
+ be_eql('READ_WRITE')
277
+ end
278
+ end
279
+
280
+ context 'with an kabab-cased string' do
281
+ it 'returns the escaped string' do
282
+ expect(described_class.prepare_type('read-write')).to \
283
+ be_eql('READ_WRITE')
284
+ end
285
+ end
286
+
287
+ context 'with an boolean (false)' do
288
+ it 'returns the escaped string' do
289
+ expect(described_class.prepare_type(false)).to be_eql('FALSE')
290
+ end
291
+ end
292
+
293
+ context 'with nil' do
294
+ it 'raises an ArgumentError' do
295
+ expect { described_class.prepare_type(nil) }.to \
296
+ raise_error(ArgumentError, /Bad types: \[nil\]/)
297
+ end
298
+ end
299
+
300
+ context 'with mixed values' do
301
+ it 'returns the escaped string' do
302
+ res = described_class.prepare_type(
303
+ [:a], [true, [false, ['"nice"', "o'neil"], nil], 1], 12.14
304
+ )
305
+ expect(res).to be_eql("1|A|FALSE|TRUE|`\"NICE\"`|`12.14`|`O\'NEIL`")
306
+ end
307
+ end
308
+
309
+ context 'with a set' do
310
+ it 'returns the escaped string' do
311
+ set = Set[:a, :b, :c, 1]
312
+ expect(described_class.prepare_type(set)).to be_eql('1|A|B|C')
313
+ end
314
+ end
315
+ end
316
+
317
+ describe '.prepare_string' do
318
+ context 'with a single string' do
319
+ it 'returns the escaped string' do
320
+ expect(described_class.prepare_string('super "cool" string')).to \
321
+ be_eql('"super \"cool\" string"')
322
+ end
323
+ end
324
+
325
+ context 'with multiple strings' do
326
+ it 'returns the escaped string' do
327
+ expect(described_class.prepare_string('a', 'b')).to \
328
+ be_eql('"a", "b"')
329
+ end
330
+ end
331
+
332
+ context 'with nested strings' do
333
+ it 'returns the escaped string' do
334
+ expect(described_class.prepare_string(['a'], [['b']], 'c')).to \
335
+ be_eql('"a", "b", "c"')
336
+ end
337
+ end
338
+
339
+ context 'with an integer' do
340
+ it 'returns the escaped string' do
341
+ expect(described_class.prepare_string(1)).to be_eql('"1"')
342
+ end
343
+ end
344
+
345
+ context 'with an boolean (true)' do
346
+ it 'returns the escaped string' do
347
+ expect(described_class.prepare_string(true)).to be_eql('"true"')
348
+ end
349
+ end
350
+
351
+ context 'with an boolean (false)' do
352
+ it 'returns the escaped string' do
353
+ expect(described_class.prepare_string(false)).to be_eql('"false"')
354
+ end
355
+ end
356
+
357
+ context 'with nil' do
358
+ it 'returns the escaped string' do
359
+ expect(described_class.prepare_string(nil)).to be_eql('""')
360
+ end
361
+ end
362
+
363
+ context 'with mixed values' do
364
+ it 'returns the escaped string' do
365
+ res = described_class.prepare_string(
366
+ [:a], [true, [false, ['"nice"', "o'neil"], nil], 1], 12.14
367
+ )
368
+ expect(res).to \
369
+ be_eql('"a", "true", "false", "\"nice\"", "o\'neil", "1", "12.14"')
370
+ end
371
+ end
372
+
373
+ context 'with a set' do
374
+ it 'returns the escaped string' do
375
+ set = Set[:a, :b, :c, 1]
376
+ expect(described_class.prepare_string(set)).to \
377
+ be_eql('"a", "b", "c", "1"')
378
+ end
379
+ end
380
+ end
381
+
382
+ describe '.to_options' do
383
+ let(:action) { described_class.to_options(obj) }
384
+ let(:obj) do
385
+ {
386
+ indexProvider: 'lucene+native-3.0',
387
+ indexConfig: {
388
+ 'spatial.wgs-84.min': [-100.0, -80.0],
389
+ 'spatial.wgs-84.max': [100.0, 80.0]
390
+ }
391
+ }
392
+ end
393
+ let(:cypher_opts) do
394
+ "{ `indexProvider`: 'lucene+native-3.0', " \
395
+ '`indexConfig`: { `spatial.wgs-84.min`: [ -100.0, -80.0 ], ' \
396
+ '`spatial.wgs-84.max`: [ 100.0, 80.0 ] } }'
397
+ end
398
+
399
+ context 'with a String' do
400
+ let(:obj) { 'test' }
401
+
402
+ it 'returns the Cypher options representation' do
403
+ expect(action).to be_eql(%('test'))
404
+ end
405
+ end
406
+
407
+ context 'with a flat Array' do
408
+ let(:obj) { ['test', 1.9235, 2, true, nil] }
409
+
410
+ it 'returns the Cypher options representation' do
411
+ expect(action).to be_eql(%([ 'test', 1.9235, 2, true ]))
412
+ end
413
+ end
414
+
415
+ context 'with a flat Hash' do
416
+ let(:obj) { { symbol: true, 'string' => 1.234 } }
417
+
418
+ it 'returns the Cypher options representation' do
419
+ expect(action).to be_eql(%({ `symbol`: true, `string`: 1.234 }))
420
+ end
421
+ end
422
+
423
+ context 'with a complex nested structure' do
424
+ it 'returns the Cypher options representation' do
425
+ expect(action).to be_eql(cypher_opts)
426
+ end
427
+ end
428
+ end
429
+
430
+ describe '.resolve_cypher' do
431
+ let(:action) { described_class.resolve_cypher(cypher, **args) }
432
+
433
+ describe 'Cypher without parameters and none given' do
434
+ let(:cypher) { 'CREATE (n:User)' }
435
+ let(:args) { {} }
436
+
437
+ it 'returns the untouched input string' do
438
+ expect(action).to be_eql(cypher)
439
+ end
440
+ end
441
+
442
+ describe 'Cypher with parameters and none given' do
443
+ let(:cypher) { 'CREATE (n:User { name: $name })' }
444
+ let(:args) { {} }
445
+
446
+ it 'returns the untouched input string' do
447
+ expect(action).to be_eql(cypher)
448
+ end
449
+ end
450
+
451
+ describe 'Cypher with parameters and parameters given (balanced)' do
452
+ let(:cypher) { 'CREATE (n:User { name: $name })' }
453
+ let(:args) { { name: 'Peter' } }
454
+
455
+ it 'returns the resolved string' do
456
+ expect(action).to be_eql('CREATE (n:User { name: "Peter" })')
457
+ end
458
+ end
459
+
460
+ describe 'Cypher with parameters and parameters given (unbalanced)' do
461
+ let(:cypher) { 'CREATE (n:User { name: $name, email: $email })' }
462
+ let(:args) { { email: 'peter@example.com' } }
463
+
464
+ it 'returns the resolved string' do
465
+ expect(action).to \
466
+ be_eql('CREATE (n:User { name: $name, email: "peter@example.com" })')
467
+ end
468
+ end
469
+ end
470
+
471
+ describe '.cypher_logging_color' do
472
+ let(:action) { described_class.cypher_logging_color(cypher) }
473
+
474
+ context 'with a transaction begin statement' do
475
+ let(:cypher) { 'BEGIN' }
476
+
477
+ it 'returns magenta' do
478
+ expect(action).to be_eql(:magenta)
479
+ end
480
+ end
481
+
482
+ context 'with a create statement' do
483
+ let(:cypher) { 'CREATE (n:User { name: $name })' }
484
+
485
+ it 'returns green' do
486
+ expect(action).to be_eql(:green)
487
+ end
488
+ end
489
+
490
+ context 'with a merge statement' do
491
+ let(:cypher) { 'MERGE (n:User { name: $name })' }
492
+
493
+ it 'returns yellow' do
494
+ expect(action).to be_eql(:yellow)
495
+ end
496
+ end
497
+
498
+ context 'with a match/set statement' do
499
+ let(:cypher) { 'MATCH (n:User { name: $name }) SET n.email = $email' }
500
+
501
+ it 'returns yellow' do
502
+ expect(action).to be_eql(:yellow)
503
+ end
504
+ end
505
+
506
+ context 'with a match/delete statement' do
507
+ let(:cypher) { 'MATCH (n:User { name: $name }) DELETE n' }
508
+
509
+ it 'returns red' do
510
+ expect(action).to be_eql(:red)
511
+ end
512
+ end
513
+
514
+ context 'with a match/remove statement' do
515
+ let(:cypher) { 'MATCH (n:User { name: $name }) REMOVE n.email' }
516
+
517
+ it 'returns red' do
518
+ expect(action).to be_eql(:red)
519
+ end
520
+ end
521
+
522
+ context 'with a transaction commit statement' do
523
+ let(:cypher) { 'COMMIT' }
524
+
525
+ it 'returns green' do
526
+ expect(action).to be_eql(:green)
527
+ end
528
+ end
529
+
530
+ context 'with a transaction rollback statement' do
531
+ let(:cypher) { 'ROLLBACK' }
532
+
533
+ it 'returns red' do
534
+ expect(action).to be_eql(:red)
535
+ end
536
+ end
537
+
538
+ context 'with a match statement' do
539
+ let(:cypher) { 'MATCH (n:User { name: $name }) RETURN n.email' }
540
+
541
+ it 'returns light blue' do
542
+ expect(action).to be_eql(:light_blue)
543
+ end
544
+ end
545
+ end
546
+ end