activerecord-mti 0.0.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 244959ce054c69902aba2b4394e0b36d97fdf5ee
4
+ data.tar.gz: b495b7183d09b9559aae287cbb6cc380c63ead0e
5
+ SHA512:
6
+ metadata.gz: 5aa32b7fae5c39a39a8541a1aa1f18a269ae3a1b41aa7af7639888021925a333df1d446c0e67acec56e714b7bc7a61599ffbc16eb4d83265fcd9a7bdfaea6353
7
+ data.tar.gz: 7b60164496b9b14a37ce13c3caae1421ce3c1766eb64427d43c97fe49ecbe11f02c7fe7ad849582f63b090223c2a4234c8433a2c45de1ce49611dd04e2fba079
@@ -0,0 +1,3 @@
1
+ .rvmrc
2
+ .idea
3
+ mti_spec_db
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'rails', '>= 3.2'
4
+
5
+ group :development, :test do
6
+ gem 'rspec-rails', '~> 2.14.0'
7
+ gem 'sqlite3'
8
+ end
@@ -0,0 +1,95 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ actionmailer (4.0.0)
5
+ actionpack (= 4.0.0)
6
+ mail (~> 2.5.3)
7
+ actionpack (4.0.0)
8
+ activesupport (= 4.0.0)
9
+ builder (~> 3.1.0)
10
+ erubis (~> 2.7.0)
11
+ rack (~> 1.5.2)
12
+ rack-test (~> 0.6.2)
13
+ activemodel (4.0.0)
14
+ activesupport (= 4.0.0)
15
+ builder (~> 3.1.0)
16
+ activerecord (4.0.0)
17
+ activemodel (= 4.0.0)
18
+ activerecord-deprecated_finders (~> 1.0.2)
19
+ activesupport (= 4.0.0)
20
+ arel (~> 4.0.0)
21
+ activerecord-deprecated_finders (1.0.3)
22
+ activesupport (4.0.0)
23
+ i18n (~> 0.6, >= 0.6.4)
24
+ minitest (~> 4.2)
25
+ multi_json (~> 1.3)
26
+ thread_safe (~> 0.1)
27
+ tzinfo (~> 0.3.37)
28
+ arel (4.0.0)
29
+ atomic (1.1.14)
30
+ builder (3.1.4)
31
+ diff-lcs (1.2.4)
32
+ erubis (2.7.0)
33
+ hike (1.2.3)
34
+ i18n (0.6.9)
35
+ mail (2.5.4)
36
+ mime-types (~> 1.16)
37
+ treetop (~> 1.4.8)
38
+ mime-types (1.25.1)
39
+ minitest (4.7.5)
40
+ multi_json (1.8.2)
41
+ polyglot (0.3.3)
42
+ rack (1.5.2)
43
+ rack-test (0.6.2)
44
+ rack (>= 1.0)
45
+ rails (4.0.0)
46
+ actionmailer (= 4.0.0)
47
+ actionpack (= 4.0.0)
48
+ activerecord (= 4.0.0)
49
+ activesupport (= 4.0.0)
50
+ bundler (>= 1.3.0, < 2.0)
51
+ railties (= 4.0.0)
52
+ sprockets-rails (~> 2.0.0)
53
+ railties (4.0.0)
54
+ actionpack (= 4.0.0)
55
+ activesupport (= 4.0.0)
56
+ rake (>= 0.8.7)
57
+ thor (>= 0.18.1, < 2.0)
58
+ rake (10.1.0)
59
+ rspec-core (2.14.5)
60
+ rspec-expectations (2.14.3)
61
+ diff-lcs (>= 1.1.3, < 2.0)
62
+ rspec-mocks (2.14.3)
63
+ rspec-rails (2.14.0)
64
+ actionpack (>= 3.0)
65
+ activesupport (>= 3.0)
66
+ railties (>= 3.0)
67
+ rspec-core (~> 2.14.0)
68
+ rspec-expectations (~> 2.14.0)
69
+ rspec-mocks (~> 2.14.0)
70
+ sprockets (2.10.0)
71
+ hike (~> 1.2)
72
+ multi_json (~> 1.0)
73
+ rack (~> 1.0)
74
+ tilt (~> 1.1, != 1.3.0)
75
+ sprockets-rails (2.0.0)
76
+ actionpack (>= 3.0)
77
+ activesupport (>= 3.0)
78
+ sprockets (~> 2.8)
79
+ sqlite3 (1.3.8)
80
+ thor (0.18.1)
81
+ thread_safe (0.1.3)
82
+ atomic
83
+ tilt (1.4.1)
84
+ treetop (1.4.15)
85
+ polyglot
86
+ polyglot (>= 0.3.1)
87
+ tzinfo (0.3.38)
88
+
89
+ PLATFORMS
90
+ ruby
91
+
92
+ DEPENDENCIES
93
+ rails (>= 3.2)
94
+ rspec-rails (~> 2.14.0)
95
+ sqlite3
@@ -0,0 +1,70 @@
1
+ # ActiveRecord MTI (Multiple Tables Inheritance)
2
+
3
+ This gem allows you to make models which attributes are distributed by two tables.
4
+
5
+ ## Installation
6
+
7
+ Add to your Gemfile:
8
+
9
+ ```gem 'activerecord-mti'```
10
+
11
+ ## Usage
12
+
13
+ Consider the following DB schema:
14
+
15
+ ```ruby
16
+ create_table :subjects do |t|
17
+ t.string :name
18
+ end
19
+
20
+ create_table :roles do |t|
21
+ t.integer :subject_id
22
+ # MTI fields
23
+ t.integer :role_id
24
+ t.integer :role_type
25
+ end
26
+
27
+ create_table :employees do |t|
28
+ t.string :appointment
29
+ end
30
+
31
+ create_table :clients do |t|
32
+ t.string :address
33
+ end
34
+ ```
35
+
36
+ and corresponding models:
37
+
38
+ ```ruby
39
+ class Subject < ActiveRecord::Base
40
+ has_many :roles
41
+ end
42
+
43
+ class Role < ActiveRecord::Base
44
+ mti_base
45
+ belongs_to :subject
46
+ end
47
+
48
+ class Employee < ActiveRecord::Base
49
+ mti_implementation_of :role
50
+ end
51
+
52
+ class Client < ActiveRecord::Base
53
+ mti_implementation_of :role
54
+ end
55
+ ```
56
+
57
+ Have fun with roles as base and implementation objects at the same time:
58
+
59
+ ```ruby
60
+ Subject.first.roles # => [#<Employee …>, #<Client …>]
61
+ Employee.first.subject # => #<User …>
62
+ Role.where(role_type: Client).first.address # => String
63
+ Client.first.role # => #<Role …>
64
+ Client.first.role_id == Client.first.role.id # => true
65
+ Client.create!(:address => 'somewhere').role # => #<Role …>
66
+ ```
67
+
68
+ ## Testing
69
+
70
+ ```bundle exec rspec spec```
@@ -0,0 +1,18 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'activerecord-mti'
3
+ s.version = '0.0.0'
4
+ s.date = '2014-06-25'
5
+ s.summary = 'ActiveRecord MTI'
6
+ s.description = 'Multiple Tables Inheritance for ActiveRecord'
7
+ s.authors = ['Timofey Martynov']
8
+ s.email = 'feymartynov@gmail.com'
9
+ s.homepage = 'http://rubygems.org/gems/activerecord-mti'
10
+ s.license = 'MIT'
11
+
12
+ s.require_paths = ['lib']
13
+ s.files = `git ls-files`.split("\n")
14
+ s.test_files = `git ls-files -- spec/*`.split("\n")
15
+
16
+ s.add_dependency 'rails', '>= 3.2'
17
+ s.add_development_dependency 'rspec-rails', '~> 2.14.0'
18
+ end
@@ -0,0 +1,9 @@
1
+ require 'delegate_missing_to'
2
+ require 'mti'
3
+
4
+ module ActiveRecord
5
+ class Base
6
+ include DelegateMissingTo
7
+ include ActiveRecord::Mti
8
+ end
9
+ end
@@ -0,0 +1,46 @@
1
+ # Delegates missing methods to another object(s).
2
+ # May be useful for inheritance mechanisms, decorator pattern or graceful object replacement for refactoring.
3
+ #
4
+ # Example with delegation of missing methods to an association:
5
+ #
6
+ # class A < ActiveRecord::Base
7
+ # def qwe
8
+ # 123
9
+ # end
10
+ # end
11
+ #
12
+ # class B < ActiveRecord::Base
13
+ # include ActiveRecord::DelegateMissingTo
14
+ #
15
+ # belongs_to :a
16
+ # delegate_missing_to :a
17
+ # end
18
+ #
19
+ # b = B.new
20
+ # b.qwe # => 123
21
+ #
22
+ # Additionally you may specify a delegation chain:
23
+ #
24
+ # delegate_missing_to :first_priority, :second_priority, :third_priority
25
+ #
26
+ module DelegateMissingTo
27
+ extend ActiveSupport::Concern
28
+
29
+ module ClassMethods
30
+ def delegate_missing_to(*object_names)
31
+ object_names.reverse_each do |object_name|
32
+ define_method "method_missing_with_delegation_to_#{object_name}" do |method, *args, &block|
33
+ object = send(object_name)
34
+
35
+ if object.respond_to?(method)
36
+ object.public_send(method, *args, &block)
37
+ else
38
+ send("method_missing_without_delegation_to_#{object_name}", method, *args, &block)
39
+ end
40
+ end
41
+
42
+ alias_method_chain :method_missing, "delegation_to_#{object_name}"
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,118 @@
1
+ # Multiple Table Inheritance (MTI)
2
+ #
3
+ # Say, you have a base model (Fruit) and its implementations (Apple & Banana).
4
+ # STI is not suitable because you have to keep Apple's & Banana's specific fields in one table (fruits).
5
+ # MTI enables you to keep a clear database schema with a table for common fields only (fruits)
6
+ # and two tables for specific fields only (apples & bananas).
7
+ # This implementation is based on delegation pattern and doesn't use class inheritance.
8
+ #
9
+ # Example:
10
+ #
11
+ # class Fruit < ActiveRecord::Base
12
+ # mti_base
13
+ # end
14
+ #
15
+ # class Apple < ActiveRecord::Base
16
+ # mti_implementation_of :fruit
17
+ # end
18
+ #
19
+ # class Banana < ActiveRecord::Base
20
+ # mti_implementation_of :fruit
21
+ # end
22
+ #
23
+ module ActiveRecord::Mti
24
+ extend ActiveSupport::Concern
25
+
26
+ module ClassMethods
27
+ def mti_base
28
+ class_attribute :mti_name
29
+ self.mti_name = self.to_s.underscore.to_sym
30
+
31
+ @@base_instance_mode = false
32
+ @@base_instance_mode_lock = Mutex.new
33
+
34
+ # Always fetch with the implementation
35
+ default_scope lambda { includes(mti_name) }
36
+
37
+ # Implementation model association
38
+ belongs_to mti_name,
39
+ polymorphic: true,
40
+ inverse_of: mti_name
41
+
42
+ # Override ActiveRecord's instantiation method
43
+ # which builds an object from a record
44
+ # Return the base class object when in "base instance mode"
45
+ # and the implementation object otherwise
46
+ def self.instantiate(*_)
47
+ if @@base_instance_mode
48
+ super
49
+ else
50
+ super.public_send(mti_name)
51
+ end
52
+ end
53
+
54
+ # Thread safe execution of a block in a "base instance mode"
55
+ def self.as_base_instance
56
+ @@base_instance_mode_lock.synchronize do
57
+ @@base_instance_mode = true
58
+ result = yield
59
+ @@base_instance_mode = false
60
+ result
61
+ end
62
+ end
63
+ end
64
+
65
+ def mti_implementation_of(mti_base_name)
66
+ class_attribute :mti_base
67
+ self.mti_base = mti_base_name.to_s.classify.constantize
68
+
69
+ # Base model association
70
+ has_one mti_base_name.to_sym,
71
+ :as => mti_base_name.to_sym,
72
+ :autosave => true,
73
+ :dependent => :destroy,
74
+ :validate => true,
75
+ :inverse_of => mti_base_name.to_sym
76
+
77
+ # When calling the base object from the implementation
78
+ # switch the base's class to the "base instance mode"
79
+ # to receive the base class object instead of another
80
+ # implementation object and avoid an infinite loop
81
+ define_method "#{mti_base_name}_with_reverse" do # def role_with_reverse
82
+ mti_base.as_base_instance do # Role.as_base_instance do
83
+ send("#{mti_base_name}_without_reverse") # role_without_reverse
84
+ end # end
85
+ end # end
86
+ alias_method_chain mti_base_name, :reverse # alias_method_chain :role, :reverse
87
+
88
+ # Auto build base model
89
+ define_method "#{mti_base_name}_with_autobuild" do # def role_with_autobuild
90
+ public_send("#{mti_base_name}_without_autobuild") || # role_without_autobuild ||
91
+ public_send("build_#{mti_base_name}") # build_role
92
+ end # end
93
+ alias_method_chain mti_base_name, :autobuild # alias_method_chain :role, :autobuild
94
+
95
+ # Delegate attributes
96
+ mti_base.content_columns.map(&:name).each do |attr|
97
+ delegate attr, "#{attr}=", "#{attr}?",
98
+ :to => mti_base_name.to_sym
99
+ end
100
+
101
+ # Delegate associations
102
+ mti_base.reflections.keys
103
+ .tap { |k| k.delete(mti_base_name.to_sym) }
104
+ .each do |association|
105
+ delegate association, "#{association}=",
106
+ :to => mti_base_name.to_sym
107
+ end
108
+
109
+ delegate_missing_to mti_base_name
110
+
111
+ accepts_nested_attributes_for mti_base_name
112
+
113
+ define_method "#{mti_base_name}_id" do
114
+ public_send(mti_base_name).id
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+
3
+ describe '.delegate_missing_to' do
4
+ before :all do
5
+ class DummyWorker
6
+ def do_work(*_)
7
+ end
8
+ end
9
+
10
+ class DummyDelegator
11
+ include DelegateMissingTo
12
+
13
+ delegate_missing_to :worker
14
+
15
+ def worker
16
+ @worker ||= DummyWorker.new
17
+ end
18
+ end
19
+ end
20
+
21
+ subject(:delegator) { DummyDelegator.new }
22
+ let(:params) { [:some, :params] }
23
+
24
+ it 'should delegate missing method call' do
25
+ expect(delegator.worker).to receive(:do_work).with(*params)
26
+ delegator.do_work(*params)
27
+ end
28
+ end
@@ -0,0 +1,141 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'MTI' do
4
+ before :all do
5
+ ActiveRecord::Base.establish_connection(:adapter => :sqlite3, :database => 'mti_spec_db')
6
+
7
+ ActiveRecord::Schema.define do
8
+ self.verbose = false
9
+
10
+ create_table :owners, force: true do |t|
11
+ t.string :name
12
+ end
13
+
14
+ create_table :devices, force: true do |t|
15
+ t.string :name
16
+ t.integer :owner_id
17
+ t.integer :device_id
18
+ t.string :device_type
19
+ end
20
+
21
+ create_table :computers, force: true do |t|
22
+ t.string :cpu_model
23
+ end
24
+
25
+ create_table :cameras, force: true do |t|
26
+ t.float :matrix_size
27
+ end
28
+ end
29
+
30
+ ActiveRecord::Base.transaction do
31
+ ActiveRecord::Base.connection.execute(<<-SQL)
32
+ INSERT INTO owners (id, name)
33
+ VALUES (1, 'john doe');
34
+ SQL
35
+
36
+ ActiveRecord::Base.connection.execute(<<-SQL)
37
+ INSERT INTO devices (id, name, owner_id, device_id, device_type)
38
+ VALUES (1, 'mac book pro', 1, 1, 'Computer');
39
+ SQL
40
+
41
+ ActiveRecord::Base.connection.execute(<<-SQL)
42
+ INSERT INTO computers (id, cpu_model)
43
+ VALUES (1, 'core i7');
44
+ SQL
45
+
46
+ ActiveRecord::Base.connection.execute(<<-SQL)
47
+ INSERT INTO devices (id, name, owner_id, device_id, device_type)
48
+ VALUES (2, 'canon 550d', 1, 1, 'Camera');
49
+ SQL
50
+
51
+ ActiveRecord::Base.connection.execute(<<-SQL)
52
+ INSERT INTO cameras (id, matrix_size)
53
+ VALUES (1, 18.7);
54
+ SQL
55
+ end
56
+
57
+ class Owner < ActiveRecord::Base; end
58
+
59
+ class Device < ActiveRecord::Base
60
+ mti_base
61
+
62
+ belongs_to :owner
63
+
64
+ def switch_on
65
+ :on
66
+ end
67
+ end
68
+
69
+ class Computer < ActiveRecord::Base
70
+ mti_implementation_of :device
71
+
72
+ def run_program
73
+ :done
74
+ end
75
+ end
76
+
77
+ class Camera < ActiveRecord::Base
78
+ mti_implementation_of :device
79
+
80
+ def make_shot
81
+ :click
82
+ end
83
+ end
84
+ end
85
+
86
+ after(:all) do
87
+ ActiveRecord::Schema.define do
88
+ drop_table :devices
89
+ drop_table :computers
90
+ drop_table :cameras
91
+ end
92
+ end
93
+
94
+ describe 'base' do
95
+ it 'finders should return the implementation' do
96
+ computer = Device.find_by_name('mac book pro')
97
+ expect(computer).to be_kind_of(Computer)
98
+ end
99
+ end
100
+
101
+ describe 'implementation' do
102
+ subject(:camera) { Camera.find(1) }
103
+
104
+ it 'should have attributes of the implementation' do
105
+ expect(camera.matrix_size).to eq(18.7)
106
+ end
107
+
108
+ it 'should have attributes of the base object' do
109
+ expect(camera.name).to eq('canon 550d')
110
+ end
111
+
112
+ it 'should act as an implementation' do
113
+ expect(camera.make_shot).to eq(:click)
114
+ end
115
+
116
+ it 'should act as a base object' do
117
+ expect(camera.switch_on).to eq(:on)
118
+ end
119
+
120
+ it 'should return the base object on demand' do
121
+ expect(camera.device).to be_kind_of(Device)
122
+ end
123
+
124
+ it 'should respond to base object\'s associations' do
125
+ expect(camera.owner.name).to eq('john doe')
126
+ end
127
+ end
128
+
129
+ describe 'building a model' do
130
+ before do
131
+ Computer.create! do |c|
132
+ c.name = 'mac book air'
133
+ c.cpu_model = 'core i3'
134
+ end
135
+ end
136
+
137
+ it 'should build both base and implementation' do
138
+ expect(Device.where(name: 'mac book air').first.cpu_model).to eq('core i3')
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,9 @@
1
+ require 'rails/all'
2
+ require 'rspec/rails'
3
+ require 'activerecord-mti'
4
+
5
+ RSpec.configure do |config|
6
+ config.color_enabled = true
7
+ config.tty = true
8
+ config.formatter = :documentation
9
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activerecord-mti
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Timofey Martynov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-06-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '3.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '3.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec-rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: 2.14.0
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: 2.14.0
41
+ description: Multiple Tables Inheritance for ActiveRecord
42
+ email: feymartynov@gmail.com
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - .gitignore
48
+ - Gemfile
49
+ - Gemfile.lock
50
+ - README.md
51
+ - activerecord-mti.gemspec
52
+ - lib/activerecord-mti.rb
53
+ - lib/delegate_missing_to.rb
54
+ - lib/mti.rb
55
+ - spec/delegate_missing_to_spec.rb
56
+ - spec/mti_spec.rb
57
+ - spec/spec_helper.rb
58
+ homepage: http://rubygems.org/gems/activerecord-mti
59
+ licenses:
60
+ - MIT
61
+ metadata: {}
62
+ post_install_message:
63
+ rdoc_options: []
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - '>='
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ requirements: []
77
+ rubyforge_project:
78
+ rubygems_version: 2.0.14
79
+ signing_key:
80
+ specification_version: 4
81
+ summary: ActiveRecord MTI
82
+ test_files:
83
+ - spec/delegate_missing_to_spec.rb
84
+ - spec/mti_spec.rb
85
+ - spec/spec_helper.rb