activerecord-mti 0.0.0

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