activerecord-model_inheritance 1.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.
- checksums.yaml +7 -0
- data/.rubocop.yml +451 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +8 -0
- data/LICENSE +21 -0
- data/README.md +255 -0
- data/Rakefile +7 -0
- data/lib/active_record/model_inheritance/error.rb +5 -0
- data/lib/active_record/model_inheritance/model.rb +169 -0
- data/lib/active_record/model_inheritance/version.rb +5 -0
- data/lib/active_record/model_inheritance/view_definition.rb +112 -0
- data/lib/active_record/model_inheritance.rb +57 -0
- data/lib/activerecord/model_inheritance.rb +1 -0
- data/lib/generators/active_record/model_inheritance/generators.rb +6 -0
- data/lib/generators/active_record/model_inheritance/model/model_generator.rb +75 -0
- data/lib/generators/active_record/model_inheritance/model/templates/definition.erb +1 -0
- data/lib/generators/active_record/model_inheritance/model/templates/model.erb +2 -0
- data/lib/generators/active_record/model_inheritance/model/templates/model_config.erb +3 -0
- data/lib/generators/active_record/model_inheritance/view/templates/create_migration.erb +13 -0
- data/lib/generators/active_record/model_inheritance/view/templates/update_migration.erb +5 -0
- data/lib/generators/active_record/model_inheritance/view/view_generator.rb +121 -0
- data/sig/active_record/model_inheritance/generators/model_generator.rbs +23 -0
- data/sig/active_record/model_inheritance/generators/view_generator.rbs +45 -0
- data/sig/active_record/model_inheritance/version.rbs +5 -0
- data/sig/active_record/model_inheritance/view_definition.rbs +20 -0
- metadata +127 -0
data/README.md
ADDED
@@ -0,0 +1,255 @@
|
|
1
|
+
# Model Inheritance
|
2
|
+
|
3
|
+
An attempt at real inheritance for ActiveRecord models.
|
4
|
+
|
5
|
+
This gem leverages database views (thanks to [Scenic](https://github.com/scenic-views/scenic)) to compose models
|
6
|
+
from other models, kind of like POROs inheritance [with limitations](#limitations). Views are defined using
|
7
|
+
[Arel](https://www.rubydoc.info/gems/arel) instead of SQL, which is cleaner and allows for easier integration.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'activerecord-model_inheritance', '~> 1.0'
|
15
|
+
```
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
```bash
|
20
|
+
bundle
|
21
|
+
```
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
First of all, if you want to make intensive use of this gem, being familiar on how [Scenic](https://github.com/scenic-views/scenic)
|
26
|
+
works is highly recommended.
|
27
|
+
|
28
|
+
### Quickstart
|
29
|
+
|
30
|
+
Assuming you want a new `DerivedModel` that inherits from a preexisting `BaseModel`, follow these steps:
|
31
|
+
|
32
|
+
- Generate the new model and its view definition:
|
33
|
+
```bash
|
34
|
+
rails g active_record:model_inheritance:model DerivedModel BaseModel
|
35
|
+
```
|
36
|
+
|
37
|
+
- Edit the generated model and definition, if needed
|
38
|
+
|
39
|
+
- Generate the SQL definition of the view and the initial migration:
|
40
|
+
```bash
|
41
|
+
rails g active_record:model_inheritance:view DerivedModel
|
42
|
+
```
|
43
|
+
|
44
|
+
- Edit the generated migration if needed
|
45
|
+
|
46
|
+
- Finally, run the migration:
|
47
|
+
```bash
|
48
|
+
rails db:migrate
|
49
|
+
```
|
50
|
+
|
51
|
+
Keep in mind that you need to generate a new version of the SQL definition whenever your view definition changes,
|
52
|
+
for example when you want to add some fields to the derived model.
|
53
|
+
To do so, just run the same generator as again:
|
54
|
+
```bash
|
55
|
+
rails g active_record:model_inheritance:view DerivedModel
|
56
|
+
```
|
57
|
+
This will take care of everything, including generating the migration to update the view.
|
58
|
+
It works similarly to Scenic.
|
59
|
+
|
60
|
+
### Concepts
|
61
|
+
|
62
|
+
A database view is like a _virtual table_ where the schema, as well as the data it contains, are
|
63
|
+
defined by a plain old SQL query. Of course, since views are just query results disguised as tables, you can't write into them.
|
64
|
+
So, at the end of the day, all this gem does is enabling write operations to Scenic view backed models.
|
65
|
+
|
66
|
+
To achieve something _resembling_ real inheritance between models, the **inner model** is introduced,
|
67
|
+
which is a third entity between the **base model** (the one you want to inherit from)
|
68
|
+
and the **derived model** (the new one you're creating).
|
69
|
+
|
70
|
+
The **inner model** holds the additional pieces your **derived model** should have.
|
71
|
+
|
72
|
+
When you apply changes to a **derived model**, those changes are mapped to **inner** and **base** models.
|
73
|
+
For example, if the **derived model** has the fields `foo` and `bar`, coming respectively from **inner** and **base** models,
|
74
|
+
changes to `foo` will be saved to the **inner model**, and changes to `bar` will be saved to the **base model**.
|
75
|
+
This way, the database view backing the **derived model** is always accessed in read-only mode.
|
76
|
+
|
77
|
+
### Configuration
|
78
|
+
|
79
|
+
If you're using Rails, the following is the code you would put inside an initializer
|
80
|
+
to configure this gem as it is configured by default. If you're ok with this defaults, then you don't need to
|
81
|
+
configure anything.
|
82
|
+
```ruby
|
83
|
+
# config/initializers/model_inheritance.rb
|
84
|
+
|
85
|
+
ActiveRecord::ModelInheritance.configure do |config|
|
86
|
+
## derived model options
|
87
|
+
|
88
|
+
# name of the dynamically generated inner model class
|
89
|
+
config.inner_class_name = 'Inner'
|
90
|
+
|
91
|
+
# base class of the dynamically generated inner model
|
92
|
+
config.inner_base_class = ApplicationRecord
|
93
|
+
|
94
|
+
# name of the belongs_to association from derived model to base model
|
95
|
+
config.base_reference_name = :model_inheritance_base
|
96
|
+
|
97
|
+
# name of the belongs_to association from derived model to its own inner model
|
98
|
+
config.inner_reference_name = :model_inheritance_inner
|
99
|
+
|
100
|
+
# whether to inherit enums from the base model
|
101
|
+
# only enums relevant to inherited fields will be added
|
102
|
+
config.inherit_enums = true
|
103
|
+
|
104
|
+
# whether to delegate missing methods from derived model to base model
|
105
|
+
config.delegate_missing_to_base = true
|
106
|
+
|
107
|
+
## paths options
|
108
|
+
|
109
|
+
# these are self explanatory
|
110
|
+
config.models_path = Rails.root.join('app/models')
|
111
|
+
config.migrations_path = Rails.root.join('db/migrate')
|
112
|
+
|
113
|
+
# where to save generated SQL definitions (Scenic default)
|
114
|
+
config.views_path = Rails.root.join('db/views')
|
115
|
+
|
116
|
+
# where to save view definitions
|
117
|
+
config.definitions_path = Rails.root.join('db/views/model_inheritance')
|
118
|
+
end
|
119
|
+
```
|
120
|
+
If you're not using Rails, the default configuration stays the same, except:
|
121
|
+
```ruby
|
122
|
+
config.inner_base_class = ActiveRecord::Base
|
123
|
+
|
124
|
+
config.models_path = Pathname('app/models')
|
125
|
+
config.migrations_path = Pathname('db/migrate')
|
126
|
+
|
127
|
+
config.views_path = Pathname('db/views')
|
128
|
+
config.definitions_path = Pathname('db/views/model_inheritance')
|
129
|
+
```
|
130
|
+
|
131
|
+
You can pass options to `derives_from` if you want to override the global derived models configuration on a per model basis:
|
132
|
+
```ruby
|
133
|
+
class DerivedModel < ApplicationRecord
|
134
|
+
include ActiveRecord::ModelInheritance::Model
|
135
|
+
|
136
|
+
derives_from BaseModel,
|
137
|
+
inner_class_name: 'Inner',
|
138
|
+
inner_base_class: ApplicationRecord,
|
139
|
+
base_reference_name: :model_inheritance_base,
|
140
|
+
inner_reference_name: :model_inheritance_inner,
|
141
|
+
inherit_enums: true,
|
142
|
+
delegate_missing_to_base: true
|
143
|
+
end
|
144
|
+
```
|
145
|
+
|
146
|
+
### View definitions
|
147
|
+
A view definition is responsible of:
|
148
|
+
- providing a convenient way of defining views using Arel
|
149
|
+
- keeping a map of which attributes belong respectively to the base and inner model
|
150
|
+
|
151
|
+
By default, the derived model will get **all** the fields from base and inner.
|
152
|
+
If that's not what you want, you can override the default behaviour like in the following example:
|
153
|
+
```ruby
|
154
|
+
# db/views/model_inheritance/derived_models.rb
|
155
|
+
|
156
|
+
ActiveRecord::ModelInheritance::ViewDefinition.define_derived_view DerivedModel do |inner_table, base_table|
|
157
|
+
inner_table
|
158
|
+
# all fields from inner
|
159
|
+
.project(inner_table[Arel.star])
|
160
|
+
# only some fields from base
|
161
|
+
.project(
|
162
|
+
base_table[:foo],
|
163
|
+
base_table[:bar],
|
164
|
+
base_table[:baz]
|
165
|
+
)
|
166
|
+
.join(base_table)
|
167
|
+
.on(inner_table[:model_inheritance_base_id].eq base_table[:id])
|
168
|
+
end
|
169
|
+
```
|
170
|
+
Here, Arel is used to describe how you want the base and inner table joined.
|
171
|
+
The block parameters `inner_table` and `base_table` are both [Arel::SelectTable](https://www.rubydoc.info/gems/arel/Arel/Table)s,
|
172
|
+
representing the inner model table and base model table respectively.
|
173
|
+
The code inside the block **must** evaluate to [Arel::SelectManager](https://www.rubydoc.info/gems/arel/Arel/SelectManager).
|
174
|
+
Note that if you set the option `base_reference_name` to something different to `:model_inheritance_base`, you have to
|
175
|
+
change the join condition accordingly.
|
176
|
+
|
177
|
+
When you run the `active_record:model_inheritance:view` generator, one of the things that's done is converting that Arel::SelectManager
|
178
|
+
(the default one or your custom provided one) to SQL. In the case of the above example, the generated SQL will look something
|
179
|
+
like this:
|
180
|
+
```sql
|
181
|
+
/* db/views/derived_models_v01.sql */
|
182
|
+
|
183
|
+
SELECT "derived_model_inners".*,
|
184
|
+
"base_models"."foo",
|
185
|
+
"base_models"."bar",
|
186
|
+
"base_models"."baz"
|
187
|
+
FROM "derived_model_inners"
|
188
|
+
INNER JOIN "base_models"
|
189
|
+
ON "derived_model_inners"."model_inheritance_base_id" = "base_models"."id"
|
190
|
+
```
|
191
|
+
This is how the database view backing the derived model will be created.
|
192
|
+
|
193
|
+
### Sharing code between derived and inner
|
194
|
+
Sometimes it could be useful to have code replicated in both derived and inner models.
|
195
|
+
This can be done by passing a block to `derives_from`.
|
196
|
+
```ruby
|
197
|
+
class DerivedModel < ApplicationRecord
|
198
|
+
include ActiveRecord::ModelInheritance::Model
|
199
|
+
|
200
|
+
derives_from BaseModel do
|
201
|
+
def foo
|
202
|
+
# ...
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
```
|
207
|
+
In the above example, `foo` you will be declared in both derived and inner models.
|
208
|
+
|
209
|
+
### Accessing the inner model
|
210
|
+
If for some reason you want to directly access the inner model, you can:
|
211
|
+
```ruby
|
212
|
+
DerivedModel::Inner # the inner model class
|
213
|
+
|
214
|
+
DerivedModel::Foo # in case you've set inner_class_name to 'Foo'
|
215
|
+
|
216
|
+
DerivedModel.first._model_inheritance_inner # instance of the inner model
|
217
|
+
```
|
218
|
+
|
219
|
+
### A few words on multiple inheritance
|
220
|
+
This gem doesn't strictly prohibit multiple inheritance, and in _in theory_ it should be possible to implement.
|
221
|
+
Currently there are no plans on this, but if you find a clean solution you can share your work with us! (see [Contributing](#contributing))
|
222
|
+
|
223
|
+
## Limitations
|
224
|
+
- A derived model is not a subclass of its base model
|
225
|
+
- Query methods called on base models will return only base models
|
226
|
+
- Query methods called on derived models will return only derived models
|
227
|
+
|
228
|
+
## Future developments
|
229
|
+
- Improved and more comprehensive documentation
|
230
|
+
- Some ways around current limitations
|
231
|
+
- Testing with a dummy Rails application
|
232
|
+
|
233
|
+
## Version numbers
|
234
|
+
|
235
|
+
Model Inheritance loosely follows [Semantic Versioning](https://semver.org/), with a hard guarantee that breaking changes to the public API will always coincide with an increase to the `MAJOR` number.
|
236
|
+
|
237
|
+
Version numbers are in three parts: `MAJOR.MINOR.PATCH`.
|
238
|
+
|
239
|
+
- Breaking changes to the public API increment the `MAJOR`. There may also be changes that would otherwise increase the `MINOR` or the `PATCH`.
|
240
|
+
- Additions, deprecations, and "big" non breaking changes to the public API increment the `MINOR`. There may also be changes that would otherwise increase the `PATCH`.
|
241
|
+
- Bug fixes and "small" non breaking changes to the public API increment the `PATCH`.
|
242
|
+
|
243
|
+
Notice that any feature deprecated by a minor release can be expected to be removed by the next major release.
|
244
|
+
|
245
|
+
## Changelog
|
246
|
+
|
247
|
+
Full list of changes in [CHANGELOG.md](CHANGELOG.md)
|
248
|
+
|
249
|
+
## Contributing
|
250
|
+
|
251
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/moku-io/activerecord-model_inheritance.
|
252
|
+
|
253
|
+
## License
|
254
|
+
|
255
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_record'
|
3
|
+
require_relative 'error'
|
4
|
+
require_relative 'view_definition'
|
5
|
+
|
6
|
+
module ActiveRecord
|
7
|
+
module ModelInheritance
|
8
|
+
class InheritanceError < Error; end
|
9
|
+
|
10
|
+
module Model
|
11
|
+
extend ActiveSupport::Concern
|
12
|
+
|
13
|
+
included do
|
14
|
+
class_attribute :model_inheritance_base_model
|
15
|
+
class_attribute :model_inheritance_inner_model
|
16
|
+
class_attribute :model_inheritance_base_name
|
17
|
+
class_attribute :model_inheritance_inner_name
|
18
|
+
class_attribute :model_inheritance_view_definition
|
19
|
+
class_attribute :model_inheritance_attributes_mapping
|
20
|
+
|
21
|
+
self.primary_key = :id
|
22
|
+
self.record_timestamps = false
|
23
|
+
end
|
24
|
+
|
25
|
+
class_methods do
|
26
|
+
def derives_from(base_model,
|
27
|
+
base_reference_name: ModelInheritance.base_reference_name,
|
28
|
+
inner_reference_name: ModelInheritance.inner_reference_name,
|
29
|
+
inherit_enums: ModelInheritance.inherit_enums,
|
30
|
+
inner_base_class: ModelInheritance.inner_base_class,
|
31
|
+
inner_class_name: ModelInheritance.inner_class_name,
|
32
|
+
delegate_missing_to_base: ModelInheritance.delegate_missing_to_base,
|
33
|
+
&block)
|
34
|
+
self.model_inheritance_base_name = base_reference_name
|
35
|
+
self.model_inheritance_inner_name = inner_reference_name
|
36
|
+
self.model_inheritance_base_model = base_model
|
37
|
+
|
38
|
+
base_ref_foreign_key = "#{model_inheritance_base_model.model_name.singular}_id"
|
39
|
+
base_ref = proc do
|
40
|
+
belongs_to base_reference_name,
|
41
|
+
class_name: "::#{base_model.name}",
|
42
|
+
foreign_key: base_ref_foreign_key
|
43
|
+
end
|
44
|
+
|
45
|
+
inner_model = Class.new inner_base_class do
|
46
|
+
instance_exec(&block) if block.present?
|
47
|
+
instance_exec(&base_ref)
|
48
|
+
end
|
49
|
+
instance_exec(&block) if block.present?
|
50
|
+
instance_exec(&base_ref)
|
51
|
+
|
52
|
+
const_set inner_class_name, inner_model
|
53
|
+
belongs_to inner_reference_name, class_name: inner_class_name, foreign_key: :id
|
54
|
+
|
55
|
+
# the secret ingredient
|
56
|
+
accepts_nested_attributes_for base_reference_name
|
57
|
+
accepts_nested_attributes_for inner_reference_name
|
58
|
+
|
59
|
+
self.model_inheritance_inner_model = inner_model
|
60
|
+
self.model_inheritance_view_definition = ViewDefinition.from_model self
|
61
|
+
self.model_inheritance_attributes_mapping = model_inheritance_view_definition.attributes_mapping
|
62
|
+
|
63
|
+
# prevents attributes from being touched when updating
|
64
|
+
# not strictly necessary, but better safe than sorry
|
65
|
+
attr_readonly attribute_names.map(&:to_sym)
|
66
|
+
|
67
|
+
if inherit_enums
|
68
|
+
base_model.defined_enums.each do |attribute, enum_values|
|
69
|
+
attribute = attribute.to_sym
|
70
|
+
enum attribute, enum_values if model_inheritance_attributes_mapping[:base].include? attribute
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
delegate_missing_to base_reference_name if delegate_missing_to_base
|
75
|
+
end
|
76
|
+
|
77
|
+
def partition_attributes attributes
|
78
|
+
inner_attributes = attributes.select { |key| model_inheritance_attributes_mapping[:inner].include? key.to_sym }
|
79
|
+
base_attributes = attributes.select { |key| model_inheritance_attributes_mapping[:base].include? key.to_sym }
|
80
|
+
|
81
|
+
[inner_attributes, base_attributes]
|
82
|
+
end
|
83
|
+
|
84
|
+
def create(...)
|
85
|
+
super.reload
|
86
|
+
end
|
87
|
+
|
88
|
+
def create!(...)
|
89
|
+
super.reload
|
90
|
+
end
|
91
|
+
|
92
|
+
# overriding the following methods to prevent ConnectionAdapter from touching the underlying view
|
93
|
+
|
94
|
+
def _insert_record(...)
|
95
|
+
nil
|
96
|
+
end
|
97
|
+
|
98
|
+
def _update_record(...)
|
99
|
+
nil
|
100
|
+
end
|
101
|
+
|
102
|
+
def _delete_record(...)
|
103
|
+
nil
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def save(**options, &)
|
108
|
+
prepare_save
|
109
|
+
super && _model_inheritance_base.save
|
110
|
+
end
|
111
|
+
|
112
|
+
def save!(**options, &)
|
113
|
+
prepare_save
|
114
|
+
super
|
115
|
+
_model_inheritance_base.save!
|
116
|
+
end
|
117
|
+
|
118
|
+
def destroy
|
119
|
+
_model_inheritance_inner.destroy
|
120
|
+
super
|
121
|
+
end
|
122
|
+
|
123
|
+
def delete
|
124
|
+
_model_inheritance_inner.delete
|
125
|
+
super
|
126
|
+
end
|
127
|
+
|
128
|
+
def _model_inheritance_base
|
129
|
+
public_send model_inheritance_base_name
|
130
|
+
end
|
131
|
+
|
132
|
+
def _model_inheritance_inner
|
133
|
+
public_send model_inheritance_inner_name
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
def prepare_save
|
139
|
+
inner_attributes, base_attributes = self.class.partition_attributes attributes_for_database
|
140
|
+
|
141
|
+
if new_record?
|
142
|
+
unless _model_inheritance_base.present?
|
143
|
+
raise InheritanceError, "#{model_inheritance_base_name} must be present"
|
144
|
+
end
|
145
|
+
|
146
|
+
# pass updated base attributes to base model
|
147
|
+
# this way it gets automatically updated
|
148
|
+
base_attributes.compact!
|
149
|
+
_model_inheritance_base.assign_attributes base_attributes if base_attributes.present?
|
150
|
+
|
151
|
+
attributes = {
|
152
|
+
model_inheritance_base_name => _model_inheritance_base,
|
153
|
+
"#{model_inheritance_inner_name}_attributes".to_sym => inner_attributes
|
154
|
+
}
|
155
|
+
else
|
156
|
+
inner_attributes[:id] = id
|
157
|
+
base_attributes[:id] = _model_inheritance_base.id
|
158
|
+
|
159
|
+
attributes = {
|
160
|
+
"#{model_inheritance_base_name}_attributes".to_sym => base_attributes,
|
161
|
+
"#{model_inheritance_inner_name}_attributes".to_sym => inner_attributes
|
162
|
+
}
|
163
|
+
end
|
164
|
+
|
165
|
+
assign_attributes attributes
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'arel'
|
2
|
+
require_relative 'error'
|
3
|
+
|
4
|
+
module ActiveRecord
|
5
|
+
module ModelInheritance
|
6
|
+
class DefinitionError < Error; end
|
7
|
+
|
8
|
+
class ViewDefinition
|
9
|
+
attr_reader :definition
|
10
|
+
attr_reader :model_class
|
11
|
+
|
12
|
+
def initialize model_class, definition
|
13
|
+
@model_class = model_class
|
14
|
+
@definition = definition
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_sql
|
18
|
+
@definition.to_sql
|
19
|
+
end
|
20
|
+
|
21
|
+
def attributes_mapping
|
22
|
+
definition
|
23
|
+
.projections
|
24
|
+
.each_with_object({base: [], inner: []}) do |projection, attributes_mapping|
|
25
|
+
if projection.is_a? Arel::Nodes::As
|
26
|
+
relation = projection.left.relation
|
27
|
+
name = projection.right
|
28
|
+
else
|
29
|
+
relation = projection.relation
|
30
|
+
name = projection.name
|
31
|
+
end
|
32
|
+
|
33
|
+
case relation
|
34
|
+
when model_class.model_inheritance_inner_model.arel_table
|
35
|
+
relation_type = :inner
|
36
|
+
relation_model = model_class.model_inheritance_inner_model
|
37
|
+
when model_class.model_inheritance_base_model.arel_table
|
38
|
+
relation_type = :base
|
39
|
+
relation_model = model_class.model_inheritance_base_model
|
40
|
+
else
|
41
|
+
raise DefinitionError, "Invalid \"#{relation}\" relation"
|
42
|
+
end
|
43
|
+
|
44
|
+
attributes = if name == Arel.star
|
45
|
+
relation_model.attribute_names.map(&:to_sym)
|
46
|
+
else
|
47
|
+
[name.to_sym]
|
48
|
+
end
|
49
|
+
|
50
|
+
attributes_mapping[relation_type] += attributes
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.from_model model_class
|
55
|
+
unless model_class.include? Model
|
56
|
+
raise ArgumentError, "#{model_class.name} doesn't include ActiveRecord::ModelInheritance::Model"
|
57
|
+
end
|
58
|
+
|
59
|
+
ViewDefinition.from_name model_class.model_name.plural
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.from_name name
|
63
|
+
definition_filename = Pathname(ModelInheritance.config.definitions_path).join "#{name}.rb"
|
64
|
+
raise ArgumentError, "Definition for \"#{name}\" doesn't exist" unless definition_filename.file?
|
65
|
+
|
66
|
+
eval File.read definition_filename
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.define_derived_view model_class, &block
|
70
|
+
inner_model = model_class.model_inheritance_inner_model
|
71
|
+
base_model = model_class.model_inheritance_base_model
|
72
|
+
|
73
|
+
inner_table = inner_model.arel_table
|
74
|
+
base_table = base_model.arel_table
|
75
|
+
|
76
|
+
definition = if block_given?
|
77
|
+
block.call(inner_table, base_table).tap do |d|
|
78
|
+
unless d.is_a? Arel::SelectManager
|
79
|
+
raise DefinitionError, 'Defined view must evaluate to Arel::SelectManager'
|
80
|
+
end
|
81
|
+
end
|
82
|
+
else
|
83
|
+
selected_base_columns = if inner_model.primary_key == base_model.primary_key
|
84
|
+
# this is a common naming conflict problem
|
85
|
+
# makes sense to try and solve automatically
|
86
|
+
|
87
|
+
# just delete the base primary key from columns that will be selected
|
88
|
+
base_model
|
89
|
+
.column_names
|
90
|
+
.dup
|
91
|
+
.delete_if { |column_name| column_name == base_model.primary_key }
|
92
|
+
else
|
93
|
+
base_model.column_names
|
94
|
+
end.map { |column_name| base_table[column_name.to_sym] }
|
95
|
+
|
96
|
+
base_reference = model_class
|
97
|
+
.reflect_on_association(model_class.model_inheritance_base_name)
|
98
|
+
.foreign_key
|
99
|
+
.to_sym
|
100
|
+
|
101
|
+
inner_table
|
102
|
+
.project(inner_table[Arel.star])
|
103
|
+
.project(*selected_base_columns)
|
104
|
+
.join(base_table)
|
105
|
+
.on(inner_table[base_reference].eq base_table[base_model.primary_key])
|
106
|
+
end
|
107
|
+
|
108
|
+
ViewDefinition.new model_class, definition
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'scenic'
|
2
|
+
require 'active_record'
|
3
|
+
require 'active_support'
|
4
|
+
require_relative 'model_inheritance/version'
|
5
|
+
|
6
|
+
module ActiveRecord
|
7
|
+
module ModelInheritance
|
8
|
+
include ActiveSupport::Configurable
|
9
|
+
|
10
|
+
config.define_singleton_method :define_lazy_property do |key, &block|
|
11
|
+
self[key] = nil
|
12
|
+
|
13
|
+
define_singleton_method key do
|
14
|
+
self[key] || (self[key] = block.call)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
config.define_singleton_method :define_lazy_path do |name, *dirs|
|
19
|
+
define_lazy_property name do
|
20
|
+
if defined?(Rails.root) && Rails.root
|
21
|
+
Rails.root.join(*dirs)
|
22
|
+
else
|
23
|
+
Pathname(dirs.join '/')
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
define_singleton_method "#{name}=".to_sym do |path|
|
28
|
+
self[name] = Pathname(path)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
config.base_reference_name = :model_inheritance_base
|
33
|
+
config.inner_reference_name = :model_inheritance_inner
|
34
|
+
config.inner_class_name = 'Inner'
|
35
|
+
config.inherit_enums = true
|
36
|
+
config.delegate_missing_to_base = true
|
37
|
+
|
38
|
+
config.define_lazy_path :views_path, 'db', 'views'
|
39
|
+
config.define_lazy_path :models_path, 'app', 'models'
|
40
|
+
config.define_lazy_path :migrations_path, 'db', 'migrate'
|
41
|
+
config.define_lazy_path :definitions_path, 'db', 'views', 'model_inheritance'
|
42
|
+
|
43
|
+
config.define_lazy_property :inner_base_class do
|
44
|
+
if defined? ApplicationRecord
|
45
|
+
ApplicationRecord
|
46
|
+
else
|
47
|
+
ActiveRecord::Base
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
singleton_class.delegate(*config.keys, to: :config)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
require_relative 'model_inheritance/error'
|
56
|
+
require_relative 'model_inheritance/model'
|
57
|
+
require_relative 'model_inheritance/view_definition'
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative '../active_record/model_inheritance'
|