activerecord-chemistry 0.1.0

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