activerecord-chemistry 0.1.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 (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +1 -0
  4. data/.ruby-version +1 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +4 -0
  7. data/Gemfile.lock +95 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +42 -0
  10. data/Rakefile +2 -0
  11. data/activerecord-chemistry.gemspec +34 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +8 -0
  14. data/circle.yml +3 -0
  15. data/lib/activerecord/chemistry/actable/instance_methods.rb +36 -0
  16. data/lib/activerecord/chemistry/actable/migration.rb +27 -0
  17. data/lib/activerecord/chemistry/actable/relation.rb +39 -0
  18. data/lib/activerecord/chemistry/actable.rb +11 -0
  19. data/lib/activerecord/chemistry/extendable/instance_methods.rb +36 -0
  20. data/lib/activerecord/chemistry/extendable/migration.rb +27 -0
  21. data/lib/activerecord/chemistry/extendable/relation.rb +36 -0
  22. data/lib/activerecord/chemistry/extendable.rb +11 -0
  23. data/lib/activerecord/chemistry/representable/class_methods.rb +8 -0
  24. data/lib/activerecord/chemistry/representable/instance_methods.rb +49 -0
  25. data/lib/activerecord/chemistry/representable/migration.rb +27 -0
  26. data/lib/activerecord/chemistry/representable/relation.rb +49 -0
  27. data/lib/activerecord/chemistry/representable.rb +11 -0
  28. data/lib/activerecord/chemistry/version.rb +5 -0
  29. data/lib/activerecord/chemistry.rb +30 -0
  30. data/spec/actable/actable_spec.rb +5 -0
  31. data/spec/actable/acts_as_spec.rb +63 -0
  32. data/spec/database_helper.rb +26 -0
  33. data/spec/extendable/extendable_by_spec.rb +5 -0
  34. data/spec/extendable/extends_spec.rb +63 -0
  35. data/spec/factories.rb +22 -0
  36. data/spec/migrations_spec.rb +128 -0
  37. data/spec/models.rb +56 -0
  38. data/spec/representable/representable_spec.rb +5 -0
  39. data/spec/representable/represents_spec.rb +91 -0
  40. data/spec/spec_helper.rb +35 -0
  41. metadata +223 -0
@@ -0,0 +1,49 @@
1
+ require 'activerecord/chemistry/representable/instance_methods'
2
+ require 'activerecord/chemistry/representable/class_methods'
3
+
4
+ module ActiveRecord
5
+ module Chemistry
6
+ module Representable
7
+ module Relation
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ end
12
+
13
+ class_methods do
14
+ def represents(name, scope = nil, options = {})
15
+ if Hash === scope
16
+ options = scope
17
+ scope = nil
18
+ end
19
+ reflections = belongs_to(name, scope, options)
20
+ default_scope -> { includes(name) }
21
+ validate :representable_must_be_valid
22
+
23
+ before_validation :find_by_or_keep_representable, on: :create
24
+ before_validation :update_or_instantiate_representable, on: :update
25
+
26
+ after_destroy do
27
+ representing.destroy if representing && representing.send(representing_model.representable_reflection.name).count == 0 && !representing.destroyed?
28
+ end
29
+
30
+ cattr_reader(:representing_reflection) { reflections.stringify_keys[name.to_s] }
31
+ cattr_reader(:representing_model) { (options[:class_name] || name.to_s.camelize).constantize }
32
+ class_eval "def #{name}; super || build_#{name}(representing_model.representable_reflection.name => [self]); end"
33
+ alias_method :representing, name
34
+ alias_method :representing=, "#{name}=".to_sym
35
+ include Representable::InstanceMethods
36
+ singleton_class.module_eval do
37
+ include Representable::ClassMethods
38
+ end
39
+ end
40
+
41
+ def representable(name, scope = nil, options = {})
42
+ reflections = has_many(name, scope, options.merge(dependent: :destroy))
43
+ cattr_reader(:representable_reflection) { reflections.stringify_keys[name.to_s] }
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,11 @@
1
+ require 'activerecord/chemistry/representable/migration'
2
+ require 'activerecord/chemistry/representable/relation'
3
+
4
+ module ActiveRecord
5
+ module Chemistry
6
+ module Representable
7
+ include ActiveRecord::Chemistry::Representable::Migration
8
+ include ActiveRecord::Chemistry::Representable::Relation
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ module ActiveRecord
2
+ module Chemistry
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,30 @@
1
+ require 'active_support'
2
+ require 'active_record'
3
+ require 'activerecord/chemistry/version'
4
+ require 'activerecord/chemistry/actable'
5
+ require 'activerecord/chemistry/extendable'
6
+ require 'activerecord/chemistry/representable'
7
+
8
+ module ActiveRecord
9
+ class Base
10
+ include ActiveRecord::Chemistry::Actable::Relation
11
+ include ActiveRecord::Chemistry::Extendable::Relation
12
+ include ActiveRecord::Chemistry::Representable::Relation
13
+ end
14
+
15
+ module ConnectionAdapters
16
+ class TableDefinition
17
+ include ActiveRecord::Chemistry::Actable::Migration::TableDefinition
18
+ include ActiveRecord::Chemistry::Extendable::Migration::TableDefinition
19
+ include ActiveRecord::Chemistry::Representable::Migration::TableDefinition
20
+ end
21
+ end
22
+
23
+ module ConnectionAdapters
24
+ class Table
25
+ include ActiveRecord::Chemistry::Actable::Migration::Table
26
+ include ActiveRecord::Chemistry::Extendable::Migration::Table
27
+ include ActiveRecord::Chemistry::Representable::Migration::Table
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,5 @@
1
+ require 'models'
2
+
3
+ RSpec.describe "ActiveRecord::Base subclass with #actable" do
4
+ subject { Person }
5
+ end
@@ -0,0 +1,63 @@
1
+ require 'models'
2
+
3
+ RSpec.describe "ActiveRecord::Base subclass with #acts_as" do
4
+ before { initialize_schema }
5
+
6
+ context 'new' do
7
+ let(:user) { create(:user) }
8
+ it 'is valid with valid attributes for actable' do
9
+ expect(user).to be_valid
10
+ end
11
+
12
+ it 'is not valid with invalid actable attributes' do
13
+ user.slug = 2
14
+ expect(user).to_not be_valid
15
+ end
16
+ end
17
+
18
+ context 'create' do
19
+ let(:user) { create(:user, slug: 'username') }
20
+ context 'without existing actable' do
21
+ it 'creates a new actable object' do
22
+ expect(user.acting_as).to be
23
+ expect(user.acting_as).to be_a(Person)
24
+ expect(user.acting_as.slug).to eq('username')
25
+ end
26
+ end
27
+ context 'with existing actable' do
28
+ before { @person = create(:person, slug: 'username') }
29
+ it 'creates a new actable object' do
30
+ expect(user.acting_as).to be
31
+ expect(user.acting_as).to be_a(Person)
32
+ expect(user.acting_as.slug).to eq('username')
33
+ expect(user.acting_as).not_to eq(@person)
34
+ end
35
+ end
36
+ end
37
+
38
+ context 'destroy' do
39
+ before do
40
+ @user = create(:user, slug: 'username')
41
+ @person = @user.acting_as
42
+ @user.destroy
43
+ end
44
+
45
+ it 'destroys the actable object' do
46
+ expect(@user).to be_destroyed
47
+ expect(@person).to be_destroyed
48
+ end
49
+ end
50
+
51
+ context 'update' do
52
+ before do
53
+ @user = create(:user, slug: 'username')
54
+ @person = @user.acting_as
55
+ @user.update_attributes(slug: 'newname')
56
+ end
57
+
58
+ it 'changes the actable object' do
59
+ expect(@user.slug).to eq('newname')
60
+ expect(@person.slug).to eq('newname')
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,26 @@
1
+ require 'active_record'
2
+
3
+ def connect_to_database
4
+ ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ':memory:'
5
+ end
6
+
7
+ def clear_database
8
+ ActiveRecord::Base.descendants.each do |model|
9
+ model.delete_all if model.table_exists?
10
+ end
11
+ end
12
+
13
+ def reset_database
14
+ ActiveRecord::Base.descendants.map(&:reset_column_information)
15
+ ActiveRecord::Base.connection.disconnect!
16
+ connect_to_database
17
+ end
18
+
19
+ def initialize_database(&block)
20
+ reset_database
21
+ ActiveRecord::Schema.define(&block)
22
+ end
23
+
24
+ I18n.enforce_available_locales = false
25
+ ActiveRecord::Migration.verbose = false
26
+ connect_to_database
@@ -0,0 +1,5 @@
1
+ require 'models'
2
+
3
+ RSpec.describe "ActiveRecord::Base subclass with #extendable_by" do
4
+ subject { Email }
5
+ end
@@ -0,0 +1,63 @@
1
+ require 'models'
2
+
3
+ RSpec.describe "ActiveRecord::Base subclass with #extends" do
4
+ before { initialize_schema }
5
+
6
+ context 'new' do
7
+ let(:confirmable) { create(:confirmable) }
8
+ it 'is valid with valid attributes for extendable' do
9
+ expect(confirmable).to be_valid
10
+ end
11
+
12
+ it 'is not valid with invalid extendable attributes' do
13
+ confirmable.address = 2
14
+ expect(confirmable).to_not be_valid
15
+ end
16
+ end
17
+
18
+ context 'create' do
19
+ let(:confirmable) { create(:confirmable, address: 'test@email.com') }
20
+ context 'without existing extendable' do
21
+ it 'creates a new extendable object' do
22
+ expect(confirmable.extendable).to be
23
+ expect(confirmable.extendable).to be_a(Email)
24
+ expect(confirmable.extendable.address).to eq('test@email.com')
25
+ end
26
+ end
27
+ context 'with existing extendable' do
28
+ before { @email = create(:email, address: 'test@email.com') }
29
+ it 'creates a new extendable object' do
30
+ expect(confirmable.extendable).to be
31
+ expect(confirmable.extendable).to be_a(Email)
32
+ expect(confirmable.extendable.address).to eq('test@email.com')
33
+ expect(confirmable.extendable).not_to eq(@email)
34
+ end
35
+ end
36
+ end
37
+
38
+ context 'destroy' do
39
+ before do
40
+ @confirmable = create(:confirmable, address: 'test@email.com')
41
+ @email = @confirmable.extendable
42
+ @confirmable.destroy
43
+ end
44
+
45
+ it 'does not destroy the extendable object' do
46
+ expect(@confirmable).to be_destroyed
47
+ expect(@email).to_not be_destroyed
48
+ end
49
+ end
50
+
51
+ context 'update' do
52
+ before do
53
+ @confirmable = create(:confirmable, address: 'test@email.com')
54
+ @email = @confirmable.extendable
55
+ @confirmable.update_attributes(address: 'newname')
56
+ end
57
+
58
+ it 'changes the extendable object' do
59
+ expect(@confirmable.address).to eq('newname')
60
+ expect(@email.address).to eq('newname')
61
+ end
62
+ end
63
+ end
data/spec/factories.rb ADDED
@@ -0,0 +1,22 @@
1
+ FactoryGirl.define do
2
+ factory :person do
3
+ end
4
+
5
+ factory :user do
6
+ slug 'username'
7
+ end
8
+
9
+ factory :personal_name do
10
+ end
11
+
12
+ factory :display_name do
13
+ appellation 'John Doe'
14
+ end
15
+
16
+ factory :confirmable do
17
+ address 'test@email.com'
18
+ end
19
+
20
+ factory :email do
21
+ end
22
+ end
@@ -0,0 +1,128 @@
1
+ require 'database_helper'
2
+ require 'activerecord/chemistry'
3
+
4
+ class User < ActiveRecord::Base
5
+ end
6
+
7
+ class DisplayName < ActiveRecord::Base
8
+ end
9
+
10
+ class Email < ActiveRecord::Base
11
+ end
12
+
13
+ class RemoveActsAsFromUsers < ActiveRecord::Migration[5.1]
14
+ def change
15
+ change_table :users do |t|
16
+ t.remove_acts_as :person
17
+ end
18
+ end
19
+ end
20
+
21
+ class RemoveRepresentsFromDisplayNames < ActiveRecord::Migration[5.1]
22
+ def change
23
+ change_table :display_names do |t|
24
+ t.remove_represents :personal_name
25
+ end
26
+ end
27
+ end
28
+
29
+ class RemoveExtendedByFromEmails < ActiveRecord::Migration[5.1]
30
+ def change
31
+ change_table :emails do |t|
32
+ t.remove_extended_by :confirmable
33
+ end
34
+ end
35
+ end
36
+
37
+ RSpec.describe ".acts_as" do
38
+ context "in .create_table block" do
39
+ # after { initialize_schema }
40
+
41
+ it "creates reference columns with given names" do
42
+ initialize_database do
43
+ create_table(:people) { |t| t.string :name }
44
+ create_table(:users) { |t| t.acts_as :person }
45
+ end
46
+ expect(User.column_names).to include('person_id')
47
+ end
48
+ end
49
+ end
50
+
51
+ RSpec.describe ".remove_acts_as" do
52
+ context 'in .modify_table block' do
53
+ before do
54
+ initialize_database do
55
+ create_table(:people) { |t| t.string :name }
56
+ create_table(:users) { |t| t.acts_as :person }
57
+ end
58
+ end
59
+ it 'removes reference columns with given names' do
60
+ migration = RemoveActsAsFromUsers.new
61
+ migration.exec_migration(ActiveRecord::Base.connection, :up)
62
+ User.reset_column_information
63
+ expect(User.column_names).not_to include('person_id')
64
+ end
65
+ end
66
+ end
67
+
68
+ RSpec.describe ".represents" do
69
+ context "in .create_table block" do
70
+ # after { initialize_schema }
71
+
72
+ it "creates reference columns with given names" do
73
+ initialize_database do
74
+ create_table(:personal_names) { |t| t.string :name }
75
+ create_table(:display_names) { |t| t.represents :personal_name }
76
+ end
77
+ expect(DisplayName.column_names).to include('personal_name_id')
78
+ end
79
+ end
80
+ end
81
+
82
+ RSpec.describe ".remove_represents" do
83
+ context 'in .modify_table block' do
84
+ before do
85
+ initialize_database do
86
+ create_table(:personal_names) { |t| t.string :name }
87
+ create_table(:display_names) { |t| t.represents :personal_name }
88
+ end
89
+ end
90
+ it 'removes reference columns with given names' do
91
+ migration = RemoveRepresentsFromDisplayNames.new
92
+ migration.exec_migration(ActiveRecord::Base.connection, :up)
93
+ DisplayName.reset_column_information
94
+ expect(DisplayName.column_names).not_to include('personal_name_id')
95
+ end
96
+ end
97
+ end
98
+
99
+ RSpec.describe ".extended_by" do
100
+ context "in .create_table block" do
101
+ # after { initialize_schema }
102
+
103
+ it "creates reference columns with given names" do
104
+ initialize_database do
105
+ create_table(:confirmables) { |t| t.string :name }
106
+ create_table(:emails) { |t| t.extended_by :confirmable }
107
+ end
108
+ expect(Email.column_names).to include('confirmable_id')
109
+ end
110
+ end
111
+ end
112
+
113
+ RSpec.describe ".remove_extended_by" do
114
+ context 'in .modify_table block' do
115
+ before do
116
+ initialize_database do
117
+ create_table(:confirmables) { |t| t.string :name }
118
+ create_table(:emails) { |t| t.extended_by :confirmable }
119
+ end
120
+ end
121
+ it 'removes reference columns with given names' do
122
+ migration = RemoveExtendedByFromEmails.new
123
+ migration.exec_migration(ActiveRecord::Base.connection, :up)
124
+ Email.reset_column_information
125
+ expect(Email.column_names).not_to include('confirmable_id')
126
+ end
127
+ end
128
+ end
data/spec/models.rb ADDED
@@ -0,0 +1,56 @@
1
+ require_relative 'database_helper'
2
+
3
+ require 'activerecord/chemistry'
4
+
5
+ class Person < ActiveRecord::Base
6
+ actable :user
7
+ validates_format_of :slug, with: /\A[a-z]+\z/i
8
+ end
9
+
10
+ class User < ActiveRecord::Base
11
+ acts_as :person
12
+ end
13
+
14
+ class PersonalName < ActiveRecord::Base
15
+ representable :display_names
16
+ validates_format_of :appellation, with: /\A[a-z ]+\z/i
17
+ end
18
+
19
+ class DisplayName < ActiveRecord::Base
20
+ represents :personal_name
21
+ end
22
+
23
+ class Email < ActiveRecord::Base
24
+ extendable_by :confirmable
25
+ validates_format_of :address, with: /@/
26
+ end
27
+
28
+ class Confirmable < ActiveRecord::Base
29
+ extends :email
30
+ end
31
+
32
+ def initialize_schema
33
+ initialize_database do
34
+ create_table :people do |t|
35
+ t.string :slug
36
+ t.timestamps null: true
37
+ end
38
+ create_table :users do |t|
39
+ t.acts_as :person
40
+ end
41
+ create_table :personal_names do |t|
42
+ t.string :appellation
43
+ t.timestamps null: true
44
+ end
45
+ create_table :display_names do |t|
46
+ t.represents :personal_name
47
+ end
48
+ create_table :confirmables do |t|
49
+ end
50
+ create_table :emails do |t|
51
+ t.string :address
52
+ t.extended_by :confirmable
53
+ t.timestamps null: true
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,5 @@
1
+ require 'models'
2
+
3
+ RSpec.describe "ActiveRecord::Base subclass with #representable" do
4
+ subject { PersonalName }
5
+ end
@@ -0,0 +1,91 @@
1
+ require 'models'
2
+
3
+ RSpec.describe "ActiveRecord::Base subclass with #represents" do
4
+ before { initialize_schema }
5
+
6
+ context 'new' do
7
+ let(:display_name) { create(:display_name) }
8
+ it 'is valid with valid attributes for representable' do
9
+ expect(display_name).to be_valid
10
+ end
11
+
12
+ it 'is not valid with invalid representable attributes' do
13
+ display_name.appellation = 2
14
+ expect(display_name).to_not be_valid
15
+ end
16
+ end
17
+
18
+ context 'create' do
19
+ let(:display_name) { create(:display_name, appellation: 'John Doe') }
20
+ context 'without existing representable' do
21
+ it 'creates a new representable object' do
22
+ expect(display_name.representing).to be
23
+ expect(display_name.representing).to be_a(PersonalName)
24
+ expect(display_name.representing.appellation).to eq('John Doe')
25
+ end
26
+ end
27
+ context 'with existing representable' do
28
+ before { @personal_name = create(:personal_name, appellation: 'John Doe') }
29
+ it 'associates with existing representable object' do
30
+ expect(display_name.representing).to be
31
+ expect(display_name.representing).to be_a(PersonalName)
32
+ expect(display_name.representing.appellation).to eq('John Doe')
33
+ expect(display_name.representing).to eq(@personal_name)
34
+ end
35
+ end
36
+ end
37
+
38
+ context 'destroy' do
39
+ before do
40
+ @display_name = create(:display_name, appellation: 'John Doe')
41
+ @personal_name = @display_name.representing
42
+ end
43
+
44
+ context 'with no other representatives' do
45
+ before { @display_name.destroy }
46
+ it 'destroys the representable object' do
47
+ expect(@display_name).to be_destroyed
48
+ expect(@personal_name).to be_destroyed
49
+ end
50
+ end
51
+
52
+ context 'with other representatives' do
53
+ before do
54
+ create(:display_name, appellation: 'John Doe')
55
+ @display_name.destroy
56
+ end
57
+ it 'does not destroy the representable object' do
58
+ expect(@display_name).to be_destroyed
59
+ expect(@personal_name).to_not be_destroyed
60
+ end
61
+ end
62
+ end
63
+
64
+ context 'update' do
65
+ before do
66
+ @display_name = create(:display_name, appellation: 'John Doe')
67
+ @personal_name = @display_name.representing
68
+ end
69
+
70
+ context 'with no other representatives' do
71
+ before { @display_name.update_attributes(appellation: 'Dave Jones') }
72
+ it 'changes the representable object' do
73
+ expect(@display_name.appellation).to eq('Dave Jones')
74
+ expect(@personal_name.appellation).to eq('Dave Jones')
75
+ end
76
+ end
77
+
78
+ context 'with other representatives' do
79
+ before do
80
+ create(:display_name, appellation: 'John Doe')
81
+ @display_name.update_attributes(appellation: 'Dave Jones')
82
+ end
83
+ it 'does not change the representable object' do
84
+ expect(@display_name.appellation).to eq('Dave Jones')
85
+ @personal_name.reload
86
+ expect(@personal_name.appellation).to_not eq('Dave Jones')
87
+ expect(@personal_name.appellation).to eq('John Doe')
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,35 @@
1
+ require 'simplecov'
2
+
3
+ require 'coveralls'
4
+ # Coveralls.wear!
5
+
6
+ SimpleCov.formatter = Coveralls::SimpleCov::Formatter
7
+ SimpleCov.start do
8
+ add_filter 'spec/'
9
+ end
10
+
11
+ require 'factory_girl'
12
+
13
+ RSpec.configure do |config|
14
+ config.expect_with :rspec do |expectations|
15
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
16
+ end
17
+
18
+ config.mock_with :rspec do |mocks|
19
+ mocks.verify_partial_doubles = true
20
+ end
21
+
22
+ config.shared_context_metadata_behavior = :apply_to_host_groups
23
+
24
+ config.include FactoryGirl::Syntax::Methods
25
+
26
+ config.before(:suite) do
27
+ FactoryGirl.find_definitions
28
+ end
29
+ end
30
+
31
+ require 'active_record'
32
+ if ActiveRecord::Base.respond_to?(:raise_in_transactional_callbacks=)
33
+ ActiveRecord::Base.raise_in_transactional_callbacks = true
34
+ end
35
+