mongoid 7.0.3 → 7.0.8

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 (102) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +2 -0
  3. data.tar.gz.sig +1 -0
  4. data/LICENSE +1 -0
  5. data/README.md +3 -2
  6. data/Rakefile +12 -0
  7. data/lib/mongoid.rb +2 -1
  8. data/lib/mongoid/association/embedded/embeds_many.rb +2 -1
  9. data/lib/mongoid/association/embedded/embeds_one.rb +2 -1
  10. data/lib/mongoid/association/proxy.rb +1 -1
  11. data/lib/mongoid/association/relatable.rb +23 -21
  12. data/lib/mongoid/atomic.rb +13 -3
  13. data/lib/mongoid/atomic/paths/embedded.rb +1 -1
  14. data/lib/mongoid/attributes.rb +28 -20
  15. data/lib/mongoid/attributes/dynamic.rb +15 -14
  16. data/lib/mongoid/config/environment.rb +21 -8
  17. data/lib/mongoid/copyable.rb +5 -1
  18. data/lib/mongoid/criteria.rb +7 -1
  19. data/lib/mongoid/criteria/modifiable.rb +13 -2
  20. data/lib/mongoid/criteria/queryable/extensions/numeric.rb +1 -1
  21. data/lib/mongoid/criteria/queryable/extensions/regexp.rb +3 -3
  22. data/lib/mongoid/criteria/queryable/key.rb +67 -8
  23. data/lib/mongoid/criteria/queryable/mergeable.rb +5 -4
  24. data/lib/mongoid/criteria/queryable/selectable.rb +3 -4
  25. data/lib/mongoid/criteria/queryable/selector.rb +9 -31
  26. data/lib/mongoid/extensions/hash.rb +4 -2
  27. data/lib/mongoid/extensions/regexp.rb +1 -1
  28. data/lib/mongoid/extensions/string.rb +5 -3
  29. data/lib/mongoid/fields.rb +2 -1
  30. data/lib/mongoid/matchable.rb +14 -15
  31. data/lib/mongoid/matchable/all.rb +4 -3
  32. data/lib/mongoid/matchable/default.rb +71 -24
  33. data/lib/mongoid/matchable/regexp.rb +2 -2
  34. data/lib/mongoid/persistable/pushable.rb +11 -2
  35. data/lib/mongoid/persistence_context.rb +6 -6
  36. data/lib/mongoid/positional.rb +1 -1
  37. data/lib/mongoid/query_cache.rb +3 -2
  38. data/lib/mongoid/validatable/macros.rb +1 -1
  39. data/lib/mongoid/validatable/uniqueness.rb +1 -1
  40. data/lib/mongoid/version.rb +2 -1
  41. data/lib/rails/generators/mongoid/model/templates/model.rb.tt +1 -1
  42. data/spec/README.md +18 -0
  43. data/spec/app/models/delegating_patient.rb +16 -0
  44. data/spec/app/models/other_owner_object.rb +2 -0
  45. data/spec/integration/app_spec.rb +192 -0
  46. data/spec/integration/associations/embedded_spec.rb +62 -0
  47. data/spec/integration/criteria/time_with_zone_spec.rb +32 -0
  48. data/spec/integration/document_spec.rb +22 -0
  49. data/spec/integration/matchable_spec.rb +680 -0
  50. data/spec/lite_spec_helper.rb +15 -5
  51. data/spec/mongoid/association/embedded/embedded_in_spec.rb +58 -0
  52. data/spec/mongoid/association/embedded/embeds_many_models.rb +53 -0
  53. data/spec/mongoid/association/embedded/embeds_many_spec.rb +10 -0
  54. data/spec/mongoid/association/embedded/embeds_one_dnl_models.rb +6 -0
  55. data/spec/mongoid/association/embedded/embeds_one_models.rb +51 -0
  56. data/spec/mongoid/association/embedded/embeds_one_spec.rb +46 -0
  57. data/spec/mongoid/association/referenced/belongs_to_spec.rb +23 -6
  58. data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +2 -1
  59. data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +2 -1
  60. data/spec/mongoid/association/referenced/has_one_spec.rb +12 -2
  61. data/spec/mongoid/attributes/dynamic_spec.rb +153 -0
  62. data/spec/mongoid/attributes_spec.rb +19 -7
  63. data/spec/mongoid/clients/factory_spec.rb +2 -2
  64. data/spec/mongoid/clients/options_spec.rb +4 -4
  65. data/spec/mongoid/clients/sessions_spec.rb +20 -7
  66. data/spec/mongoid/clients/transactions_spec.rb +36 -15
  67. data/spec/mongoid/clients_spec.rb +2 -2
  68. data/spec/mongoid/contextual/atomic_spec.rb +20 -10
  69. data/spec/mongoid/contextual/geo_near_spec.rb +1 -0
  70. data/spec/mongoid/contextual/map_reduce_spec.rb +20 -5
  71. data/spec/mongoid/contextual/mongo_spec.rb +76 -53
  72. data/spec/mongoid/criteria/modifiable_spec.rb +59 -10
  73. data/spec/mongoid/criteria/queryable/extensions/numeric_spec.rb +54 -0
  74. data/spec/mongoid/criteria/queryable/extensions/regexp_spec.rb +7 -7
  75. data/spec/mongoid/criteria/queryable/extensions/string_spec.rb +1 -1
  76. data/spec/mongoid/criteria/queryable/key_spec.rb +48 -6
  77. data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +762 -0
  78. data/spec/mongoid/criteria/queryable/selectable_spec.rb +5 -224
  79. data/spec/mongoid/criteria/queryable/selector_spec.rb +37 -0
  80. data/spec/mongoid/criteria_spec.rb +7 -2
  81. data/spec/mongoid/document_fields_spec.rb +88 -0
  82. data/spec/mongoid/document_persistence_context_spec.rb +33 -0
  83. data/spec/mongoid/extensions/string_spec.rb +35 -7
  84. data/spec/mongoid/indexable_spec.rb +6 -4
  85. data/spec/mongoid/matchable/default_spec.rb +10 -3
  86. data/spec/mongoid/matchable/regexp_spec.rb +2 -2
  87. data/spec/mongoid/matchable_spec.rb +2 -2
  88. data/spec/mongoid/persistable/pushable_spec.rb +55 -1
  89. data/spec/mongoid/query_cache_spec.rb +2 -1
  90. data/spec/mongoid/relations/proxy_spec.rb +1 -1
  91. data/spec/mongoid/scopable_spec.rb +2 -1
  92. data/spec/mongoid/tasks/database_rake_spec.rb +13 -13
  93. data/spec/mongoid/tasks/database_spec.rb +1 -1
  94. data/spec/mongoid/validatable/uniqueness_spec.rb +33 -6
  95. data/spec/spec_helper.rb +4 -37
  96. data/spec/support/child_process_helper.rb +76 -0
  97. data/spec/support/cluster_config.rb +158 -0
  98. data/spec/support/constraints.rb +29 -19
  99. data/spec/support/expectations.rb +17 -3
  100. data/spec/support/spec_config.rb +12 -4
  101. metadata +525 -464
  102. metadata.gz.sig +2 -0
@@ -4,7 +4,6 @@ $LOAD_PATH.unshift(File.dirname(__FILE__))
4
4
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
5
5
 
6
6
  require "mongoid"
7
- require "rspec"
8
7
 
9
8
  # MRI 2.5 and JRuby 9.2 change visibility of Object#pp when 'pp' is required,
10
9
  # which happens when RSpec reports anything. This creates an issue for tests
@@ -36,16 +35,27 @@ else
36
35
  end
37
36
 
38
37
  RSpec.configure do |config|
38
+ config.expect_with(:rspec) do |c|
39
+ c.syntax = [:should, :expect]
40
+ end
41
+
39
42
  if SpecConfig.instance.ci?
40
43
  config.add_formatter(RSpec::Core::Formatters::JsonFormatter, File.join(File.dirname(__FILE__), '../tmp/rspec.json'))
41
44
  end
42
45
 
43
46
  if SpecConfig.instance.ci?
44
- # Allow a max of 30 seconds per test.
45
- # Tests should take under 10 seconds ideally but it seems
46
- # we have some that run for more than 10 seconds in CI.
47
+ timeout = if SpecConfig.instance.app_tests?
48
+ # Allow 5 minutes per test for the app tests, since they install
49
+ # gems for Rails applications which can take a long time.
50
+ 300
51
+ else
52
+ # Allow a max of 30 seconds per test.
53
+ # Tests should take under 10 seconds ideally but it seems
54
+ # we have some that run for more than 10 seconds in CI.
55
+ 30
56
+ end
47
57
  config.around(:each) do |example|
48
- TimeoutInterrupt.timeout(30) do
58
+ TimeoutInterrupt.timeout(timeout) do
49
59
  example.run
50
60
  end
51
61
  end
@@ -1,4 +1,5 @@
1
1
  require "spec_helper"
2
+ require_relative './embeds_one_models'
2
3
 
3
4
  describe Mongoid::Association::Embedded::EmbeddedIn do
4
5
 
@@ -313,6 +314,42 @@ describe Mongoid::Association::Embedded::EmbeddedIn do
313
314
  it 'returns nil' do
314
315
  expect(association.inverses).to eq(nil)
315
316
  end
317
+
318
+ context 'when class_name is given and is a plain string' do
319
+ let(:association) do
320
+ EomParent.relations['child']
321
+ end
322
+
323
+ it 'returns the inverse in an array' do
324
+ inverses = association.inverses
325
+ expect(inverses).to eq([:parent])
326
+ end
327
+ end
328
+
329
+ context 'when class_name is given and is a :: prefixed string' do
330
+ let(:association) do
331
+ EomCcChild.relations['parent']
332
+ end
333
+
334
+ it 'returns the inverse in an array' do
335
+ pending 'MONGOID-4751'
336
+
337
+ inverses = association.inverses
338
+ expect(inverses).to eq([:child])
339
+ end
340
+
341
+ context 'when other associations referencing unloaded classes exist' do
342
+ let(:association) do
343
+ EomDnlChild.relations['parent']
344
+ end
345
+
346
+ it 'does not load other classes' do
347
+ inverses = association.inverses
348
+ expect(inverses).to eq([:child])
349
+ expect(Object.const_defined?(:EoDnlMarker)).to be false
350
+ end
351
+ end
352
+ end
316
353
  end
317
354
  end
318
355
  end
@@ -549,6 +586,16 @@ describe Mongoid::Association::Embedded::EmbeddedIn do
549
586
  it 'returns the class name option' do
550
587
  expect(association.relation_class_name).to eq('OtherContainer')
551
588
  end
589
+
590
+ context ':class_name is a :: prefixed string' do
591
+ let(:association) do
592
+ EomCcChild.relations['parent']
593
+ end
594
+
595
+ it 'returns the :: prefixed string' do
596
+ expect(association.relation_class_name).to eq('::EomCcParent')
597
+ end
598
+ end
552
599
  end
553
600
 
554
601
  context 'when the class_name option is not specified' do
@@ -557,6 +604,17 @@ describe Mongoid::Association::Embedded::EmbeddedIn do
557
604
  expect(association.relation_class_name).to eq('Container')
558
605
  end
559
606
  end
607
+
608
+ context 'when another association in the model is referencing a third model class' do
609
+ let(:association) do
610
+ EomDnlChild.relations['parent']
611
+ end
612
+
613
+ it 'does not attempt to load the third class' do
614
+ expect(association.relation_class_name).to eq('EomDnlParent')
615
+ expect(Object.const_defined?(:EoDnlMarker)).to be false
616
+ end
617
+ end
560
618
  end
561
619
 
562
620
  describe '#klass' do
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+ # encoding: utf-8
3
+
4
+ class EmmCongress
5
+ include Mongoid::Document
6
+
7
+ embeds_many :legislators, class_name: 'EmmLegislator'
8
+
9
+ field :name, type: String
10
+ end
11
+
12
+ class EmmLegislator
13
+ include Mongoid::Document
14
+
15
+ embedded_in :congress, class_name: 'EmmCongress'
16
+
17
+ field :a, type: Integer, default: 0
18
+ field :b, type: Integer, default: 0
19
+ end
20
+
21
+ # Models with associations with :class_name as a :: prefixed string
22
+
23
+ class EmmCcCongress
24
+ include Mongoid::Document
25
+
26
+ embeds_many :legislators, class_name: '::EmmCcLegislator'
27
+
28
+ field :name, type: String
29
+ end
30
+
31
+ class EmmCcLegislator
32
+ include Mongoid::Document
33
+
34
+ embedded_in :congress, class_name: '::EmmCcCongress'
35
+
36
+ field :a, type: Integer, default: 0
37
+ field :b, type: Integer, default: 0
38
+ end
39
+
40
+ class EmmManufactory
41
+ include Mongoid::Document
42
+
43
+ embeds_many :products, order: :id.desc, class_name: 'EmmProduct'
44
+ end
45
+
46
+
47
+ class EmmProduct
48
+ include Mongoid::Document
49
+
50
+ embedded_in :manufactory, class_name: 'EmmManufactory'
51
+
52
+ field :name, type: String
53
+ end
@@ -646,6 +646,16 @@ describe Mongoid::Association::Embedded::EmbedsMany do
646
646
  it 'returns the class name option' do
647
647
  expect(association.klass).to eq(_class)
648
648
  end
649
+
650
+ context 'when the class name is prefixed with ::' do
651
+ let(:options) do
652
+ { class_name: '::OtherEmbeddedObject' }
653
+ end
654
+
655
+ it 'returns the class name option' do
656
+ expect(association.klass).to eq(_class)
657
+ end
658
+ end
649
659
  end
650
660
 
651
661
  context 'when the class_name option is not specified' do
@@ -0,0 +1,6 @@
1
+ # This file is added as an autoload target, but should never be actually
2
+ # processed.
3
+
4
+ # Marker class - if this class exists, this file was attempted to be required.
5
+ class EoDnlMarker
6
+ end
@@ -0,0 +1,51 @@
1
+ class EomParent
2
+ include Mongoid::Document
3
+
4
+ embeds_one :child, class_name: 'EomChild'
5
+
6
+ field :name, type: String
7
+ end
8
+
9
+ class EomChild
10
+ include Mongoid::Document
11
+
12
+ embedded_in :parent, class_name: 'EomParent'
13
+
14
+ field :a, type: Integer, default: 0
15
+ field :b, type: Integer, default: 0
16
+ end
17
+
18
+ # Models with associations with :class_name as a :: prefixed string
19
+
20
+ class EomCcParent
21
+ include Mongoid::Document
22
+
23
+ embeds_one :child, class_name: '::EomCcChild'
24
+ end
25
+
26
+ class EomCcChild
27
+ include Mongoid::Document
28
+
29
+ embedded_in :parent, class_name: '::EomCcParent'
30
+ end
31
+
32
+ # Models referencing other models which should not be loaded unless the
33
+ # respective association is referenced
34
+
35
+ autoload :EomDnlMissingChild, 'mongoid/association/embedded/embeds_one_dnl_models'
36
+
37
+ class EomDnlParent
38
+ include Mongoid::Document
39
+
40
+ embeds_one :child, class_name: 'EomDnlChild'
41
+ embeds_one :missing_child, class_name: 'EomDnlMissingChild'
42
+ end
43
+
44
+ autoload :EomDnlMissingParent, 'mongoid/association/embedded/embeds_one_dnl_models'
45
+
46
+ class EomDnlChild
47
+ include Mongoid::Document
48
+
49
+ embedded_in :parent, class_name: 'EomDnlParent'
50
+ embedded_in :missing_parent, class_name: 'EomDnlMissingParent'
51
+ end
@@ -1,4 +1,5 @@
1
1
  require "spec_helper"
2
+ require_relative './embeds_one_models'
2
3
 
3
4
  describe Mongoid::Association::Embedded::EmbedsOne do
4
5
 
@@ -444,6 +445,30 @@ describe Mongoid::Association::Embedded::EmbedsOne do
444
445
  it 'returns the :as attribute' do
445
446
  expect(association.inverses).to eq([ :containable ])
446
447
  end
448
+
449
+ context 'when class_name is given and is a :: prefixed string' do
450
+
451
+ let(:association) do
452
+ EomCcParent.relations['child']
453
+ end
454
+
455
+ it 'returns the inverse in an array' do
456
+ inverses = association.inverses
457
+ expect(inverses).to eq([:parent])
458
+ end
459
+
460
+ context 'when other associations referencing unloaded classes exist' do
461
+ let(:association) do
462
+ EomDnlParent.relations['child']
463
+ end
464
+
465
+ it 'does not load other classes' do
466
+ inverses = association.inverses
467
+ expect(inverses).to eq([:parent])
468
+ expect(Object.const_defined?(:EoDnlMarker)).to be false
469
+ end
470
+ end
471
+ end
447
472
  end
448
473
  end
449
474
  end
@@ -604,6 +629,16 @@ describe Mongoid::Association::Embedded::EmbedsOne do
604
629
  it 'returns the class name option' do
605
630
  expect(association.relation_class_name).to eq('OtherEmbeddedObject')
606
631
  end
632
+
633
+ context ':class_name is a :: prefixed string' do
634
+ let(:association) do
635
+ EomCcParent.relations['child']
636
+ end
637
+
638
+ it 'returns the :: prefixed string' do
639
+ expect(association.relation_class_name).to eq('::EomCcChild')
640
+ end
641
+ end
607
642
  end
608
643
 
609
644
  context 'when the class_name option is not specified' do
@@ -612,6 +647,17 @@ describe Mongoid::Association::Embedded::EmbedsOne do
612
647
  expect(association.relation_class_name).to eq('EmbeddedObject')
613
648
  end
614
649
  end
650
+
651
+ context 'when another association in the model is referencing a third model class' do
652
+ let(:association) do
653
+ EomDnlParent.relations['child']
654
+ end
655
+
656
+ it 'does not attempt to load the third class' do
657
+ expect(association.relation_class_name).to eq('EomDnlChild')
658
+ expect(Object.const_defined?(:EoDnlMarker)).to be false
659
+ end
660
+ end
615
661
  end
616
662
 
617
663
  describe '#klass' do
@@ -1622,6 +1622,17 @@ describe Mongoid::Association::Referenced::BelongsTo do
1622
1622
  end
1623
1623
  end
1624
1624
 
1625
+ context 'when the :class_name option is scoped with ::' do
1626
+
1627
+ let(:options) do
1628
+ { class_name: '::OtherOwnerObject' }
1629
+ end
1630
+
1631
+ it 'returns the class name option' do
1632
+ expect(association.relation_class_name).to eq('::OtherOwnerObject')
1633
+ end
1634
+ end
1635
+
1625
1636
  context 'when the class_name option is not specified' do
1626
1637
 
1627
1638
  it 'uses the name of the relation to deduce the class name' do
@@ -1644,17 +1655,23 @@ describe Mongoid::Association::Referenced::BelongsTo do
1644
1655
 
1645
1656
  context 'when the :class_name option is specified' do
1646
1657
 
1647
- let!(:_class) do
1648
- class OtherOwnerObject; end
1649
- OtherOwnerObject
1658
+ let(:options) do
1659
+ { class_name: 'OtherOwnerObject' }
1660
+ end
1661
+
1662
+ it 'returns the named class' do
1663
+ expect(association.relation_class).to eq(OtherOwnerObject)
1650
1664
  end
1665
+ end
1666
+
1667
+ context 'when the :class_name option is scoped with ::' do
1651
1668
 
1652
1669
  let(:options) do
1653
- { class_name: 'OtherOwnerObject' }
1670
+ { class_name: '::OtherOwnerObject' }
1654
1671
  end
1655
1672
 
1656
- it 'returns the class name option' do
1657
- expect(association.relation_class).to eq(_class)
1673
+ it 'returns the named class' do
1674
+ expect(association.relation_class).to eq(OtherOwnerObject)
1658
1675
  end
1659
1676
  end
1660
1677
 
@@ -2686,7 +2686,8 @@ describe Mongoid::Association::Referenced::HasAndBelongsToMany::Proxy do
2686
2686
  expect(preferences).to eq([ preference_one ])
2687
2687
  end
2688
2688
 
2689
- context 'when providing a collation', if: collation_supported? do
2689
+ context 'when providing a collation' do
2690
+ min_server_version '3.4'
2690
2691
 
2691
2692
  let(:preferences) do
2692
2693
  person.preferences.where(name: "FIRST").collation(locale: 'en_US', strength: 2).to_a
@@ -2904,7 +2904,8 @@ describe Mongoid::Association::Referenced::HasMany::Proxy do
2904
2904
  expect(posts).to eq([ post_one ])
2905
2905
  end
2906
2906
 
2907
- context 'when providing a collation', if: collation_supported? do
2907
+ context 'when providing a collation' do
2908
+ min_server_version '3.4'
2908
2909
 
2909
2910
  let(:posts) do
2910
2911
  person.posts.where(title: "FIRST").collation(locale: 'en_US', strength: 2)
@@ -909,12 +909,12 @@ describe Mongoid::Association::Referenced::HasOne do
909
909
  end
910
910
  end
911
911
 
912
- it 'returns the proper namespaced class name' do
912
+ it 'returns the inferred unqualified class name' do
913
913
  define_classes
914
914
 
915
915
  expect(
916
916
  HasOneAssociationClassName::OwnedClass.relations['owner_class'].relation_class_name
917
- ).to eq('HasOneAssociationClassName::OwnerClass')
917
+ ).to eq('OwnerClass')
918
918
  end
919
919
  end
920
920
 
@@ -945,6 +945,16 @@ describe Mongoid::Association::Referenced::HasOne do
945
945
  expect(association.relation_class_name).to eq('BelongingObject')
946
946
  end
947
947
  end
948
+
949
+ context "when the class is not defined" do
950
+ let(:name) do
951
+ :undefined_class
952
+ end
953
+
954
+ it 'does not trigger autoloading' do
955
+ expect(association.relation_class_name).to eq('UndefinedClass')
956
+ end
957
+ end
948
958
  end
949
959
 
950
960
  describe '#relation_class' do
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+ # encoding: utf-8
3
+
4
+ require "spec_helper"
5
+
6
+ describe Mongoid::Attributes::Dynamic do
7
+ shared_examples_for 'dynamic field' do
8
+ let(:raw_attributes) do
9
+ {attr_name => 'foo bar'}
10
+ end
11
+
12
+ context 'when reading attributes' do
13
+ context 'an unsaved model' do
14
+ it 'can be read' do
15
+ bar = Bar.new(raw_attributes)
16
+ expect(bar.send(attr_name)).to eq('foo bar')
17
+ end
18
+ end
19
+
20
+ context 'saved model' do
21
+ it 'can be read' do
22
+ bar = Bar.new(raw_attributes)
23
+ bar.save!
24
+
25
+ bar = Bar.find(bar.id)
26
+ expect(bar.send(attr_name)).to eq('foo bar')
27
+ end
28
+ end
29
+
30
+ context 'when attribute is not set' do
31
+ it 'cannot be read' do
32
+ bar = Bar.new
33
+ expect do
34
+ bar.send(attr_name)
35
+ end.to raise_error(NoMethodError)
36
+ end
37
+
38
+ context 'reading via read_attribute' do
39
+ it 'returns nil' do
40
+ bar = Bar.new
41
+ expect(bar.read_attribute(:foo)).to be nil
42
+ end
43
+ end
44
+
45
+ context 'reading via []' do
46
+ it 'returns nil' do
47
+ bar = Bar.new
48
+ expect(bar[:foo]).to be nil
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ context 'when writing attributes via constructor' do
55
+ it 'can be written' do
56
+ bar = Bar.new(raw_attributes)
57
+ bar.save!
58
+
59
+ bar = Bar.find(bar.id)
60
+ expect(bar.send(attr_name)).to eq('foo bar')
61
+ end
62
+ end
63
+
64
+ context 'when writing attributes via attributes=' do
65
+ it 'can be written' do
66
+ bar = Bar.new
67
+ bar.attributes = raw_attributes
68
+ bar.save!
69
+
70
+ bar = Bar.find(bar.id)
71
+ expect(bar.send(attr_name)).to eq('foo bar')
72
+ end
73
+ end
74
+
75
+ context 'when writing attributes via write_attribute' do
76
+ it 'can be written' do
77
+ bar = Bar.new
78
+ bar.write_attribute(attr_name, 'foo bar')
79
+ bar.save!
80
+
81
+ bar = Bar.find(bar.id)
82
+ expect(bar.send(attr_name)).to eq('foo bar')
83
+ end
84
+ end
85
+
86
+ context 'when writing attributes via []=' do
87
+ context 'string key' do
88
+ it 'can be written' do
89
+ bar = Bar.new
90
+ bar[attr_name.to_s] = 'foo bar'
91
+ bar.save!
92
+
93
+ bar = Bar.find(bar.id)
94
+ expect(bar.send(attr_name)).to eq('foo bar')
95
+ end
96
+ end
97
+
98
+ context 'symbol key' do
99
+ it 'can be written' do
100
+ bar = Bar.new
101
+ bar[attr_name.to_sym] = 'foo bar'
102
+ bar.save!
103
+
104
+ bar = Bar.find(bar.id)
105
+ expect(bar.send(attr_name)).to eq('foo bar')
106
+ end
107
+ end
108
+ end
109
+
110
+ context 'when writing attributes via #{attribute}=' do
111
+ context 'when attribute is not already set' do
112
+ let(:bar) { Bar.new }
113
+
114
+ it 'cannot be written' do
115
+ expect do
116
+ bar.send("#{attr_name}=", 'foo bar')
117
+ bar.save!
118
+ end.to raise_error(NoMethodError)
119
+ end
120
+ end
121
+
122
+ context 'when attribute is already set' do
123
+ let(:bar) { Bar.new(attr_name => 'foo bar') }
124
+
125
+ it 'can be written' do
126
+ bar.send("#{attr_name}=", 'new foo bar')
127
+ bar.save!
128
+
129
+ _bar = Bar.find(bar.id)
130
+ expect(_bar.send(attr_name)).to eq('new foo bar')
131
+ end
132
+ end
133
+ end
134
+ end
135
+
136
+ context 'when attribute name is alphanumeric' do
137
+ let(:attr_name) { 'foo' }
138
+
139
+ it_behaves_like 'dynamic field'
140
+ end
141
+
142
+ context 'when attribute name contains spaces' do
143
+ let(:attr_name) { 'hello world' }
144
+
145
+ it_behaves_like 'dynamic field'
146
+ end
147
+
148
+ context 'when attribute name contains special characters' do
149
+ let(:attr_name) { 'hello%world' }
150
+
151
+ it_behaves_like 'dynamic field'
152
+ end
153
+ end