closure_tree 4.6.3 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: afbd40867cadc58befec5d4aeda0c0f2684906b7
4
- data.tar.gz: 27f8b0415ba7f10e84f50918f8fb4a3304d5d3fa
3
+ metadata.gz: 92930bde62d7c1d8460fcefb8964da22b6edca2b
4
+ data.tar.gz: fe1eb67d373bda0ce3e4f7a404e521d7dce52bf7
5
5
  SHA512:
6
- metadata.gz: e1b47e639ab23e6eec46d1ede597da1bfc2c3a1f2a7b82d9024272695f985ebd8704b9230ab44b5507ebb1226bc098ef5386e7e267f15ca50e9d31f5d7605bab
7
- data.tar.gz: ab3224ef55cee5cac66106d80aadf8b144abf132ac1a56ce7fb4b45fc37f38ff72060c336a9ee3ca696785529c2ce872d9b4fcfbb63ded55fbd4171cc111d491
6
+ metadata.gz: e07b36bf6badfeb5d9035eddd650e821159a8963b71df737f8e42947401aecf7456a218fe34f787cad6f8ddbd341e11072fb4a23f1c96c8d45e0a8ba42290fae
7
+ data.tar.gz: f767928d73fefcc76338fe0edb444f6b282b7249dfa808e7f406f4b04593f63696ecabd23e3b421b39f2ea09f9883b9f4000a91c23ab3e24bcadfd8955a0f813
@@ -19,6 +19,9 @@ env:
19
19
  - DB=mysql
20
20
  - DB=postgresql
21
21
 
22
+ addons:
23
+ postgresql: "9.3"
24
+
22
25
  script: WITH_ADVISORY_LOCK_PREFIX=$TRAVIS_JOB_ID bundle exec rake --trace all_spec_flavors
23
26
 
24
27
  matrix:
data/Appraisals CHANGED
@@ -1,17 +1,17 @@
1
- appraise "activerecord-3.2" do
2
- gem 'activerecord', '~> 3.2'
1
+ appraise 'activerecord-3.2' do
2
+ gem 'activerecord', '~> 3.2.0'
3
3
  gem 'strong_parameters'
4
4
  end
5
5
 
6
- appraise "activerecord-4.0" do
7
- gem "activerecord", "~> 4.0"
6
+ appraise 'activerecord-4.0' do
7
+ gem 'activerecord', '~> 4.0.0'
8
8
  end
9
9
 
10
- appraise "activerecord-4.1" do
11
- gem "activerecord", "~> 4.1"
10
+ appraise 'activerecord-4.1' do
11
+ gem 'activerecord', '~> 4.1.0'
12
12
  end
13
13
 
14
- appraise "activerecord-edge" do
15
- gem "activerecord", github: "rails/rails"
14
+ appraise 'activerecord-edge' do
15
+ gem 'activerecord', github: 'rails/rails'
16
16
  gem 'arel', github: 'rails/arel'
17
17
  end
@@ -1,33 +1,48 @@
1
1
  # Changelog
2
2
 
3
+ ### 5.0.0
4
+
5
+ #### Breaking API changes
6
+
7
+ * `find_by_path` and `find_or_create_by_path` now takes either an array of strings
8
+ or an array of attribute hashes, which can include the inheritance column for STI support.
9
+ * Removed the extraneous `base_class` `acts_as_tree` option—it needlessly duplicated ActiveRecord's method.
10
+ * Removed the unused `name` `acts_as_tree` option.
11
+
12
+ #### Improvements and bugfixes
13
+
14
+ * Cleaned up the inheritance support methods to delegate correctly to ActiveRecord
15
+ * Fixed a query generation error when ancestor paths exceeded 50 items.
16
+ * Documented the `.touch` option
17
+
3
18
  ### 4.6.3
4
19
 
5
- * More goodness from from [Abdelkader Boudih](https://github.com/seuros), including rspec 3 support.
20
+ * More goodness from [Abdelkader Boudih](https://github.com/seuros), including rspec 3 support.
6
21
 
7
22
  ### 4.6.2
8
23
 
9
- * Pulled in [106](https://github.com/mceachen/closure_tree/pull/106) which fixed a bug introduced
10
- in 4.6.0 which broke if the numeric ordering column wasn't named 'sort_order'. Tests have been
11
- added. Thanks for the fix, [Fission Xuiptz](https://github.com/fissionxuiptz)!
24
+ * Pulled in [106](https://github.com/mceachen/closure_tree/pull/106) which fixed a bug introduced
25
+ in 4.6.0 which broke if the numeric ordering column wasn't named 'sort_order'. Tests have been
26
+ added. Thanks for the fix, [Fission Xuiptz](https://github.com/fissionxuiptz)!
12
27
 
13
28
  ### 4.6.1
14
29
 
15
- * Address [issue 60](https://github.com/mceachen/closure_tree/issues/60) (use `.empty?` rather
16
- than `.nil?`—thanks for the suggestion, [Leonel Galán](https://github.com/leonelgalan),
17
- [Doug Mayer](https://github.com/doxavore) and [Samnang Chhun](https://github.com/samnang)!
30
+ * Address [issue 60](https://github.com/mceachen/closure_tree/issues/60) (use `.empty?` rather
31
+ than `.nil?`—thanks for the suggestion, [Leonel Galán](https://github.com/leonelgalan),
32
+ [Doug Mayer](https://github.com/doxavore) and [Samnang Chhun](https://github.com/samnang)!
18
33
 
19
34
  ### 4.6.0
20
35
 
21
- * Deterministically ordered trees are guaranteed to have a sort_order now.
36
+ * Deterministically ordered trees are guaranteed to have a sort_order now.
37
+
38
+ **This may be a breaking change if you're expecting sort_order to be nullable.**
22
39
 
23
- **This may be a breaking change if you're expecting sort_order to be nullable.**
40
+ Many thanks to [David Schmidt](https://github.com/inetdavid) for raising and
41
+ working on the issue!
24
42
 
25
- Many thanks to [David Schmidt](https://github.com/inetdavid) for raising and
26
- working on the issue!
43
+ * Added ```append_child``` and ```prepend_child```
27
44
 
28
- * Added ```append_child``` and ```prepend_child```
29
-
30
- * All raw SQL is now ```strip_heredoc```'ed
45
+ * All raw SQL is now ```strip_heredoc```'ed
31
46
 
32
47
  ### 4.5.0
33
48
 
data/README.md CHANGED
@@ -10,7 +10,7 @@ and tracking user referrals.
10
10
  [![Code Climate](https://codeclimate.com/github/mceachen/closure_tree.png)](https://codeclimate.com/github/mceachen/closure_tree)
11
11
  [![Dependency Status](https://gemnasium.com/mceachen/closure_tree.png)](https://gemnasium.com/mceachen/closure_tree)
12
12
 
13
- Substantially more efficient than
13
+ Dramatically more performant than
14
14
  [ancestry](https://github.com/stefankroes/ancestry) and
15
15
  [acts_as_tree](https://github.com/amerine/acts_as_tree), and even more
16
16
  awesome than [awesome_nested_set](https://github.com/collectiveidea/awesome_nested_set/),
@@ -25,16 +25,16 @@ closure_tree has some great features:
25
25
  * __Best-in-class mutation performance__:
26
26
  * 2 SQL INSERTs on node creation
27
27
  * 3 SQL INSERT/UPDATEs on node reparenting
28
+ * __Support for [concurrency](#concurrency)__ (using [with_advisory_lock](https://github.com/mceachen/with_advisory_lock))
28
29
  * __Support for Rails 3.2, 4.0, and 4.1__
29
- * __Support for Ruby 1.9 and 2.1 (jRuby and Rubinius are still in development)__
30
+ * __Support for Ruby 1.9, 2.1, and jRuby 1.6.13
30
31
  * Support for reparenting children (and all their descendants)
31
- * Support for [concurrency](#concurrency) (using [with_advisory_lock](https://github.com/mceachen/with_advisory_lock))
32
- * Support for polymorphism [STI](#sti) within the hierarchy
33
- * ```find_or_create_by_path``` for [building out hierarchies quickly and conveniently](#find_or_create_by_path)
34
- * Support for [deterministic ordering](#deterministic-ordering) of children
32
+ * Support for [single-table inheritance (STI)](#sti) within the hierarchy
33
+ * ```find_or_create_by_path``` for [building out heterogeneous hierarchies quickly and conveniently](#find_or_create_by_path)
34
+ * Support for [deterministic ordering](#deterministic-ordering)
35
35
  * Support for [preordered](http://en.wikipedia.org/wiki/Tree_traversal#Pre-order) traversal of descendants
36
36
  * Support for rendering trees in [DOT format](http://en.wikipedia.org/wiki/DOT_(graph_description_language)), using [Graphviz](http://www.graphviz.org/)
37
- * Excellent [test coverage](#testing) in a variety of environments
37
+ * Excellent [test coverage](#testing) in a comprehensive variety of environments
38
38
 
39
39
  See [Bill Karwin](http://karwin.blogspot.com/)'s excellent
40
40
  [Models for hierarchical data presentation](http://www.slideshare.net/billkarwin/models-for-hierarchical-data)
@@ -153,30 +153,40 @@ child1.ancestry_path
153
153
 
154
154
  ### find_or_create_by_path
155
155
 
156
- We can do all the node creation and add_child calls with one method call:
156
+ You can ```find``` as well as ```find_or_create``` by "ancestry paths".
157
+
158
+ If you provide an array of strings to these methods, they reference the `name` column in your
159
+ model, which can be overridden with the `:name_column` option provided to ```acts_as_tree```.
157
160
 
158
161
  ```ruby
159
162
  child = Tag.find_or_create_by_path(["grandparent", "parent", "child"])
160
163
  ```
161
164
 
162
- You can ```find``` as well as ```find_or_create``` by "ancestry paths".
163
- Ancestry paths may be built using any column in your model. The default
164
- column is ```name```, which can be changed with the :name_column option
165
- provided to ```acts_as_tree```.
165
+ As of v5.0.0, `find_or_create_by_path` can also take an array of attribute hashes:
166
166
 
167
- Note that any other AR fields can be set with the second, optional ```attributes``` argument,
168
- and as of version 4.2.0, these attributes are added to the where clause as selection criteria.
167
+ ```ruby
168
+ child = Tag.find_or_create_by_path([
169
+ {name: "Grandparent", title: "Sr."},
170
+ {name: "Parent", title: "Mrs."},
171
+ {name: "Child", title: "Jr."}
172
+ ])
173
+ ```
174
+
175
+ If you're using STI, The attribute hashes can contain the `sti_name` and things work as expected:
169
176
 
170
177
  ```ruby
171
- child = Tag.find_or_create_by_path(%w{home chuck Photos"}, {:tag_type => "File"})
178
+ child = Label.find_or_create_by_path([
179
+ {type: 'DateLabel', name: '2014'},
180
+ {type: 'DateLabel', name: 'August'},
181
+ {type: 'DateLabel', name: '5'},
182
+ {type: 'EventLabel', name: 'Visit the Getty Center'}
183
+ ])
172
184
  ```
173
- This will pass the attribute hash of ```{:name => "home", :tag_type => "File"}``` to
174
- ```Tag.find_or_create_by_name``` if the root directory doesn't exist (and
175
- ```{:name => "chuck", :tag_type => "File"}``` if the second-level tag doesn't exist, and so on).
176
185
 
177
186
  ### Moving nodes around the tree
178
187
 
179
- Nodes can be moved around to other parents, and closure_tree moves the node's descendancy to the new parent for you:
188
+ Nodes can be moved around to other parents, and closure_tree moves the node's descendancy to the
189
+ new parent for you:
180
190
 
181
191
  ```ruby
182
192
  d = Tag.find_or_create_by_path %w(a b c d)
@@ -252,6 +262,7 @@ When you include ```acts_as_tree``` in your model, you can provide a hash to ove
252
262
  * ```:destroy``` will destroy all descendant nodes (which runs the destroy hooks on each child node)
253
263
  * ```:name_column``` used by #```find_or_create_by_path```, #```find_by_path```, and ```ancestry_path``` instance methods. This is primarily useful if the model only has one required field (like a "tag").
254
264
  * ```:order``` used to set up [deterministic ordering](#deterministic-ordering)
265
+ * ```:touch``` delegates to the `belongs_to` annotation for the parent, so `touch`ing cascades to all children (the performance of this for deep trees isn't currently optimal).
255
266
 
256
267
  ## Accessing Data
257
268
 
@@ -529,7 +540,6 @@ end
529
540
 
530
541
  ```
531
542
 
532
-
533
543
  ## Testing
534
544
 
535
545
  Closure tree is [tested under every valid combination](http://travis-ci.org/#!/mceachen/closure_tree) of
@@ -8,27 +8,26 @@ Gem::Specification.new do |gem|
8
8
  gem.email = ['matthew-github@mceachen.org']
9
9
  gem.homepage = 'http://mceachen.github.io/closure_tree/'
10
10
 
11
- gem.summary = %q(Easily and efficiently make your ActiveRecord model support hierarchies)
11
+ gem.summary = %q(Easily and efficiently make your ActiveRecord model support hierarchies)
12
12
  gem.description = gem.summary
13
- gem.license = 'MIT'
13
+ gem.license = 'MIT'
14
14
 
15
- gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
16
- gem.test_files = gem.files.grep(%r{^spec/})
17
- gem.required_ruby_version = '>= 1.9.3'
15
+ gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
16
+ gem.test_files = gem.files.grep(%r{^spec/})
17
+ gem.required_ruby_version = '>= 1.9.3'
18
18
 
19
19
  gem.add_runtime_dependency 'activerecord', '>= 3.2.0'
20
- gem.add_runtime_dependency 'with_advisory_lock', '>= 0.0.9' # <- to prevent duplicate roots
20
+ gem.add_runtime_dependency 'with_advisory_lock', '>= 3.0.0'
21
21
 
22
- gem.add_development_dependency 'rake'
23
22
  gem.add_development_dependency 'yard'
24
23
  gem.add_development_dependency 'rspec', '>= 3.0'
25
24
  gem.add_development_dependency 'rspec-instafail'
25
+ # TODO: delete rspec-rails.
26
26
  gem.add_development_dependency 'rspec-rails' # FIXME: for rspec-rails and rspec fixture support
27
27
  gem.add_development_dependency 'uuidtools'
28
28
  gem.add_development_dependency 'database_cleaner'
29
29
  gem.add_development_dependency 'appraisal'
30
30
  gem.add_development_dependency 'timecop'
31
-
31
+ gem.add_development_dependency 'parallel'
32
32
  # gem.add_development_dependency 'ruby-prof' # <- don't need this normally.
33
-
34
33
  end
@@ -4,16 +4,14 @@ module ClosureTree
4
4
  module ActsAsTree
5
5
  def acts_as_tree(options = {})
6
6
  options.assert_valid_keys(
7
- :base_class,
7
+ :parent_column_name,
8
8
  :dependent,
9
9
  :hierarchy_class_name,
10
10
  :hierarchy_table_name,
11
- :name,
12
11
  :name_column,
13
12
  :order,
14
- :parent_column_name,
15
- :with_advisory_lock,
16
- :touch
13
+ :touch,
14
+ :with_advisory_lock
17
15
  )
18
16
 
19
17
  class_attribute :_ct
@@ -2,34 +2,34 @@ module ClosureTree
2
2
  module Finders
3
3
  extend ActiveSupport::Concern
4
4
 
5
- # Find a child node whose +ancestry_path+ minus self.ancestry_path is +path+.
5
+ # Find a descendant node whose +ancestry_path+ will be ```self.ancestry_path + path```
6
6
  def find_by_path(path, attributes = {})
7
7
  return self if path.empty?
8
8
  self.class.find_by_path(path, attributes, id)
9
9
  end
10
10
 
11
- # Find a child node whose +ancestry_path+ minus self.ancestry_path is +path+
12
- def find_or_create_by_path(path, attributes = {}, find_before_lock = true)
13
- attributes[:type] ||= self.type if _ct.subclass? && _ct.has_type?
14
- # only bother trying to find_by_path on the first call:
15
- (find_before_lock && find_by_path(path, attributes)) || begin
16
- subpath = path.is_a?(Enumerable) ? path.dup : [path]
17
- return self if subpath.empty?
18
- child_name = subpath.shift
19
- attrs = attributes.merge(_ct.name_sym => child_name)
20
- _ct.with_advisory_lock do
21
- # shenanigans because children.create is bound to the superclass
22
- # (in the case of polymorphism):
23
- child = self.children.where(attrs).first || begin
24
- self.class.new(attrs).tap do |ea|
25
- # We know that there isn't a cycle, because we just created it, and
26
- # cycle detection is expensive when the node is deep.
27
- ea._ct_skip_cycle_detection!
28
- self.children << ea
29
- end
11
+ # Find or create a descendant node whose +ancestry_path+ will be ```self.ancestry_path + path```
12
+ def find_or_create_by_path(path, attributes = {})
13
+ subpath = _ct.build_ancestry_attr_path(path, attributes)
14
+ return self if subpath.empty?
15
+
16
+ found = find_by_path(subpath, attributes)
17
+ return found if found
18
+
19
+ attrs = subpath.shift
20
+ _ct.with_advisory_lock do
21
+ # shenanigans because children.create is bound to the superclass
22
+ # (in the case of polymorphism):
23
+ child = self.children.where(attrs).first || begin
24
+ # Support STI creation by using base_class:
25
+ _ct.create(self.class, attrs).tap do |ea|
26
+ # We know that there isn't a cycle, because we just created it, and
27
+ # cycle detection is expensive when the node is deep.
28
+ ea._ct_skip_cycle_detection!
29
+ self.children << ea
30
30
  end
31
- child.find_or_create_by_path(subpath, attributes, false)
32
31
  end
32
+ child.find_or_create_by_path(subpath, attributes)
33
33
  end
34
34
  end
35
35
 
@@ -54,7 +54,7 @@ module ClosureTree
54
54
 
55
55
  # Fix deprecation warning:
56
56
  def _ct_all
57
- (ActiveRecord::VERSION::MAJOR >= 4 ) ? all : scoped
57
+ (ActiveRecord::VERSION::MAJOR >= 4) ? all : scoped
58
58
  end
59
59
 
60
60
  def without(instance)
@@ -115,49 +115,37 @@ module ClosureTree
115
115
  _ct.scope_with_order(s)
116
116
  end
117
117
 
118
- def ct_scoped_attributes(scope, attributes, target_table = table_name)
119
- attributes.inject(scope) do |scope, pair|
120
- scope.where("#{target_table}.#{pair.first}" => pair.last)
121
- end
122
- end
123
-
124
118
  # Find the node whose +ancestry_path+ is +path+
125
119
  def find_by_path(path, attributes = {}, parent_id = nil)
126
- path = path.is_a?(Enumerable) ? path.dup : [path]
127
- scope = where(_ct.name_sym => path.pop).readonly(false)
128
- scope = ct_scoped_attributes(scope, attributes)
120
+ path = _ct.build_ancestry_attr_path(path, attributes)
121
+ if path.size > _ct.max_join_tables
122
+ return _ct.find_by_large_path(path, attributes, parent_id)
123
+ end
124
+ scope = where(path.pop)
129
125
  last_joined_table = _ct.table_name
130
- # MySQL doesn't support more than 61 joined tables (!!):
131
- path.first(50).reverse.each_with_index do |ea, idx|
126
+ path.reverse.each_with_index do |ea, idx|
132
127
  next_joined_table = "p#{idx}"
133
128
  scope = scope.joins(<<-SQL.strip_heredoc)
134
129
  INNER JOIN #{_ct.quoted_table_name} AS #{next_joined_table}
135
130
  ON #{next_joined_table}.#{_ct.quoted_id_column_name} =
136
- #{connection.quote_table_name(last_joined_table)}.#{_ct.quoted_parent_column_name}
131
+ #{connection.quote_table_name(last_joined_table)}.#{_ct.quoted_parent_column_name}
137
132
  SQL
138
- scope = scope.where("#{next_joined_table}.#{_ct.name_column}" => ea)
139
- scope = ct_scoped_attributes(scope, attributes, next_joined_table)
133
+ scope = _ct.scoped_attributes(scope, ea, next_joined_table)
140
134
  last_joined_table = next_joined_table
141
135
  end
142
- result = scope.where("#{last_joined_table}.#{_ct.parent_column_name}" => parent_id).first
143
- if path.size > 50 && result
144
- find_by_path(path[50..-1], attributes, result.primary_key)
145
- else
146
- result
147
- end
136
+ scope.where("#{last_joined_table}.#{_ct.parent_column_name}" => parent_id).readonly(false).first
148
137
  end
149
138
 
150
139
  # Find or create nodes such that the +ancestry_path+ is +path+
151
140
  def find_or_create_by_path(path, attributes = {})
152
- find_by_path(path, attributes) || begin
153
- subpath = path.dup
154
- root_name = subpath.shift
141
+ attr_path = _ct.build_ancestry_attr_path(path, attributes)
142
+ find_by_path(attr_path) || begin
143
+ root_attrs = attr_path.shift
155
144
  _ct.with_advisory_lock do
156
145
  # shenanigans because find_or_create can't infer that we want the same class as this:
157
146
  # Note that roots will already be constrained to this subclass (in the case of polymorphism):
158
- attrs = attributes.merge(_ct.name_sym => root_name)
159
- root = roots.where(attrs).first || roots.create!(attrs)
160
- root.find_or_create_by_path(subpath, attributes)
147
+ root = roots.where(root_attrs).first || _ct.create!(self, root_attrs)
148
+ root.find_or_create_by_path(attr_path)
161
149
  end
162
150
  end
163
151
  end
@@ -80,6 +80,7 @@ module ClosureTree
80
80
 
81
81
  alias_method :level, :depth
82
82
 
83
+ # enumerable of ancestors, immediate parent is first, root is last.
83
84
  def ancestors
84
85
  without_self(self_and_ancestors)
85
86
  end
@@ -2,6 +2,7 @@ require 'closure_tree/support_flags'
2
2
  require 'closure_tree/support_attributes'
3
3
  require 'closure_tree/numeric_order_support'
4
4
 
5
+ # This class and mixins are an effort to reduce the namespace pollution to models that act_as_tree.
5
6
  module ClosureTree
6
7
  class Support
7
8
  include ClosureTree::SupportFlags
@@ -13,7 +14,6 @@ module ClosureTree
13
14
  def initialize(model_class, options)
14
15
  @model_class = model_class
15
16
  @options = {
16
- :base_class => model_class,
17
17
  :parent_column_name => 'parent_id',
18
18
  :dependent => :nullify, # or :destroy or :delete_all -- see the README
19
19
  :name_column => 'name',
@@ -116,12 +116,65 @@ module ClosureTree
116
116
 
117
117
  def with_advisory_lock(&block)
118
118
  if options[:with_advisory_lock]
119
- model_class.with_advisory_lock("closure_tree") do
119
+ model_class.with_advisory_lock(advisory_lock_name) do
120
120
  transaction { yield }
121
121
  end
122
122
  else
123
123
  yield
124
124
  end
125
125
  end
126
+
127
+ def build_ancestry_attr_path(path, attributes)
128
+ path = path.is_a?(Array) ? path.dup : [path]
129
+ unless path.first.is_a?(Hash)
130
+ if subclass? && has_inheritance_column?
131
+ attributes = attributes.with_indifferent_access
132
+ attributes[inheritance_column] ||= self.sti_name
133
+ end
134
+ path = path.map { |ea| attributes.merge(name_column => ea) }
135
+ end
136
+ path
137
+ end
138
+
139
+ def scoped_attributes(scope, attributes, target_table = model_class.table_name)
140
+ table_prefixed_attributes = Hash[
141
+ attributes.map do |column_name, column_value|
142
+ ["#{target_table}.#{column_name}", column_value]
143
+ end
144
+ ]
145
+ scope.where(table_prefixed_attributes)
146
+ end
147
+
148
+ def max_join_tables
149
+ # MySQL doesn't support more than 61 joined tables (!!):
150
+ 50
151
+ end
152
+
153
+ def find_by_large_path(path, attributes = {}, parent_id = nil)
154
+ next_parent_id = parent_id
155
+ child = nil
156
+ path.in_groups(max_join_tables, false).each do |subpath|
157
+ child = model_class.find_by_path(subpath, attributes, next_parent_id)
158
+ return nil if child.nil?
159
+ next_parent_id = child._ct_id
160
+ end
161
+ child
162
+ end
163
+
164
+ def creator_class(model_class, sti_class)
165
+ if sti_class.present?
166
+ base_class.send(:find_sti_class, sti_class)
167
+ else
168
+ model_class
169
+ end
170
+ end
171
+
172
+ def create(model_class, attributes)
173
+ creator_class(model_class, attributes.with_indifferent_access[inheritance_column]).new(attributes)
174
+ end
175
+
176
+ def create!(model_class, attributes)
177
+ create(model_class, attributes).tap { |ea| ea.save! }
178
+ end
126
179
  end
127
180
  end