contracted_value 0.1.0.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,680 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ ::RSpec.describe ::ContractedValue::Value do
6
+
7
+ shared_examples_for "attribute declaration" do
8
+ let(:value_class) do
9
+ raise "`value_class` absent"
10
+ end
11
+
12
+ example "does not raise error when NOT declaring any attribute" do
13
+ expect(->{ value_class }).to_not raise_error
14
+ end
15
+
16
+ example "does not raise error when declaring 1 attribute" do
17
+ expect(
18
+ ->{
19
+ value_class.class_eval do
20
+ attribute(:attribute_1)
21
+ end
22
+ },
23
+ ).to_not raise_error
24
+ end
25
+
26
+ example "does not raise error when declaring N attributes with different names" do
27
+ expect(
28
+ ->{
29
+ value_class.class_eval do
30
+ attribute(:attribute_1)
31
+ attribute(:attribute_2)
32
+ end
33
+ },
34
+ ).to_not raise_error
35
+ end
36
+
37
+ example "does raise error when declaring N attributes with the same name" do
38
+ expect(
39
+ ->{
40
+ value_class.class_eval do
41
+ attribute(:attribute_1)
42
+ attribute(:attribute_2)
43
+ attribute(:attribute_1)
44
+ end
45
+ },
46
+ ).to raise_error(::ContractedValue::Errors::DuplicateAttributeDeclaration)
47
+ end
48
+ end
49
+
50
+
51
+ describe "attribute declaration" do
52
+ it_behaves_like "attribute declaration" do
53
+ let(:value_class) do
54
+ Class.new(described_class)
55
+ end
56
+ end
57
+ end
58
+
59
+ describe "attribute assignment" do
60
+ context "on object initialization" do
61
+ context "with class with no attribute declared with contract" do
62
+ let(:value_class) do
63
+ Class.new(described_class) do
64
+ attribute(:attribute_1)
65
+ attribute(:attribute_2)
66
+ end
67
+ end
68
+
69
+ let(:default_inputs) do
70
+ {
71
+ attribute_1: 1,
72
+ attribute_2: 1,
73
+ }
74
+ end
75
+
76
+ let(:non_hash) do
77
+ []
78
+ end
79
+
80
+ it "does raise error when input is not a hash" do
81
+ aggregate_failures do
82
+ expect(
83
+ ->{
84
+ value_class.new(
85
+ non_hash,
86
+ )
87
+ },
88
+ ).to raise_error(::ContractedValue::Errors::InvalidInputType)
89
+ end
90
+ end
91
+
92
+ it "does not raise error when input is a hash" do
93
+ aggregate_failures do
94
+ expect(
95
+ ->{
96
+ value_class.new(
97
+ default_inputs,
98
+ )
99
+ },
100
+ ).to_not raise_error
101
+ end
102
+ end
103
+
104
+ it "does not raise error when input is a value" do
105
+ aggregate_failures do
106
+ expect(
107
+ ->{
108
+ value_class.new(
109
+ value_class.new(
110
+ default_inputs,
111
+ ),
112
+ )
113
+ },
114
+ ).to_not raise_error
115
+ end
116
+ end
117
+
118
+ it "does raise error when value of any declared attribute missing from input" do
119
+ aggregate_failures do
120
+ [
121
+ :attribute_1,
122
+ :attribute_2,
123
+ ].each do |attr_name|
124
+ expect(
125
+ ->{
126
+ value_class.new(
127
+ default_inputs.dup.tap{|h| h.delete(attr_name)},
128
+ )
129
+ },
130
+ ).to raise_error(::ContractedValue::Errors::MissingAttributeInput)
131
+ end
132
+ end
133
+ end
134
+
135
+ it "does not raise error when values of all declared attributes are present, even they are all nil" do
136
+ aggregate_failures do
137
+ [
138
+ :attribute_1,
139
+ :attribute_2,
140
+ ].each do |attr_name|
141
+ expect(
142
+ ->{
143
+ value_class.new(
144
+ default_inputs.each_with_object({}) do |(k, _v), h|
145
+ h[k] = nil
146
+ end
147
+ )
148
+ },
149
+ ).to_not raise_error
150
+ end
151
+ end
152
+ end
153
+ end
154
+
155
+ context "with class with some attributes declared with contract" do
156
+ let(:value_class) do
157
+ Class.new(described_class) do
158
+ attribute(
159
+ :attribute_with_contract_1,
160
+ contract: ::Contracts::Builtin::And[::String, ::Contracts::Builtin::Not[::Contracts::Builtin::Send[:empty?]]],
161
+ )
162
+ attribute(
163
+ :attribute_with_contract_2,
164
+ contract: ::Contracts::Builtin::NatPos,
165
+ )
166
+ end
167
+ end
168
+
169
+ let(:default_inputs) do
170
+ {
171
+ attribute_with_contract_1: "yo",
172
+ attribute_with_contract_2: 1,
173
+ }
174
+ end
175
+
176
+
177
+ it "does not raise error when all values are valid according to contracts" do
178
+ expect(
179
+ ->{
180
+ value_class.new(
181
+ default_inputs
182
+ )
183
+ },
184
+ ).to_not raise_error
185
+ end
186
+
187
+ it "does raise error when any value is invalid according to contracts" do
188
+ aggregate_failures do
189
+ expect(
190
+ ->{
191
+ value_class.new(
192
+ default_inputs.merge(
193
+ attribute_with_contract_1: "",
194
+ ),
195
+ )
196
+ },
197
+ ).to raise_error(::ContractedValue::Errors::InvalidAttributeValue)
198
+
199
+ expect(
200
+ ->{
201
+ value_class.new(
202
+ default_inputs.merge(
203
+ attribute_with_contract_2: 0,
204
+ ),
205
+ )
206
+ },
207
+ ).to raise_error(::ContractedValue::Errors::InvalidAttributeValue)
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
213
+
214
+ describe "attribute input value freezing" do
215
+ let(:value_object) do
216
+ value_class.new(
217
+ a_hash: hash_as_input,
218
+ )
219
+ end
220
+
221
+ let(:hash_as_input) do
222
+ {
223
+ hash: hash_as_deep_nested_content,
224
+ }
225
+ end
226
+
227
+ let(:hash_as_deep_nested_content) do
228
+ ::Hash.new
229
+ end
230
+
231
+ shared_examples "attribute deeply frozen" do
232
+ it "does freeze the new object" do
233
+ expect(value_object).to be_frozen
234
+ end
235
+
236
+ it "does freeze the inputs" do
237
+ # Create it just before expectation
238
+ value_object
239
+
240
+ expect(
241
+ ->{
242
+ hash_as_input[:a] = nil
243
+ },
244
+ ).to raise_error(::RuntimeError, /can't modify frozen/)
245
+ end
246
+
247
+ it "does deeply freeze the inputs" do
248
+ # Create it just before expectation
249
+ value_object
250
+
251
+ expect(
252
+ ->{
253
+ hash_as_deep_nested_content[:a] = nil
254
+ },
255
+ ).to raise_error(::RuntimeError, /can't modify frozen/)
256
+ end
257
+ end
258
+
259
+ shared_examples "attribute shallowly frozen" do
260
+ it "does freeze the new object" do
261
+ expect(value_object).to be_frozen
262
+ end
263
+
264
+ it "does freeze the inputs" do
265
+ # Create it just before expectation
266
+ value_object
267
+
268
+ expect(
269
+ ->{
270
+ hash_as_input[:a] = nil
271
+ },
272
+ ).to raise_error(::RuntimeError, /can't modify frozen/)
273
+ end
274
+
275
+ it "does not deeply freeze the inputs" do
276
+ # Create it just before expectation
277
+ value_object
278
+
279
+ expect(
280
+ ->{
281
+ hash_as_deep_nested_content[:a] = nil
282
+ },
283
+ ).to_not raise_error
284
+ end
285
+ end
286
+
287
+ shared_examples "attribute not frozen" do
288
+ it "does freeze the new object" do
289
+ expect(value_object).to be_frozen
290
+ end
291
+
292
+ it "does not freeze the inputs" do
293
+ # Create it just before expectation
294
+ value_object
295
+
296
+ expect(
297
+ ->{
298
+ hash_as_input[:a] = nil
299
+ },
300
+ ).to_not raise_error
301
+ end
302
+
303
+ it "does not deeply freeze the inputs" do
304
+ # Create it just before expectation
305
+ value_object
306
+
307
+ expect(
308
+ ->{
309
+ hash_as_deep_nested_content[:a] = nil
310
+ },
311
+ ).to_not raise_error
312
+ end
313
+ end
314
+
315
+ context "when an attribute is declared without `refrigeration_mode` option" do
316
+ let(:value_class) do
317
+ Class.new(described_class) do
318
+ attribute(:a_hash)
319
+ end
320
+ end
321
+
322
+ it_behaves_like "attribute deeply frozen"
323
+ end
324
+
325
+ context "when an attribute is declared with `refrigeration_mode` option (:deep)" do
326
+ let(:value_class) do
327
+ Class.new(described_class) do
328
+ attribute(
329
+ :a_hash,
330
+ refrigeration_mode: :deep,
331
+ )
332
+ end
333
+ end
334
+
335
+ it_behaves_like "attribute deeply frozen"
336
+ end
337
+
338
+ context "when an attribute is declared with `refrigeration_mode` option (:shallow)" do
339
+ let(:value_class) do
340
+ Class.new(described_class) do
341
+ attribute(
342
+ :a_hash,
343
+ refrigeration_mode: :shallow,
344
+ )
345
+ end
346
+ end
347
+
348
+ it_behaves_like "attribute shallowly frozen"
349
+ end
350
+
351
+ context "when an attribute is declared with `refrigeration_mode` option (:none)" do
352
+ let(:value_class) do
353
+ Class.new(described_class) do
354
+ attribute(
355
+ :a_hash,
356
+ refrigeration_mode: :none,
357
+ )
358
+ end
359
+ end
360
+
361
+ it_behaves_like "attribute not frozen"
362
+ end
363
+
364
+ context "when an attribute is declared with invalid `refrigeration_mode` option" do
365
+ let(:value_class) do
366
+ Class.new(described_class) do
367
+ attribute(
368
+ :a_hash,
369
+ refrigeration_mode: :meow,
370
+ )
371
+ end
372
+ end
373
+
374
+ it "raises error on attribute declaration" do
375
+ expect do
376
+ value_class
377
+ end.to raise_error(::ContractedValue::Errors::InvalidRefrigerationMode)
378
+ end
379
+ end
380
+ end
381
+
382
+ describe "attribute default value" do
383
+ context "when with option :default_value" do
384
+ let(:value_class) do
385
+ # Workaround strange issue that
386
+ # `let` cannot be used in class block
387
+ default_val = default_value
388
+ attr_contract = attribute_contract
389
+
390
+ Class.new(described_class) do
391
+ attribute(
392
+ :attr_with_default,
393
+ default_value: default_val,
394
+ contract: attr_contract,
395
+ )
396
+ end
397
+ end
398
+
399
+ let(:default_value) do
400
+ 1
401
+ end
402
+
403
+ let(:attribute_contract) do
404
+ ::Contracts::Builtin::Any
405
+ end
406
+
407
+ shared_examples "attribute with default value" do
408
+ it "returns default value when nothing provided" do
409
+ expect(
410
+ value_class.new.attr_with_default,
411
+ ).to eq(default_value)
412
+ end
413
+
414
+ it "returns provided value when something provided" do
415
+ [
416
+ nil,
417
+ :wut,
418
+ -> { },
419
+ ].each do |provided_value|
420
+ aggregate_failures do
421
+ expect(
422
+ value_class.new(
423
+ attr_with_default: provided_value,
424
+ ).attr_with_default,
425
+ ).to eq(provided_value)
426
+ end
427
+ end
428
+ end
429
+ end
430
+
431
+ context "and default value provided is `nil`" do
432
+ let(:default_value) do
433
+ nil
434
+ end
435
+
436
+ it_behaves_like "attribute with default value"
437
+
438
+ context "when default value violates contract" do
439
+ let(:attribute_contract) do
440
+ ::Contracts::Builtin::Not[nil]
441
+ end
442
+
443
+ it "does raise error on attribute declaration" do
444
+ expect do
445
+ value_class
446
+ end.to raise_error(::ContractedValue::Errors::InvalidAttributeDefaultValue)
447
+ end
448
+ end
449
+ end
450
+
451
+ context "and default value provided is not `nil`" do
452
+ let(:default_value) do
453
+ 123
454
+ end
455
+
456
+ it_behaves_like "attribute with default value"
457
+
458
+ context "when default value violates contract" do
459
+ let(:attribute_contract) do
460
+ ::Symbol
461
+ end
462
+
463
+ it "does raise error on attribute declaration" do
464
+ expect do
465
+ value_class
466
+ end.to raise_error(::ContractedValue::Errors::InvalidAttributeDefaultValue)
467
+ end
468
+ end
469
+ end
470
+
471
+ context "and default value provided is mutable" do
472
+
473
+ context "as a string" do
474
+ let(:default_value) do
475
+ "PikaPika"
476
+ end
477
+
478
+ it_behaves_like "attribute with default value"
479
+
480
+ it "returns default value as frozen" do
481
+ expect(
482
+ value_class.new.attr_with_default,
483
+ ).to be_frozen
484
+ end
485
+ end
486
+
487
+ context "as a hash" do
488
+ let(:default_value) do
489
+ {a: {b: :c}}
490
+ end
491
+
492
+ it_behaves_like "attribute with default value"
493
+
494
+ it "returns default value as frozen" do
495
+ aggregate_failures do
496
+ val = value_class.new.attr_with_default.fetch(:a)
497
+
498
+ expect(val).to eq({b: :c})
499
+ expect(val).to be_frozen
500
+ end
501
+ end
502
+ end
503
+
504
+ end
505
+ end
506
+ end
507
+
508
+ describe "sub-classes of a value class" do
509
+
510
+ describe "attribute declaration" do
511
+ let(:parent_value_class) do
512
+ Class.new(described_class)
513
+ end
514
+
515
+ it_behaves_like "attribute declaration" do
516
+ let(:value_class) do
517
+ Class.new(parent_value_class)
518
+ end
519
+ end
520
+
521
+ describe "usage of parent class attribute" do
522
+
523
+ let(:parent_value_class) do
524
+ Class.new(described_class).tap do |klass|
525
+ klass.class_eval do
526
+ # Too lazy to include parent attributes in all examples
527
+ attribute(:attribute_1)
528
+ end
529
+ end
530
+ end
531
+ let(:child_value_class) do
532
+ Class.new(parent_value_class)
533
+ end
534
+
535
+ example "does not raise error" do
536
+ expect(
537
+ ->{
538
+ child_value_class.new(attribute_1: "wut")
539
+ },
540
+ ).to_not raise_error
541
+ end
542
+
543
+ end
544
+
545
+ describe "for new attributes absent in parent class" do
546
+
547
+ let(:parent_value_class) do
548
+ Class.new(described_class).tap do |klass|
549
+ klass.class_eval do
550
+ # Too lazy to include parent attributes in all examples
551
+ attribute(:attribute_1, default_value: nil)
552
+ attribute(:attribute_2, default_value: nil)
553
+ end
554
+ end
555
+ end
556
+
557
+ let(:child_value_class) do
558
+ Class.new(parent_value_class)
559
+ end
560
+
561
+ example "does not raise error when declaring 1 new attribute" do
562
+ expect(
563
+ ->{
564
+ child_value_class.class_eval do
565
+ attribute(:attribute_3)
566
+ end
567
+ },
568
+ ).to_not raise_error
569
+ end
570
+
571
+ example "does not raise error when declaring N attributes with different names" do
572
+ expect(
573
+ ->{
574
+ child_value_class.class_eval do
575
+ attribute(:attribute_3)
576
+ attribute(:attribute_4)
577
+ end
578
+ },
579
+ ).to_not raise_error
580
+ end
581
+
582
+ example "does raise error when declaring N attributes with the same name" do
583
+ expect(
584
+ ->{
585
+ child_value_class.class_eval do
586
+ attribute(:attribute_3)
587
+ attribute(:attribute_4)
588
+ attribute(:attribute_3)
589
+ end
590
+ },
591
+ ).to raise_error(::ContractedValue::Errors::DuplicateAttributeDeclaration)
592
+ end
593
+
594
+ end
595
+ end
596
+
597
+ describe "attribute redeclaration" do
598
+
599
+ let(:child_value_class) do
600
+ Class.new(parent_value_class)
601
+ end
602
+
603
+ describe "for existing attributes from parent class" do
604
+ let(:parent_value_class) do
605
+ Class.new(described_class).tap do |klass|
606
+ klass.class_eval do
607
+ attribute(:attribute_1)
608
+ attribute(:attribute_2)
609
+ end
610
+ end
611
+ end
612
+ it_behaves_like "attribute declaration" do
613
+ let(:value_class) do
614
+ child_value_class
615
+ end
616
+ end
617
+ end
618
+
619
+ describe "for existing attributes from parent class" do
620
+
621
+ let(:parent_value_class) do
622
+ Class.new(described_class).tap do |klass|
623
+ klass.class_eval do
624
+ attribute(
625
+ :attribute_1,
626
+ contract: ::String,
627
+ refrigeration_mode: :deep,
628
+ # `default_value` not specified on purpose
629
+ )
630
+ end
631
+ end
632
+ end
633
+
634
+ example "does not raise error when declaring existing attribute with different contract" do
635
+ expect(
636
+ ->{
637
+ child_value_class.class_eval do
638
+ attribute(
639
+ :attribute_1,
640
+ contract: ::Contracts::Builtin::NatPos
641
+ )
642
+ end
643
+ child_value_class.new(attribute_1: "")
644
+ },
645
+ ).to raise_error(::ContractedValue::Errors::InvalidAttributeValue)
646
+ end
647
+
648
+ example "does not raise error when declaring existing attribute with different default_value" do
649
+ expect(
650
+ ->{
651
+ child_value_class.class_eval do
652
+ attribute(
653
+ :attribute_1,
654
+ default_value: nil,
655
+ )
656
+ end
657
+ child_value_class.new
658
+ },
659
+ ).to_not raise_error
660
+ end
661
+
662
+ example "does not raise error when declaring existing attribute with different refrigeration_mode" do
663
+ child_value_class.class_eval do
664
+ attribute(
665
+ :attribute_1,
666
+ refrigeration_mode: :none,
667
+ )
668
+ end
669
+ value_object = child_value_class.new(attribute_1: String.new)
670
+ expect(value_object).to be_frozen
671
+ expect(value_object.attribute_1).to_not be_frozen
672
+ end
673
+
674
+ end
675
+
676
+ end
677
+
678
+ end
679
+
680
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ ::RSpec.describe ::ContractedValue do
6
+
7
+ it "has a version number" do
8
+ expect(described_class::VERSION).not_to eq(nil)
9
+ end
10
+
11
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift(File.expand_path("../../lib", __FILE__))
4
+ require "contracted_value"
5
+
6
+ if ENV["TRAVIS"]
7
+ require "simplecov"
8
+ SimpleCov.start
9
+
10
+ require "codecov"
11
+ SimpleCov.formatter = SimpleCov::Formatter::Codecov
12
+ end
13
+
14
+ require "rspec"
15
+ require "rspec/its"