mongoid 7.0.3 → 7.0.8

Sign up to get free protection for your applications and to get access to all the features.
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