deimos-ruby 2.6.0.pre.beta1 → 2.6.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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -2
  3. data/README.md +45 -20
  4. data/deimos-ruby.gemspec +0 -1
  5. data/docs/CONFIGURATION.md +5 -11
  6. data/lib/deimos/active_record_consume/batch_consumption.rb +128 -31
  7. data/lib/deimos/active_record_consume/batch_record.rb +3 -0
  8. data/lib/deimos/active_record_consume/message_consumption.rb +2 -2
  9. data/lib/deimos/active_record_consumer.rb +6 -1
  10. data/lib/deimos/active_record_producer.rb +1 -1
  11. data/lib/deimos/config/configuration.rb +11 -34
  12. data/lib/deimos/exceptions.rb +17 -0
  13. data/lib/deimos/ext/consumer_route.rb +3 -1
  14. data/lib/deimos/ext/producer_middleware.rb +2 -2
  15. data/lib/deimos/logging.rb +0 -9
  16. data/lib/deimos/producer.rb +5 -5
  17. data/lib/deimos/schema_backends/avro_base.rb +36 -7
  18. data/lib/deimos/schema_class/base.rb +62 -0
  19. data/lib/deimos/schema_class/enum.rb +49 -0
  20. data/lib/deimos/schema_class/record.rb +100 -0
  21. data/lib/deimos/transcoder.rb +6 -6
  22. data/lib/deimos/utils/deadlock_retry.rb +16 -2
  23. data/lib/deimos/utils/schema_class.rb +47 -14
  24. data/lib/deimos/version.rb +1 -1
  25. data/lib/deimos.rb +4 -3
  26. data/lib/generators/deimos/schema_class/templates/schema_class.rb.tt +8 -0
  27. data/lib/generators/deimos/schema_class/templates/schema_enum.rb.tt +13 -0
  28. data/lib/generators/deimos/schema_class/templates/schema_record.rb.tt +102 -0
  29. data/lib/generators/deimos/schema_class_generator.rb +369 -0
  30. data/lib/tasks/deimos.rake +4 -16
  31. data/regenerate_test_schema_classes.rb +3 -12
  32. data/spec/active_record_batch_consumer_association_spec.rb +1 -1
  33. data/spec/active_record_batch_consumer_spec.rb +286 -1
  34. data/spec/active_record_consume/batch_consumption_spec.rb +1 -1
  35. data/spec/active_record_consumer_spec.rb +3 -3
  36. data/spec/active_record_producer_spec.rb +1 -1
  37. data/spec/batch_consumer_spec.rb +2 -2
  38. data/spec/consumer_spec.rb +5 -5
  39. data/spec/generators/schema_class/my_schema_spec.rb +16 -0
  40. data/spec/generators/schema_class/my_schema_with_circular_reference_spec.rb +98 -0
  41. data/spec/generators/schema_class/my_schema_with_complex_types_spec.rb +237 -0
  42. data/spec/generators/schema_class_generator_spec.rb +283 -0
  43. data/spec/schemas/my_namespace/generated.rb +4 -4
  44. data/spec/schemas/my_namespace/my_long_namespace_schema.rb +2 -2
  45. data/spec/schemas/my_namespace/my_nested_schema.rb +3 -3
  46. data/spec/schemas/my_namespace/my_schema.rb +2 -2
  47. data/spec/schemas/my_namespace/my_schema_compound_key.rb +2 -2
  48. data/spec/schemas/my_namespace/my_schema_id_key.rb +2 -2
  49. data/spec/schemas/my_namespace/my_schema_key.rb +2 -2
  50. data/spec/schemas/my_namespace/my_schema_with_boolean.rb +2 -2
  51. data/spec/schemas/my_namespace/my_schema_with_circular_reference.rb +3 -3
  52. data/spec/schemas/my_namespace/my_schema_with_complex_type.rb +6 -6
  53. data/spec/schemas/my_namespace/my_schema_with_date_time.rb +2 -2
  54. data/spec/schemas/my_namespace/my_schema_with_id.rb +2 -2
  55. data/spec/schemas/my_namespace/my_schema_with_title.rb +2 -2
  56. data/spec/schemas/my_namespace/my_schema_with_union_type.rb +6 -6
  57. data/spec/schemas/my_namespace/my_schema_with_unique_id.rb +2 -2
  58. data/spec/schemas/my_namespace/my_updated_schema.rb +1 -1
  59. data/spec/schemas/my_namespace/request/create_topic.rb +2 -2
  60. data/spec/schemas/my_namespace/request/index.rb +2 -2
  61. data/spec/schemas/my_namespace/request/update_request.rb +2 -2
  62. data/spec/schemas/my_namespace/response/create_topic.rb +2 -2
  63. data/spec/schemas/my_namespace/response/index.rb +2 -2
  64. data/spec/schemas/my_namespace/response/update_response.rb +2 -2
  65. data/spec/schemas/my_namespace/wibble.rb +2 -2
  66. data/spec/schemas/my_namespace/widget.rb +2 -2
  67. data/spec/schemas/my_namespace/widget_the_second.rb +2 -2
  68. data/spec/schemas/my_namespace/widget_the_third.rb +2 -2
  69. data/spec/snapshots/consumers-no-nest.snap +1740 -0
  70. data/spec/snapshots/consumers.snap +1720 -0
  71. data/spec/snapshots/consumers_and_producers-no-nest.snap +1740 -0
  72. data/spec/snapshots/consumers_and_producers.snap +1720 -0
  73. data/spec/snapshots/consumers_circular-no-nest.snap +1740 -0
  74. data/spec/snapshots/consumers_circular.snap +1720 -0
  75. data/spec/snapshots/consumers_complex_types-no-nest.snap +1740 -0
  76. data/spec/snapshots/consumers_complex_types.snap +1720 -0
  77. data/spec/snapshots/consumers_nested-no-nest.snap +1740 -0
  78. data/spec/snapshots/consumers_nested.snap +1720 -0
  79. data/spec/snapshots/namespace_folders.snap +1800 -0
  80. data/spec/snapshots/namespace_map.snap +1800 -0
  81. data/spec/snapshots/producers_with_key-no-nest.snap +1740 -0
  82. data/spec/snapshots/producers_with_key.snap +1720 -0
  83. data/spec/spec_helper.rb +2 -2
  84. data/spec/utils/db_poller_spec.rb +1 -1
  85. data/spec/utils/deadlock_retry_spec.rb +57 -0
  86. metadata +27 -18
  87. data/lib/deimos/schema_class.rb +0 -72
  88. data/spec/schema_class_spec.rb +0 -65
@@ -86,7 +86,7 @@ module ActiveRecordBatchConsumerTest
86
86
  before(:each) do
87
87
  Deimos.configure do |config|
88
88
  config.schema.use_schema_classes = use_schema_classes
89
- config.avrogen.use_full_namespace = true
89
+ config.schema.use_full_namespace = true
90
90
  end
91
91
  end
92
92
 
@@ -460,6 +460,291 @@ module ActiveRecordBatchConsumerTest
460
460
  end
461
461
  end
462
462
 
463
+ describe 'individual fallback when a batch operation fails' do
464
+ # `test_id: ''` passes Avro validation (it's a string) but fails the model's
465
+ # `validates :test_id, presence: true`, so `MassUpdater` raises before it issues any SQL.
466
+ # That's the poison-message case: one record that can never be persisted.
467
+ let(:poison_payload) { { test_id: '', some_int: 3 } }
468
+
469
+ let(:consumer_class) do
470
+ Class.new(described_class) do
471
+ record_class Widget
472
+ compacted false
473
+ end
474
+ end
475
+
476
+ before(:each) do
477
+ register_consumer(consumer_class, 'MySchema',
478
+ key_config: { plain: true },
479
+ configs: { reraise_errors: true })
480
+ end
481
+
482
+ it 'should not raise when the batch succeeds' do
483
+ publish_batch(
484
+ [
485
+ { key: 1, payload: { test_id: 'abc', some_int: 1 } },
486
+ { key: 2, payload: { test_id: 'def', some_int: 2 } }
487
+ ]
488
+ )
489
+
490
+ expect(all_widgets).
491
+ to contain_exactly(have_attributes(id: 1, test_id: 'abc'),
492
+ have_attributes(id: 2, test_id: 'def'))
493
+ end
494
+
495
+ context 'when one record in the batch cannot be persisted' do
496
+
497
+ it 'should save every record except the bad one and report the failed key' do
498
+ expect {
499
+ publish_batch(
500
+ [
501
+ { key: 1, payload: { test_id: 'abc', some_int: 1 } },
502
+ { key: 2, payload: poison_payload },
503
+ { key: 3, payload: { test_id: 'ghi', some_int: 3 } }
504
+ ]
505
+ )
506
+ }.to raise_error(Deimos::BatchFallbackError, /Failed keys: "2"/)
507
+
508
+ expect(all_widgets).
509
+ to contain_exactly(have_attributes(id: 1, test_id: 'abc', some_int: 1),
510
+ have_attributes(id: 3, test_id: 'ghi', some_int: 3))
511
+ end
512
+
513
+ it 'should collect the failures of every bad record in the batch' do
514
+ expect {
515
+ publish_batch(
516
+ [
517
+ { key: 1, payload: poison_payload },
518
+ { key: 2, payload: { test_id: 'def', some_int: 2 } },
519
+ { key: 3, payload: poison_payload }
520
+ ]
521
+ )
522
+ }.to raise_error(Deimos::BatchFallbackError) { |error|
523
+ expect(error.failures.map { |message, _| message.key }).to contain_exactly('1', '3')
524
+ expect(error.failures.map(&:last)).to all(be_a(ActiveRecord::RecordInvalid))
525
+ }
526
+
527
+ expect(all_widgets).to contain_exactly(have_attributes(id: 2, test_id: 'def'))
528
+ end
529
+
530
+ it 'should raise the original error when no record could be saved on its own' do
531
+ # Isolating salvaged nothing, so the failure was never about one bad message. The
532
+ # original error is more useful than a BatchFallbackError listing every key.
533
+ expect {
534
+ publish_batch(
535
+ [
536
+ { key: 1, payload: poison_payload },
537
+ { key: 2, payload: poison_payload }
538
+ ]
539
+ )
540
+ }.to raise_error(ActiveRecord::RecordInvalid)
541
+
542
+ expect(all_widgets).to be_empty
543
+ end
544
+
545
+ it 'should emit an instrumentation event naming the topic and operation' do
546
+ events = []
547
+ Karafka.monitor.subscribe('deimos.batch_consumption.initial_failure') do |event|
548
+ events << event.payload
549
+ end
550
+
551
+ expect {
552
+ publish_batch(
553
+ [
554
+ { key: 1, payload: { test_id: 'abc', some_int: 1 } },
555
+ { key: 2, payload: poison_payload }
556
+ ]
557
+ )
558
+ }.to raise_error(Deimos::BatchFallbackError)
559
+
560
+ expect(events.size).to eq(1)
561
+ expect(events.first).to include(consumer: consumer_class,
562
+ topic: 'my-topic',
563
+ operation: :upsert_records,
564
+ count: 2)
565
+ end
566
+
567
+ it 'should only re-run the database write, not the message-level processing' do
568
+ # Retrying by breaking the batch into writes of one must not re-run anything that
569
+ # already ran for the batch as a whole. pre_process is called once for the group, and
570
+ # valid_records is announced once with every record that made it, rather than once per
571
+ # record.
572
+ pre_processed = []
573
+ allow_any_instance_of(consumer_class).to receive(:pre_process) do |_, messages|
574
+ pre_processed << messages.map(&:key)
575
+ end
576
+
577
+ valid_records = []
578
+ Karafka.monitor.subscribe('deimos.batch_consumption.valid_records') do |event|
579
+ valid_records << event.payload[:records]
580
+ end
581
+
582
+ expect {
583
+ publish_batch(
584
+ [
585
+ { key: 1, payload: { test_id: 'abc', some_int: 1 } },
586
+ { key: 2, payload: poison_payload },
587
+ { key: 3, payload: { test_id: 'ghi', some_int: 3 } }
588
+ ]
589
+ )
590
+ }.to raise_error(Deimos::BatchFallbackError)
591
+
592
+ expect(pre_processed).to eq([%w(1 2 3)])
593
+ expect(valid_records.size).to eq(1)
594
+ expect(valid_records.first.map(&:test_id)).to contain_exactly('abc', 'ghi')
595
+ end
596
+
597
+ it 'should not retry individually when the batch failed on a deadlock' do
598
+ allow(Deimos::Utils::DeadlockRetry).to receive(:sleep)
599
+ allow(Widget).to receive(:import!).
600
+ and_raise(ActiveRecord::Deadlocked.new('Lock wait timeout exceeded'))
601
+
602
+ expect {
603
+ publish_batch(
604
+ [
605
+ { key: 1, payload: { test_id: 'abc', some_int: 1 } },
606
+ { key: 2, payload: { test_id: 'def', some_int: 2 } }
607
+ ]
608
+ )
609
+ }.to raise_error(ActiveRecord::Deadlocked)
610
+
611
+ # Only DeadlockRetry's own 3 attempts - no per-message retries on top.
612
+ expect(Widget).to have_received(:import!).exactly(3).times
613
+ end
614
+ end
615
+
616
+ context 'with a single-message batch' do
617
+ it 'should raise the original error rather than wrapping it' do
618
+ expect {
619
+ publish_batch([{ key: 1, payload: poison_payload }])
620
+ }.to raise_error(ActiveRecord::RecordInvalid)
621
+
622
+ expect(all_widgets).to be_empty
623
+ end
624
+ end
625
+
626
+ context 'when batch_message_fallback is turned off' do
627
+ before(:each) do
628
+ register_consumer(consumer_class, 'MySchema',
629
+ key_config: { plain: true },
630
+ configs: { reraise_errors: true,
631
+ batch_message_fallback: false })
632
+ end
633
+
634
+ it 'should lose the whole batch to a single bad record' do
635
+ expect {
636
+ publish_batch(
637
+ [
638
+ { key: 1, payload: { test_id: 'abc', some_int: 1 } },
639
+ { key: 2, payload: poison_payload },
640
+ { key: 3, payload: { test_id: 'ghi', some_int: 3 } }
641
+ ]
642
+ )
643
+ }.to raise_error(ActiveRecord::RecordInvalid)
644
+
645
+ expect(all_widgets).to be_empty
646
+ end
647
+
648
+ it 'should not attempt any individual writes' do
649
+ allow(Widget).to receive(:import!).and_call_original
650
+
651
+ expect {
652
+ publish_batch(
653
+ [
654
+ { key: 1, payload: { test_id: 'abc', some_int: 1 } },
655
+ { key: 2, payload: poison_payload }
656
+ ]
657
+ )
658
+ }.to raise_error(ActiveRecord::RecordInvalid)
659
+
660
+ # The batch write raises during validation, before any import is issued.
661
+ expect(Widget).not_to have_received(:import!)
662
+ end
663
+ end
664
+
665
+ context 'with keys repeated across BatchSlicer slices' do
666
+ it 'should still process the later slices after an earlier one fails' do
667
+ # Key 1 appears twice, so BatchSlicer splits the batch into two slices:
668
+ # [key 1 (poison), key 2] and [key 1 (valid)]. The valid update for key 1 sits in the
669
+ # second slice, so it only lands if a failure in the first slice doesn't abort the rest.
670
+ expect {
671
+ publish_batch(
672
+ [
673
+ { key: 1, payload: poison_payload },
674
+ { key: 1, payload: { test_id: 'later', some_int: 2 } },
675
+ { key: 2, payload: { test_id: 'ok', some_int: 3 } }
676
+ ]
677
+ )
678
+ }.to raise_error(Deimos::BatchFallbackError, /Failed keys: "1"/)
679
+
680
+ expect(all_widgets).
681
+ to contain_exactly(have_attributes(id: 1, test_id: 'later', some_int: 2),
682
+ have_attributes(id: 2, test_id: 'ok', some_int: 3))
683
+ end
684
+ end
685
+
686
+ context 'with max_db_batch_size' do
687
+ let(:consumer_class) do
688
+ Class.new(described_class) do
689
+ record_class Widget
690
+ compacted false
691
+ max_db_batch_size 2
692
+ end
693
+ end
694
+
695
+ it 'should still process the later groups after an earlier one fails' do
696
+ expect {
697
+ publish_batch(
698
+ [
699
+ { key: 1, payload: { test_id: 'abc', some_int: 1 } },
700
+ { key: 2, payload: poison_payload },
701
+ { key: 3, payload: { test_id: 'ghi', some_int: 3 } },
702
+ { key: 4, payload: { test_id: 'jkl', some_int: 4 } }
703
+ ]
704
+ )
705
+ }.to raise_error(Deimos::BatchFallbackError, /Failed keys: "2"/)
706
+
707
+ expect(all_widgets).
708
+ to contain_exactly(have_attributes(id: 1, test_id: 'abc'),
709
+ have_attributes(id: 3, test_id: 'ghi'),
710
+ have_attributes(id: 4, test_id: 'jkl'))
711
+ end
712
+ end
713
+
714
+ context 'when removing records fails' do
715
+ let(:consumer_class) do
716
+ Class.new(described_class) do
717
+ record_class Widget
718
+ compacted false
719
+
720
+ def remove_records(messages)
721
+ raise 'cannot delete widget 2' if messages.any? { |m| m.key.to_s == '2' }
722
+
723
+ super
724
+ end
725
+ end
726
+ end
727
+
728
+ it 'should delete the records it can and report the one it cannot' do
729
+ Widget.create!(id: 1, test_id: 'abc', some_int: 1)
730
+ Widget.create!(id: 2, test_id: 'def', some_int: 2)
731
+ Widget.create!(id: 3, test_id: 'ghi', some_int: 3)
732
+
733
+ expect {
734
+ publish_batch(
735
+ [
736
+ { key: 1, payload: nil },
737
+ { key: 2, payload: nil },
738
+ { key: 3, payload: nil }
739
+ ]
740
+ )
741
+ }.to raise_error(Deimos::BatchFallbackError, /Failed keys: "2"/)
742
+
743
+ expect(all_widgets).to contain_exactly(have_attributes(id: 2, test_id: 'def'))
744
+ end
745
+ end
746
+ end
747
+
463
748
  describe 'skipping records' do
464
749
  before(:each) do
465
750
  register_consumer(consumer_class,
@@ -119,7 +119,7 @@ RSpec.describe Deimos::ActiveRecordConsume::BatchConsumption do
119
119
  end
120
120
 
121
121
  it 'for schema classes' do
122
- klass = Class.new(AvroGen::SchemaClass::Record) do
122
+ klass = Class.new(Deimos::SchemaClass::Record) do
123
123
  attr_accessor :some_name
124
124
 
125
125
  # @override
@@ -59,7 +59,7 @@ module ActiveRecordConsumerTest
59
59
 
60
60
  Time.zone = 'Eastern Time (US & Canada)'
61
61
 
62
- schema_class = Class.new(AvroGen::SchemaClass::Record) do
62
+ schema_class = Class.new(Deimos::SchemaClass::Record) do
63
63
  def schema
64
64
  'MySchema'
65
65
  end
@@ -88,7 +88,7 @@ module ActiveRecordConsumerTest
88
88
  end
89
89
  stub_const('Schemas::MySchema', schema_class)
90
90
 
91
- schema_datetime_class = Class.new(AvroGen::SchemaClass::Record) do
91
+ schema_datetime_class = Class.new(Deimos::SchemaClass::Record) do
92
92
  def schema
93
93
  'MySchemaWithDateTimes'
94
94
  end
@@ -164,7 +164,7 @@ module ActiveRecordConsumerTest
164
164
  end
165
165
  end
166
166
  Deimos.configure do |config|
167
- config.avrogen.use_full_namespace = true
167
+ config.schema.use_full_namespace = true
168
168
  end
169
169
  end
170
170
 
@@ -95,7 +95,7 @@ describe Deimos::ActiveRecordProducer do
95
95
  before(:each) do
96
96
  Deimos.configure do |config|
97
97
  config.schema.use_schema_classes = use_schema_classes
98
- config.avrogen.use_full_namespace = true
98
+ config.schema.use_full_namespace = true
99
99
  end
100
100
  end
101
101
 
@@ -51,14 +51,14 @@ module ConsumerTest
51
51
 
52
52
  let(:schema_class_batch) do
53
53
  batch.map do |p|
54
- AvroGen::SchemaClass.instance(p, 'MySchema', 'com.my-namespace')
54
+ Deimos::Utils::SchemaClass.instance(p, 'MySchema', 'com.my-namespace')
55
55
  end
56
56
  end
57
57
  let(:use_schema_classes) { true }
58
58
 
59
59
  before(:each) do
60
60
  Deimos.configure do |config|
61
- config.avrogen.use_full_namespace = true
61
+ config.schema.use_full_namespace = true
62
62
  end
63
63
  end
64
64
 
@@ -48,8 +48,8 @@ module ConsumerTest
48
48
 
49
49
  before(:each) do
50
50
  Deimos.configure do |config|
51
- config.avrogen.use_full_namespace = true
52
- config.avrogen.schema_namespace_map = {
51
+ config.schema.use_full_namespace = true
52
+ config.schema.schema_namespace_map = {
53
53
  'com' => 'Schemas',
54
54
  'com.my-namespace.my-suborg' => %w(Schemas MyNamespace)
55
55
  }
@@ -160,7 +160,7 @@ module ConsumerTest
160
160
  before(:each) do
161
161
  set_karafka_config(:use_schema_classes, true)
162
162
  Deimos.configure do |config|
163
- config.avrogen.use_full_namespace = true
163
+ config.schema.use_full_namespace = true
164
164
  end
165
165
  end
166
166
 
@@ -205,8 +205,8 @@ module ConsumerTest
205
205
  before(:each) do
206
206
  set_karafka_config(:use_schema_classes, true)
207
207
  Deimos.configure do |config|
208
- config.avrogen.use_full_namespace = true
209
- config.avrogen.schema_namespace_map = {
208
+ config.schema.use_full_namespace = true
209
+ config.schema.schema_namespace_map = {
210
210
  'com' => 'Schemas',
211
211
  'com.my-namespace.my-suborg' => %w(Schemas MyNamespace)
212
212
  }
@@ -0,0 +1,16 @@
1
+ RSpec.describe Schemas::MyNamespace::MySchema do
2
+ let(:key) { Schemas::MyNamespace::MySchemaKey.new(test_id: 123) }
3
+
4
+ it 'should produce a tombstone with a hash' do
5
+ result = described_class.tombstone({ test_id: 123 })
6
+ expect(result.payload_key).to eq(key)
7
+ expect(result.to_h).to eq({ payload_key: { 'test_id' => 123 } })
8
+ end
9
+
10
+ it 'should work with a record' do
11
+ key = Schemas::MyNamespace::MySchemaKey.new(test_id: 123)
12
+ result = described_class.tombstone(key)
13
+ expect(result.payload_key).to eq(key)
14
+ expect(result.to_h).to eq({ payload_key: { 'test_id' => 123 } })
15
+ end
16
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ # For testing the generated class.
4
+ RSpec.describe Schemas::MyNamespace::MySchemaWithCircularReference do
5
+ let(:payload_hash) do
6
+ {
7
+ properties: {
8
+ a_boolean: {
9
+ 'property' => true
10
+ },
11
+ an_integer: {
12
+ 'property' => 1
13
+ },
14
+ a_float: {
15
+ 'property' => 4.5
16
+ },
17
+ a_string: {
18
+ 'property' => 'string'
19
+ },
20
+ an_array: {
21
+ 'property' => [1, 2, 3]
22
+ },
23
+ an_hash: {
24
+ 'property' => {
25
+ 'a_key' => 'a_value'
26
+ }
27
+ }
28
+ }
29
+ }
30
+ end
31
+
32
+ describe 'class initialization' do
33
+ it 'should initialize the class from keyword arguments' do
34
+ klass = described_class.new(properties: payload_hash[:properties])
35
+ expect(klass).to be_instance_of(described_class)
36
+ end
37
+
38
+ it 'should initialize the class from a hash with symbols as keys' do
39
+ klass = described_class.new(**payload_hash)
40
+ expect(klass).to be_instance_of(described_class)
41
+ end
42
+
43
+ it 'should initialize the class when missing attributes' do
44
+ payload_hash.delete(:properties)
45
+ klass = described_class.new(**payload_hash)
46
+ expect(klass).to be_instance_of(described_class)
47
+ end
48
+
49
+ end
50
+
51
+ describe 'base class methods' do
52
+ let(:klass) do
53
+ described_class.new(**payload_hash)
54
+ end
55
+
56
+ it 'should return the name of the schema and namespace' do
57
+ expect(klass.schema).to eq('MySchemaWithCircularReference')
58
+ expect(klass.namespace).to eq('com.my-namespace')
59
+ expect(klass.full_schema).to eq('com.my-namespace.MySchemaWithCircularReference')
60
+ end
61
+
62
+ it 'should return a json version of the payload' do
63
+ described_class.new(**payload_hash)
64
+ payload_h = {
65
+ 'properties' => {
66
+ a_boolean: {
67
+ 'property' =>true
68
+ },
69
+ an_integer: {
70
+ 'property' =>1
71
+ },
72
+ a_float: {
73
+ 'property' =>4.5
74
+ },
75
+ a_string: {
76
+ 'property' =>'string'
77
+ },
78
+ an_array: {
79
+ 'property' =>[1, 2, 3]
80
+ },
81
+ an_hash: {
82
+ 'property' =>{
83
+ 'a_key' => 'a_value'
84
+ }
85
+ }
86
+ }
87
+ }
88
+
89
+ expect(klass.as_json).to eq(payload_h)
90
+ end
91
+
92
+ it 'should return a JSON string of the payload' do
93
+ s = '{"properties":{"a_boolean":{"property":true},"an_integer":{"property":1},"a_float":{"property":4.5},"a_str' \
94
+ 'ing":{"property":"string"},"an_array":{"property":[1,2,3]},"an_hash":{"property":{"a_key":"a_value"}}}}'
95
+ expect(klass.to_json).to eq(s)
96
+ end
97
+ end
98
+ end