association_accessors 1.3.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 (42) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/Appraisals +25 -0
  4. data/Gemfile +6 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +254 -0
  7. data/Rakefile +27 -0
  8. data/association_accessors.gemspec +39 -0
  9. data/bin/test +21 -0
  10. data/gemfiles/activerecord_4.1.gemfile +7 -0
  11. data/gemfiles/activerecord_4.1.gemfile.lock +67 -0
  12. data/gemfiles/activerecord_4.2.gemfile +7 -0
  13. data/gemfiles/activerecord_4.2.gemfile.lock +67 -0
  14. data/gemfiles/activerecord_5.0.gemfile +7 -0
  15. data/gemfiles/activerecord_5.0.gemfile.lock +63 -0
  16. data/gemfiles/activerecord_5.1.gemfile +7 -0
  17. data/gemfiles/activerecord_5.1.gemfile.lock +63 -0
  18. data/gemfiles/activerecord_5.2.gemfile +7 -0
  19. data/gemfiles/activerecord_5.2.gemfile.lock +63 -0
  20. data/gemfiles/rspec_2.gemfile +7 -0
  21. data/gemfiles/rspec_2.gemfile.lock +58 -0
  22. data/lib/association_accessors.rb +30 -0
  23. data/lib/association_accessors/collection_association.rb +35 -0
  24. data/lib/association_accessors/singular_association.rb +19 -0
  25. data/lib/association_accessors/test/matcher.rb +60 -0
  26. data/lib/association_accessors/version.rb +3 -0
  27. data/spec/association_accessors_spec.rb +5 -0
  28. data/spec/dummy.rb +99 -0
  29. data/spec/matcher_spec.rb +56 -0
  30. data/spec/spec_helper.rb +23 -0
  31. data/spec/with_association/belongs_to_spec.rb +33 -0
  32. data/spec/with_association/has_and_belongs_to_many_spec.rb +45 -0
  33. data/spec/with_association/has_many_spec.rb +45 -0
  34. data/spec/with_association/has_many_through_spec.rb +27 -0
  35. data/spec/with_association/has_one_spec.rb +33 -0
  36. data/spec/with_association/has_one_through_spec.rb +43 -0
  37. data/spec/with_association/polymorphic/belongs_to_spec.rb +38 -0
  38. data/spec/with_association/polymorphic/has_many_spec.rb +44 -0
  39. data/spec/with_association/polymorphic/has_one_spec.rb +33 -0
  40. data/spec/with_association/with_custom_name_spec.rb +33 -0
  41. data/spec/with_attribute_spec.rb +47 -0
  42. metadata +203 -0
@@ -0,0 +1,60 @@
1
+ module AssociationAccessors
2
+ module Test
3
+ def have_association_accessor_for association
4
+ Matcher.new association
5
+ end
6
+
7
+ class Matcher
8
+ attr_reader :failure_message, :subject, :association_name, :attribute, :association
9
+
10
+ def initialize association_name
11
+ @association_name = association_name
12
+ end
13
+
14
+ def description
15
+ "have association accessor for #{association_name.inspect} with attribute #{attribute.inspect}."
16
+ end
17
+
18
+ def matches? subject
19
+ @subject = subject
20
+ @association = get_association
21
+
22
+ check_attribute!
23
+
24
+ has_accessors?
25
+ end
26
+
27
+ def with_attribute attribute
28
+ @attribute = attribute
29
+ self
30
+ end
31
+
32
+
33
+ private
34
+
35
+ def get_association
36
+ subject.association(association_name)
37
+ end
38
+
39
+ def check_attribute!
40
+ raise ArgumentError, "'with_attribute' is required" if attribute.nil?
41
+ end
42
+
43
+ def has_accessors?
44
+ accessor_name =
45
+ if association.reflection.collection?
46
+ :"#{association_name.to_s.singularize}_#{attribute.to_s.pluralize}"
47
+ else
48
+ :"#{association_name}_#{attribute}"
49
+ end
50
+
51
+ if subject.respond_to?(accessor_name) && subject.respond_to?(:"#{accessor_name}=")
52
+ true
53
+ else
54
+ @failure_message = "reader and/or writer `#{accessor_name}` not defined on #{subject.class.name}."
55
+ false
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,3 @@
1
+ module AssociationAccessors
2
+ VERSION = "1.3.0"
3
+ end
@@ -0,0 +1,5 @@
1
+ RSpec.describe AssociationAccessors do
2
+ it "has a version number" do
3
+ expect(AssociationAccessors::VERSION).to eq '1.3.0'
4
+ end
5
+ end
data/spec/dummy.rb ADDED
@@ -0,0 +1,99 @@
1
+ require 'active_record'
2
+
3
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
4
+
5
+ ActiveRecord::Schema.define do
6
+ create_table :authors, force: true do |t|
7
+ t.string :serial
8
+ end
9
+
10
+ create_table :books, force: true do |t|
11
+ t.string :serial
12
+ t.integer :author_id
13
+ end
14
+
15
+ create_table :addresses, force: true do |t|
16
+ t.string :serial
17
+ t.integer :author_id
18
+ end
19
+
20
+ create_table :chapters, force: true do |t|
21
+ t.string :serial
22
+ t.integer :book_id
23
+ end
24
+
25
+ create_table :publishers, force: true do |t|
26
+ t.string :uuid
27
+ end
28
+
29
+ create_join_table :authors, :publishers
30
+
31
+ create_table :images, force: true do |t|
32
+ t.string :serial
33
+ t.string :imageable_type
34
+ t.integer :imageable_id
35
+ end
36
+ end
37
+
38
+ class Serialable < ActiveRecord::Base
39
+ include AssociationAccessors
40
+ self.abstract_class = true
41
+ before_create do
42
+ send :serial=, SecureRandom.urlsafe_base64
43
+ end
44
+ end
45
+
46
+ class Author < Serialable
47
+ has_many :books
48
+ association_accessor_for :books, with_attribute: :serial
49
+
50
+ has_one :address
51
+ association_accessor_for :address, with_attribute: :serial
52
+
53
+ has_many :chapters, through: :books
54
+ association_accessor_for :chapters, with_attribute: :serial
55
+
56
+ has_and_belongs_to_many :publishers
57
+ association_accessor_for :publishers, with_attribute: :uuid
58
+
59
+ has_one :image, as: :imageable
60
+ association_accessor_for :image, with_attribute: :serial
61
+ end
62
+
63
+ class Book < Serialable
64
+ OPTIONAL = ActiveRecord.gem_version.segments.first == 5 ? { optional: true } : {}
65
+
66
+ belongs_to :author, OPTIONAL
67
+ association_accessor_for :author, with_attribute: :serial
68
+
69
+ belongs_to :writer, { class_name: 'Author', foreign_key: :author_id }.merge(OPTIONAL)
70
+ association_accessor_for :writer, with_attribute: :serial
71
+
72
+ has_one :address, through: :author
73
+ association_accessor_for :address, with_attribute: :serial
74
+
75
+ has_many :images, as: :imageable
76
+ association_accessor_for :images, with_attribute: :serial
77
+
78
+ has_many :chapters
79
+ end
80
+
81
+ class Address < Serialable
82
+ belongs_to :author
83
+ end
84
+
85
+ class Chapter < Serialable
86
+ belongs_to :book
87
+ end
88
+
89
+ class Publisher < ActiveRecord::Base
90
+ has_and_belongs_to_many :authors
91
+ before_create do
92
+ send :uuid=, SecureRandom.uuid
93
+ end
94
+ end
95
+
96
+ class Image < Serialable
97
+ belongs_to :imageable, polymorphic: true
98
+ association_accessor_for :imageable, with_attribute: :serial
99
+ end
@@ -0,0 +1,56 @@
1
+ RSpec.configure do |config|
2
+ config.include AssociationAccessors::Test
3
+
4
+ # only the matcher should be tested against older version of RSpec
5
+ config.filter_run matcher: true if RSPEC_MAJOR_VERSION == 2
6
+ end
7
+
8
+ RSpec.describe 'the test matcher', matcher: true do
9
+ subject { Author.new }
10
+
11
+ it 'raises ActiveRecord::AssociationNotFoundError if the association does not exist on subject.' do
12
+ expect {
13
+ should have_association_accessor_for(:book).with_attribute(:serial)
14
+ }.to raise_exception ActiveRecord::AssociationNotFoundError, /Association named 'book' was not found on Author/
15
+ end
16
+
17
+ it 'raises ArgumentError if `with_attribute` is blank.' do
18
+ expect {
19
+ should have_association_accessor_for(:books)
20
+ }.to raise_exception ArgumentError, /'with_attribute' is required/
21
+ end
22
+
23
+ describe 'for singular association' do
24
+ it 'fails if subject does not respond to the reader or writer `[association]_[attribute]`.' do
25
+ expect {
26
+ should have_association_accessor_for(:address).with_attribute(:uuid)
27
+ }.to raise_exception RSpec::Expectations::ExpectationNotMetError, /reader and\/or writer `address_uuid` not defined on Author/
28
+ end
29
+
30
+ it 'passes if subject responds to the reader and writer `[association]_[attribute]`.' do
31
+ should have_association_accessor_for(:address).with_attribute(:serial)
32
+ should have_association_accessor_for(:image).with_attribute(:serial)
33
+ expect(Book.new).to have_association_accessor_for(:author).with_attribute(:serial)
34
+ end
35
+ end
36
+
37
+ describe 'for collection association' do
38
+ it 'fails if subject does not respond to the reader or writer `[association_singular]_[attribute_plural]`.' do
39
+ expect {
40
+ should have_association_accessor_for(:chapters).with_attribute(:uuid)
41
+ }.to raise_exception RSpec::Expectations::ExpectationNotMetError, /reader and\/or writer `chapter_uuids` not defined on Author/
42
+ end
43
+
44
+ it 'passes if subject responds to the reader and writer `[association_singular]_[attribute_plural]`.' do
45
+ should have_association_accessor_for(:books).with_attribute(:serial)
46
+ should have_association_accessor_for(:chapters).with_attribute(:serial)
47
+ should have_association_accessor_for(:publishers).with_attribute(:uuid)
48
+ expect(Book.new).to have_association_accessor_for(:images).with_attribute(:serial)
49
+ end
50
+ end
51
+
52
+ it 'has a concise but expressive default output.' do
53
+ expect(have_association_accessor_for(:books).with_attribute(:serial).description)
54
+ .to eq 'have association accessor for :books with attribute :serial.'
55
+ end
56
+ end
@@ -0,0 +1,23 @@
1
+ require "bundler/setup"
2
+ require "association_accessors"
3
+ require 'dummy'
4
+
5
+ RSPEC_MAJOR_VERSION = RSpec::Core::Version::STRING.split('.').first.to_i
6
+
7
+ RSpec.configure do |config|
8
+ if RSPEC_MAJOR_VERSION == 3
9
+ # Enable flags like --only-failures and --next-failure
10
+ config.example_status_persistence_file_path = ".rspec_status"
11
+
12
+ # Disable RSpec exposing methods globally on `Module` and `main`
13
+ config.disable_monkey_patching!
14
+ end
15
+
16
+ config.expect_with :rspec do |c|
17
+ c.syntax = :expect
18
+ end
19
+
20
+ config.filter_run_excluding activerecord: proc { |versions|
21
+ versions.none? { |version| ActiveRecord.gem_version.to_s.include? version }
22
+ }
23
+ end
@@ -0,0 +1,33 @@
1
+ RSpec.describe 'for `belongs_to` association' do
2
+ let!(:author) { Author.create! }
3
+ let!(:book) { Book.create! author: author }
4
+
5
+ it 'generates `#[association_name]_[attribute_name]` accessor.' do
6
+ expect(book).to respond_to :author_serial, :author_serial=
7
+ end
8
+
9
+ describe '`#[association_name]_[attribute_name]`' do
10
+ it 'returns the value of the `[attribute]` of the associated record if there is any.' do
11
+ expect { book.author = nil }.to change(book, :author_serial).from(author.serial).to nil
12
+ end
13
+ end
14
+
15
+ describe '`#[association_name]_[attribute_name]=(value)`' do
16
+ it 'sets the association value to nil if `value` is nil.' do
17
+ expect { book.author_serial = nil }.to change(book, :author).from(author).to nil
18
+ end
19
+
20
+ it 'sets the association value to the record queried by its `[attribute]` having the value `value` if such record exists.' do
21
+ new_author = Author.create!
22
+ expect { book.author_serial = new_author.serial }.to change(book, :author).from(author).to new_author
23
+ end
24
+
25
+ it 'raises ActiveRecord::RecordNotFound if record with `[attribute]` = `value` does not exist.', activerecord: [ '4.1' ] do
26
+ expect { book.author_serial = author.serial.next }.to raise_exception ActiveRecord::RecordNotFound
27
+ end
28
+
29
+ it 'raises ActiveRecord::RecordNotFound if record with `[attribute]` = `value` does not exist.', activerecord: [ '4.2', '5' ] do
30
+ expect { book.author_serial = author.serial.next }.to raise_exception ActiveRecord::RecordNotFound, "Couldn't find Author"
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,45 @@
1
+ RSpec.describe 'for `has_and_belongs_to_many` association' do
2
+ let!(:author_1) { Author.create! }
3
+ let!(:author_2) { Author.create! }
4
+ let!(:publisher_1) { Publisher.create! authors: [ author_1 ] }
5
+ let!(:publisher_2) { Publisher.create! authors: [ author_2 ] }
6
+
7
+ it 'generates `#[association_name.singularize]_[attribute_name.pluralize]` accessor.' do
8
+ expect(author_1).to respond_to :publisher_uuids, :publisher_uuids=
9
+ end
10
+
11
+ describe '`#[association_name.singularize]_[attribute_name.pluralize]`' do
12
+ it 'returns an array with the values of `[attribute]` of the associated records.' do
13
+ expect(author_1.publisher_uuids).to eq [ publisher_1.uuid ]
14
+ expect(author_2.publisher_uuids).to eq [ publisher_2.uuid ]
15
+ end
16
+ end
17
+
18
+ describe '`#[association_name.singularize]_[attribute_name.pluralize]`=(values)' do
19
+ it 'sets the association value to the records queried by `[attribute]` = `value` if all exist.' do
20
+ expect { author_1.publisher_uuids = [] }.to change { author_1.publishers.ids }.from([ publisher_1.id ]).to []
21
+ expect { author_2.publisher_uuids = [ publisher_1.uuid ] }.to change { author_2.publishers.ids }.from([ publisher_2.id ]).to [ publisher_1.id ]
22
+ end
23
+
24
+ it 'raises ActiveRecord::RecordNotFound if any of the given `values` has no record.', activerecord: [ '4' ] do
25
+ expect { author_2.publisher_uuids = [ publisher_1.uuid, publisher_2.uuid.next ] }
26
+ .to raise_exception(ActiveRecord::RecordNotFound,
27
+ %{Couldn't find all Publishers with 'id': ("#{publisher_1.uuid}", "#{publisher_2.uuid.next}")} +
28
+ %{ (found 1 results, but was looking for 2)})
29
+ end
30
+
31
+ it 'raises ActiveRecord::RecordNotFound if any of the given `values` has no record.', activerecord: [ '5.0', '5.1' ] do
32
+ expect { author_2.publisher_uuids = [ publisher_1.uuid, publisher_2.uuid.next ] }
33
+ .to raise_exception(ActiveRecord::RecordNotFound,
34
+ %{Couldn't find all Publishers with 'uuid': ("#{publisher_1.uuid}", "#{publisher_2.uuid.next}")} +
35
+ %{ (found 1 results, but was looking for 2)})
36
+ end
37
+
38
+ it 'raises ActiveRecord::RecordNotFound if any of the given `values` has no record.', activerecord: [ '5.2' ] do
39
+ expect { author_2.publisher_uuids = [ publisher_1.uuid, publisher_2.uuid.next ] }
40
+ .to raise_exception(ActiveRecord::RecordNotFound,
41
+ %{Couldn't find all Publishers with 'uuid': ("#{publisher_1.uuid}", "#{publisher_2.uuid.next}")} +
42
+ %{ (found 1 results, but was looking for 2). Couldn't find Publisher with uuid "#{publisher_2.uuid.next}".})
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,45 @@
1
+ RSpec.describe 'for `has_many` association' do
2
+ let!(:author_1) { Author.create! }
3
+ let!(:author_2) { Author.create! }
4
+ let!(:book_1) { Book.create! author: author_1 }
5
+ let!(:book_2) { Book.create! author: author_1 }
6
+
7
+ it 'generates `#[association_name.singularize]_[attribute_name.pluralize]` accessor.' do
8
+ expect(author_1).to respond_to :book_serials, :book_serials=
9
+ end
10
+
11
+ describe '`#[association_name.singularize]_[attribute_name.pluralize]`' do
12
+ it 'returns an array with the values of `[attribute]` of the associated records.' do
13
+ expect(author_1.book_serials).to match_array [ book_1.serial, book_2.serial ]
14
+ expect(author_2.book_serials).to eq []
15
+ end
16
+ end
17
+
18
+ describe '`#[association_name.singularize]_[attribute_name.pluralize]`=(values)' do
19
+ it 'sets the association value to the records queried by `[attribute]` = `value` if all exist.' do
20
+ expect { author_1.book_serials = [] }.to change { author_1.books.ids }.from([ book_1.id, book_2.id ]).to []
21
+ expect { author_2.book_serials = [ book_1.serial ] }.to change { author_2.books.ids }.from([]).to [ book_1.id ]
22
+ end
23
+
24
+ it 'raises ActiveRecord::RecordNotFound if any of the given `values` has no record.', activerecord: [ '4' ] do
25
+ expect { author_2.book_serials = [ book_1.serial, book_2.serial.next ] }
26
+ .to raise_exception(ActiveRecord::RecordNotFound,
27
+ %{Couldn't find all Books with 'id': ("#{book_1.serial}", "#{book_2.serial.next}")} +
28
+ %{ (found 1 results, but was looking for 2)})
29
+ end
30
+
31
+ it 'raises ActiveRecord::RecordNotFound if any of the given `values` has no record.', activerecord: [ '5.0', '5.1' ] do
32
+ expect { author_2.book_serials = [ book_1.serial, book_2.serial.next ] }
33
+ .to raise_exception(ActiveRecord::RecordNotFound,
34
+ %{Couldn't find all Books with 'serial': ("#{book_1.serial}", "#{book_2.serial.next}")} +
35
+ %{ (found 1 results, but was looking for 2)})
36
+ end
37
+
38
+ it 'raises ActiveRecord::RecordNotFound if any of the given `values` has no record.', activerecord: [ '5.2' ] do
39
+ expect { author_2.book_serials = [ book_1.serial, book_2.serial.next ] }
40
+ .to raise_exception(ActiveRecord::RecordNotFound,
41
+ %{Couldn't find all Books with 'serial': ("#{book_1.serial}", "#{book_2.serial.next}")} +
42
+ %{ (found 1 results, but was looking for 2). Couldn't find Book with serial "#{book_2.serial.next}".})
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,27 @@
1
+ RSpec.describe 'for `has_many through:` association' do
2
+ let!(:author_1) { Author.create! }
3
+ let!(:author_2) { Author.create! }
4
+ let!(:book_1) { Book.create! author: author_1 }
5
+ let!(:book_2) { Book.create! author: author_2 }
6
+ let!(:chapter_1) { Chapter.create! book: book_1 }
7
+ let!(:chapter_2) { Chapter.create! book: book_2 }
8
+
9
+ it 'generates `#[association_name.singularize]_[attribute_name.pluralize]` accessor.' do
10
+ expect(author_1).to respond_to :chapter_serials, :chapter_serials=
11
+ end
12
+
13
+ describe '`#[association_name.singularize]_[attribute_name.pluralize]`' do
14
+ it 'returns an array with the values of `[attribute]` of the associated records.' do
15
+ expect(author_1.chapter_serials).to eq [ chapter_1.serial ]
16
+ expect(author_2.chapter_serials).to eq [ chapter_2.serial ]
17
+ end
18
+ end
19
+
20
+ describe '`#[association_name.singularize]_[attribute_name.pluralize]`=(values)' do
21
+ it 'raises ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.' do
22
+ expect { author_1.chapter_serials = [] }
23
+ .to raise_exception(ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection,
24
+ %{Cannot modify association 'Author#chapters' because the source reflection class 'Chapter' is associated to 'Book' via :has_many.})
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,33 @@
1
+ RSpec.describe 'for `has_one` association' do
2
+ let!(:author) { Author.create! }
3
+ let!(:address) { Address.create! author: author }
4
+
5
+ it 'generates `#[association_name]_[attribute_name]` accessor.' do
6
+ expect(author).to respond_to :address_serial, :address_serial=
7
+ end
8
+
9
+ describe '`#[association_name]_[attribute_name]`' do
10
+ it 'returns the value of the `[attribute]` of the associated record if there is any.' do
11
+ expect { author.address = nil }.to change(author, :address_serial).from(address.serial).to nil
12
+ end
13
+ end
14
+
15
+ describe '`#[association_name]_[attribute_name]=(value)`' do
16
+ it 'sets the association value to nil if `value` is nil.' do
17
+ expect { author.address_serial = nil }.to change(author, :address).from(address).to nil
18
+ end
19
+
20
+ it 'sets the association value to the record queried by its `[attribute]` having the value `value` if such record exists.' do
21
+ new_address = Address.create!
22
+ expect { author.address_serial = new_address.serial }.to change(author, :address).from(address).to new_address
23
+ end
24
+
25
+ it 'raises ActiveRecord::RecordNotFound if record with `[attribute]` = `value` does not exist.', activerecord: [ '4.1' ] do
26
+ expect { author.address_serial = address.serial.next }.to raise_exception ActiveRecord::RecordNotFound
27
+ end
28
+
29
+ it 'raises ActiveRecord::RecordNotFound if record with `[attribute]` = `value` does not exist.', activerecord: [ '4.2', '5' ] do
30
+ expect { author.address_serial = address.serial.next }.to raise_exception ActiveRecord::RecordNotFound, "Couldn't find Address"
31
+ end
32
+ end
33
+ end