copyable 0.2.0 → 0.3.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 +4 -4
- data/README.md +4 -11
- data/copyable.gemspec +2 -2
- data/lib/copyable/declarations/associations.rb +7 -8
- data/lib/copyable/model_hooks.rb +20 -17
- data/lib/copyable/saver.rb +19 -11
- data/lib/copyable/syntax_checking/association_checker.rb +2 -1
- data/lib/copyable/version.rb +1 -1
- data/lib/tasks/copyable.rake +2 -1
- data/spec/helper/test_tables.rb +18 -18
- data/spec/model_hooks_spec.rb +10 -64
- metadata +9 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0ae89109e012130ce8cc01fa12b08ada0b3e1d09
|
4
|
+
data.tar.gz: a62a5cd9c1573d6e0da5f4a2d0d05b703f2af36b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 25b7247a6471abb6114632dfa7818cb9bfd1ccc51d1ca89a9a6a60fce3057fbb55432135f67445a2ef5bc9590db975616235275e20c16ca6f339bf1d91383fc1
|
7
|
+
data.tar.gz: 1341e8a9c2101a8980af25a9b3eade8c2cc321448465cd7398a3e4df802a96cfd6bc27cf69d0e49beed8ba27e6d91ab825c9be46a34d90e659cb8bc5cdfc38eb
|
data/README.md
CHANGED
@@ -121,16 +121,9 @@ The advice must be one of the following:
|
|
121
121
|
|
122
122
|
## Callbacks
|
123
123
|
|
124
|
-
It depends on the situation as to whether you would want a particular callback to be fired when a model is copied. Since the logic of callbacks is situational, Copyable makes the decision to
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
copyable do
|
129
|
-
disable_all_callbacks_and_observers_except_validate
|
130
|
-
...
|
131
|
-
end
|
132
|
-
|
133
|
-
|
124
|
+
It depends on the situation as to whether you would want a particular callback to be fired when a model is copied. Since the logic of callbacks is situational, Copyable makes the decision to bypass callbacks
|
125
|
+
when saving the copied models by using raw SQL insert statements during the copy. It will check
|
126
|
+
validations unless `skip_validations` is passed to `create_copy`.
|
134
127
|
|
135
128
|
## The After Copy Declaration
|
136
129
|
|
@@ -267,4 +260,4 @@ So the recommended approach is to use `after_copy` to tweak the columns of the c
|
|
267
260
|
|
268
261
|
## About
|
269
262
|
|
270
|
-
Copyable was developed at [District Management Group](https://dmgroupK12.com).
|
263
|
+
Copyable was developed at [District Management Group](https://dmgroupK12.com).
|
data/copyable.gemspec
CHANGED
@@ -18,11 +18,11 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
|
-
spec.add_dependency "activerecord", "~>4.
|
21
|
+
spec.add_dependency "activerecord", "~>4.2"
|
22
22
|
|
23
23
|
spec.add_development_dependency "database_cleaner", "~> 1.4.0"
|
24
24
|
spec.add_development_dependency "bundler", "~> 1.6"
|
25
25
|
spec.add_development_dependency "rake"
|
26
26
|
spec.add_development_dependency "rspec"
|
27
|
-
spec.add_development_dependency "sqlite3"
|
27
|
+
spec.add_development_dependency "sqlite3", "~> 1.3.6"
|
28
28
|
end
|
@@ -11,7 +11,7 @@ module Copyable
|
|
11
11
|
@skip_associations = skip_associations
|
12
12
|
@global_override = global_override
|
13
13
|
association_list.each do |assoc_name, advice|
|
14
|
-
association = original_model.class.reflections[assoc_name.
|
14
|
+
association = original_model.class.reflections[assoc_name.to_s]
|
15
15
|
check_advice(association, advice, original_model)
|
16
16
|
unless advice == :do_not_copy || skip_associations.include?(assoc_name.to_sym)
|
17
17
|
copy_association(association, original_model, new_model)
|
@@ -28,14 +28,14 @@ module Copyable
|
|
28
28
|
message << "has unrecognized advice '#{advice}'."
|
29
29
|
raise AssociationError.new(message)
|
30
30
|
end
|
31
|
-
if association.
|
31
|
+
if association.is_a?(ActiveRecord::Reflection::HasAndBelongsToManyReflection) && advice == :copy
|
32
32
|
message = "Error in copyable:associations of "
|
33
33
|
message << "#{original_model.class.name}: the association '#{association.name}' "
|
34
34
|
message << "only supports the :copy_only_habtm_join_records advice, not the :copy advice, "
|
35
35
|
message << "because it is a has_and_belongs_to_many association."
|
36
36
|
raise AssociationError.new(message)
|
37
37
|
end
|
38
|
-
if association.
|
38
|
+
if !association.is_a?(ActiveRecord::Reflection::HasAndBelongsToManyReflection) && advice == :copy_only_habtm_join_records
|
39
39
|
message = "Error in copyable:associations of "
|
40
40
|
message << "#{original_model.class.name}: the association '#{association.name}' "
|
41
41
|
message << "only supports the :copy advice, not the :copy_only_habtm_join_records advice, "
|
@@ -45,15 +45,14 @@ module Copyable
|
|
45
45
|
end
|
46
46
|
|
47
47
|
def copy_association(association, original_model, new_model)
|
48
|
-
|
49
|
-
when :has_many
|
48
|
+
if association.is_a?(ActiveRecord::Reflection::HasManyReflection)
|
50
49
|
copy_has_many(association, original_model, new_model)
|
51
|
-
|
50
|
+
elsif association.is_a?(ActiveRecord::Reflection::HasOneReflection)
|
52
51
|
copy_has_one(association, original_model, new_model)
|
53
|
-
|
52
|
+
elsif association.is_a?(ActiveRecord::Reflection::HasAndBelongsToManyReflection)
|
54
53
|
copy_habtm(association, original_model, new_model)
|
55
54
|
else
|
56
|
-
raise "Unsupported association #{association.
|
55
|
+
raise "Unsupported association #{association.class}" # should never happen, since we filter out belongs_to
|
57
56
|
end
|
58
57
|
end
|
59
58
|
|
data/lib/copyable/model_hooks.rb
CHANGED
@@ -16,30 +16,33 @@ module Copyable
|
|
16
16
|
|
17
17
|
def self.disable_all_callbacks(klass)
|
18
18
|
klass.class_eval do
|
19
|
-
# Don't do
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
19
|
+
return if self.method_defined? :old_save! # Don't do this more than once
|
20
|
+
|
21
|
+
@all_callbacks_disabled = true
|
22
|
+
class << self
|
23
|
+
attr_reader :all_callbacks_disabled
|
24
|
+
end
|
25
|
+
|
26
|
+
alias_method :old_save!, :save!
|
27
|
+
|
28
|
+
# Hold my beer while we bypass all the Rails callbacks
|
29
|
+
# in favor of some raw SQL
|
30
|
+
def save!
|
31
|
+
Copyable::Saver.save!(self)
|
32
32
|
end
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
36
36
|
def self.reenable_all_callbacks(klass)
|
37
37
|
klass.class_eval do
|
38
|
-
|
39
|
-
|
38
|
+
return unless self.method_defined? :old_save! # Don't do this more than once
|
39
|
+
@all_callbacks_disabled = false
|
40
|
+
class << self
|
41
|
+
attr_reader :all_callbacks_disabled
|
42
|
+
end
|
40
43
|
|
41
|
-
alias_method :
|
42
|
-
remove_method :
|
44
|
+
alias_method :save!, :old_save!
|
45
|
+
remove_method :old_save!
|
43
46
|
end
|
44
47
|
end
|
45
48
|
|
data/lib/copyable/saver.rb
CHANGED
@@ -1,19 +1,27 @@
|
|
1
1
|
module Copyable
|
2
2
|
class Saver
|
3
3
|
|
4
|
+
def self.direct_sql_insert!(new_model)
|
5
|
+
new_model.send(:_create_record) # bypass all callbacks and validations
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.direct_sql_update!(new_model)
|
9
|
+
new_model.send(:_update_record) # bypass all callbacks and validations
|
10
|
+
end
|
11
|
+
|
4
12
|
# this is the algorithm for saving the new record
|
5
|
-
def self.save!(new_model, skip_validations)
|
6
|
-
unless skip_validations
|
7
|
-
|
8
|
-
if !new_model.valid?(:create)
|
9
|
-
ModelHooks.disable!(new_model.class)
|
10
|
-
raise(ActiveRecord::RecordInvalid.new(new_model))
|
11
|
-
else
|
12
|
-
ModelHooks.disable!(new_model.class)
|
13
|
-
end
|
13
|
+
def self.save!(new_model, skip_validations=false)
|
14
|
+
unless skip_validations || new_model.valid?(:create)
|
15
|
+
raise(ActiveRecord::RecordInvalid.new(new_model))
|
14
16
|
end
|
15
|
-
new_model.save!
|
16
|
-
end
|
17
17
|
|
18
|
+
if new_model.class.all_callbacks_disabled && new_model.id.nil?
|
19
|
+
self.direct_sql_insert!(new_model)
|
20
|
+
elsif new_model.class.all_callbacks_disabled
|
21
|
+
self.direct_sql_update!(new_model)
|
22
|
+
else
|
23
|
+
new_model.save!
|
24
|
+
end
|
25
|
+
end
|
18
26
|
end
|
19
27
|
end
|
@@ -10,7 +10,8 @@ module Copyable
|
|
10
10
|
def expected_entries
|
11
11
|
all_associations = model_class.reflect_on_all_associations
|
12
12
|
required_associations = all_associations.select do |ass|
|
13
|
-
|
13
|
+
!ass.is_a?(ActiveRecord::Reflection::BelongsToReflection) &&
|
14
|
+
!ass.is_a?(ActiveRecord::Reflection::ThroughReflection)
|
14
15
|
end
|
15
16
|
required_associations.map(&:name).map(&:to_s)
|
16
17
|
end
|
data/lib/copyable/version.rb
CHANGED
data/lib/tasks/copyable.rake
CHANGED
@@ -39,7 +39,8 @@ task :copyable => :environment do
|
|
39
39
|
|
40
40
|
all_associations = model_class.reflect_on_all_associations
|
41
41
|
required_associations = all_associations.select do |ass|
|
42
|
-
|
42
|
+
!ass.is_a?(ActiveRecord::Reflection::BelongsToReflection) &&
|
43
|
+
!ass.is_a?(ActiveRecord::Reflection::ThroughReflection)
|
43
44
|
end
|
44
45
|
associations = required_associations.map(&:name).map(&:to_s)
|
45
46
|
max_length = associations.map(&:length).max
|
data/spec/helper/test_tables.rb
CHANGED
@@ -11,14 +11,14 @@ module Copyable
|
|
11
11
|
|
12
12
|
create_table :copyable_albums, force: true do |t|
|
13
13
|
t.string :name, null: false
|
14
|
-
t.timestamps
|
14
|
+
t.timestamps null: false
|
15
15
|
end
|
16
16
|
|
17
17
|
create_table :copyable_amenities, force: true do |t|
|
18
18
|
t.string :name
|
19
19
|
t.integer :copyable_vehicle_id
|
20
20
|
t.integer :copyable_owner_id
|
21
|
-
t.timestamps
|
21
|
+
t.timestamps null: false
|
22
22
|
end
|
23
23
|
|
24
24
|
create_table :copyable_addresses, force: true do |t|
|
@@ -27,30 +27,30 @@ module Copyable
|
|
27
27
|
t.string :city, null: false
|
28
28
|
t.string :state, null: false
|
29
29
|
t.integer :copyable_pet_profile_id, null: false
|
30
|
-
t.timestamps
|
30
|
+
t.timestamps null: false
|
31
31
|
end
|
32
32
|
|
33
33
|
create_table :copyable_cars, force: true do |t|
|
34
34
|
t.string :make, null: false
|
35
35
|
t.string :model, null: false
|
36
36
|
t.integer :year, null: false
|
37
|
-
t.timestamps
|
37
|
+
t.timestamps null: false
|
38
38
|
end
|
39
39
|
|
40
40
|
create_table :copyable_coins, force: true do |t|
|
41
41
|
t.string :kind
|
42
42
|
t.integer :year
|
43
|
-
t.timestamps
|
43
|
+
t.timestamps null: false
|
44
44
|
end
|
45
45
|
|
46
46
|
create_table :copyable_owners, force: true do |t|
|
47
47
|
t.string :name, null: false
|
48
|
-
t.timestamps
|
48
|
+
t.timestamps null: false
|
49
49
|
end
|
50
50
|
|
51
51
|
create_table :copyable_pet_foods, force: true do |t|
|
52
52
|
t.string :name, null: false
|
53
|
-
t.timestamps
|
53
|
+
t.timestamps null: false
|
54
54
|
end
|
55
55
|
|
56
56
|
create_table :copyable_pet_foods_pets, force: true, id: false do |t|
|
@@ -62,31 +62,31 @@ module Copyable
|
|
62
62
|
t.string :description, null: false
|
63
63
|
t.string :nickname
|
64
64
|
t.integer :copyable_pet_id, null: false
|
65
|
-
t.timestamps
|
65
|
+
t.timestamps null: false
|
66
66
|
end
|
67
67
|
|
68
68
|
create_table :copyable_pet_sitters, force: true do |t|
|
69
69
|
t.string :name, null: false
|
70
|
-
t.timestamps
|
70
|
+
t.timestamps null: false
|
71
71
|
end
|
72
72
|
|
73
73
|
create_table :copyable_pet_sitting_patronages, force: true do |t|
|
74
74
|
t.integer :copyable_pet_id, null: false
|
75
75
|
t.integer :copyable_pet_sitter_id, null: false
|
76
|
-
t.timestamps
|
76
|
+
t.timestamps null: false
|
77
77
|
end
|
78
78
|
|
79
79
|
create_table :copyable_pet_tags, force: true do |t|
|
80
80
|
t.string :registered_name, null: false
|
81
81
|
t.integer :copyable_pet_id, null: false
|
82
|
-
t.timestamps
|
82
|
+
t.timestamps null: false
|
83
83
|
end
|
84
84
|
|
85
85
|
create_table :copyable_pets, force: true do |t|
|
86
86
|
t.string :name, null: false
|
87
87
|
t.string :kind, null: false
|
88
88
|
t.integer :birth_year
|
89
|
-
t.timestamps
|
89
|
+
t.timestamps null: false
|
90
90
|
end
|
91
91
|
|
92
92
|
create_table :copyable_pictures, force: true do |t|
|
@@ -94,36 +94,36 @@ module Copyable
|
|
94
94
|
t.integer :imageable_id
|
95
95
|
t.string :imageable_type
|
96
96
|
t.integer :picture_album_id
|
97
|
-
t.timestamps
|
97
|
+
t.timestamps null: false
|
98
98
|
end
|
99
99
|
|
100
100
|
create_table :copyable_products, force: true do |t|
|
101
101
|
t.string :name
|
102
|
-
t.timestamps
|
102
|
+
t.timestamps null: false
|
103
103
|
end
|
104
104
|
|
105
105
|
create_table :copyable_toys, force: true do |t|
|
106
106
|
t.string :name
|
107
107
|
t.string :kind
|
108
108
|
t.integer :copyable_pet_id
|
109
|
-
t.timestamps
|
109
|
+
t.timestamps null: false
|
110
110
|
end
|
111
111
|
|
112
112
|
create_table :copyable_trees, force: true do |t|
|
113
113
|
t.string :kind
|
114
|
-
t.timestamps
|
114
|
+
t.timestamps null: false
|
115
115
|
end
|
116
116
|
|
117
117
|
create_table :copyable_vehicles, force: true do |t|
|
118
118
|
t.string :name
|
119
119
|
t.integer :copyable_owner_id
|
120
|
-
t.timestamps
|
120
|
+
t.timestamps null: false
|
121
121
|
end
|
122
122
|
|
123
123
|
create_table :copyable_warranties, force: true do |t|
|
124
124
|
t.string :name
|
125
125
|
t.integer :copyable_amenity_id
|
126
|
-
t.timestamps
|
126
|
+
t.timestamps null: false
|
127
127
|
end
|
128
128
|
|
129
129
|
end
|
data/spec/model_hooks_spec.rb
CHANGED
@@ -9,85 +9,31 @@ describe Copyable::ModelHooks do
|
|
9
9
|
before(:each) do
|
10
10
|
Copyable::ModelHooks.disable!(CopyableTree)
|
11
11
|
end
|
12
|
+
|
12
13
|
after(:each) do
|
13
14
|
Copyable::ModelHooks.reenable!(CopyableTree)
|
14
15
|
end
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
}.to_not raise_error
|
16
|
+
|
17
|
+
it 'defines an instance variable on the class' do
|
18
|
+
expect(CopyableTree.all_callbacks_disabled).to eq(true)
|
19
19
|
end
|
20
|
+
|
20
21
|
it 'should not prevent model actions from executing' do
|
21
22
|
expect(CopyableTree.count).to eq(0)
|
22
|
-
CopyableTree.create!(kind: 'magnolia')
|
23
|
-
expect(CopyableTree.count).to eq(1)
|
24
23
|
end
|
25
24
|
end
|
26
25
|
|
27
26
|
describe '.reenable!' do
|
28
|
-
it '
|
29
|
-
Copyable::ModelHooks.disable!(CopyableTree)
|
27
|
+
it 'defines an instance variable on the class' do
|
30
28
|
Copyable::ModelHooks.reenable!(CopyableTree)
|
31
|
-
expect
|
32
|
-
CopyableTree.create!(kind: 'magnolia')
|
33
|
-
}.to raise_error(RuntimeError, "callback2 called")
|
29
|
+
expect(CopyableTree.all_callbacks_disabled).to eq(false)
|
34
30
|
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
context 'validations' do
|
39
31
|
|
40
|
-
|
41
|
-
|
42
|
-
describe '.disable!' do
|
43
|
-
before(:each) do
|
44
|
-
Copyable::ModelHooks.disable!(CopyableCoin)
|
45
|
-
end
|
46
|
-
after(:each) do
|
47
|
-
Copyable::ModelHooks.reenable!(CopyableCoin)
|
48
|
-
end
|
49
|
-
it 'should prevent validations from executing' do
|
50
|
-
expect {
|
51
|
-
CopyableCoin.create!(year: -10)
|
52
|
-
}.to_not raise_error
|
53
|
-
end
|
54
|
-
it 'should not prevent model actions from executing' do
|
55
|
-
expect(CopyableCoin.count).to eq(0)
|
56
|
-
CopyableCoin.create!(year: -10)
|
57
|
-
expect(CopyableCoin.count).to eq(1)
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
describe '.reenable!' do
|
62
|
-
it 'should allow validations to execute again' do
|
63
|
-
Copyable::ModelHooks.disable!(CopyableCoin)
|
64
|
-
Copyable::ModelHooks.reenable!(CopyableCoin)
|
32
|
+
it 'should allow callbacks to execute again' do
|
65
33
|
expect {
|
66
|
-
|
67
|
-
}.to raise_error(
|
34
|
+
CopyableTree.create!(kind: 'magnolia')
|
35
|
+
}.to raise_error(RuntimeError, "callback2 called")
|
68
36
|
end
|
69
37
|
end
|
70
38
|
end
|
71
|
-
|
72
|
-
describe 'nested disables and enables' do
|
73
|
-
it 'should allow callbacks to execute again' do
|
74
|
-
Copyable::ModelHooks.disable!(CopyableTree)
|
75
|
-
expect { CopyableTree.create!(kind: 'magnolia') }.to_not raise_error
|
76
|
-
|
77
|
-
Copyable::ModelHooks.disable!(CopyableCoin)
|
78
|
-
expect { CopyableCoin.create!(year: -10) }.to_not raise_error
|
79
|
-
|
80
|
-
Copyable::ModelHooks.disable!(CopyableTree)
|
81
|
-
expect { CopyableTree.create!(kind: 'magnolia') }.to_not raise_error
|
82
|
-
|
83
|
-
Copyable::ModelHooks.reenable!(CopyableCoin)
|
84
|
-
expect { CopyableCoin.create!(year: -10) }.to raise_error(ActiveRecord::RecordInvalid)
|
85
|
-
|
86
|
-
Copyable::ModelHooks.reenable!(CopyableTree)
|
87
|
-
expect { CopyableTree.create!(kind: 'magnolia') }.to raise_error(RuntimeError)
|
88
|
-
|
89
|
-
Copyable::ModelHooks.reenable!(CopyableTree)
|
90
|
-
expect { CopyableTree.create!(kind: 'magnolia') }.to raise_error(RuntimeError)
|
91
|
-
end
|
92
|
-
end
|
93
39
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: copyable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Wyatt Greene
|
@@ -11,7 +11,7 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
date:
|
14
|
+
date: 2019-03-18 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: activerecord
|
@@ -19,14 +19,14 @@ dependencies:
|
|
19
19
|
requirements:
|
20
20
|
- - "~>"
|
21
21
|
- !ruby/object:Gem::Version
|
22
|
-
version: 4.
|
22
|
+
version: '4.2'
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
26
26
|
requirements:
|
27
27
|
- - "~>"
|
28
28
|
- !ruby/object:Gem::Version
|
29
|
-
version: 4.
|
29
|
+
version: '4.2'
|
30
30
|
- !ruby/object:Gem::Dependency
|
31
31
|
name: database_cleaner
|
32
32
|
requirement: !ruby/object:Gem::Requirement
|
@@ -87,16 +87,16 @@ dependencies:
|
|
87
87
|
name: sqlite3
|
88
88
|
requirement: !ruby/object:Gem::Requirement
|
89
89
|
requirements:
|
90
|
-
- - "
|
90
|
+
- - "~>"
|
91
91
|
- !ruby/object:Gem::Version
|
92
|
-
version:
|
92
|
+
version: 1.3.6
|
93
93
|
type: :development
|
94
94
|
prerelease: false
|
95
95
|
version_requirements: !ruby/object:Gem::Requirement
|
96
96
|
requirements:
|
97
|
-
- - "
|
97
|
+
- - "~>"
|
98
98
|
- !ruby/object:Gem::Version
|
99
|
-
version:
|
99
|
+
version: 1.3.6
|
100
100
|
description: Copyable makes it easy to copy ActiveRecord models.
|
101
101
|
email:
|
102
102
|
- dchan@dmgroupK12.com
|
@@ -178,7 +178,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
178
178
|
version: '0'
|
179
179
|
requirements: []
|
180
180
|
rubyforge_project:
|
181
|
-
rubygems_version: 2.4.
|
181
|
+
rubygems_version: 2.4.8
|
182
182
|
signing_key:
|
183
183
|
specification_version: 4
|
184
184
|
summary: ActiveRecord copier
|