arborist-rails 0.1.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/.gitignore +15 -0
- data/.rubocop.yml +20 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +317 -0
- data/Rakefile +7 -0
- data/arborist.gemspec +27 -0
- data/lib/arborist.rb +8 -0
- data/lib/arborist/configuration.rb +21 -0
- data/lib/arborist/exceptions.rb +25 -0
- data/lib/arborist/migration.rb +52 -0
- data/lib/arborist/migration/collection.rb +10 -0
- data/lib/arborist/migration/data.rb +53 -0
- data/lib/arborist/migration/data_migration.rb +23 -0
- data/lib/arborist/migration/model_arguments.rb +25 -0
- data/lib/arborist/migration/schema.rb +17 -0
- data/lib/arborist/version.rb +3 -0
- data/spec/arborist/confirguration_spec.rb +20 -0
- data/spec/arborist/migration/collection_spec.rb +9 -0
- data/spec/arborist/migration/model_arguments_spec.rb +33 -0
- data/spec/arborist/migration_spec.rb +118 -0
- data/spec/arborist_spec.rb +34 -0
- data/spec/spec_helper.rb +50 -0
- metadata +159 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: cdaa1c3f36f75f8d4c3ef4049bcd7775208b3055
|
4
|
+
data.tar.gz: e044762cbc071aaed0f5209d5b45b9e221b49039
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 73642c5648a2e03503be97a996430dcf14bab7e50150fac4ff0e479993e44d698d292a8cc78c4748e1746a9b4e2acee66260d9d3635deb61d3276b28ff0e6a8a
|
7
|
+
data.tar.gz: fe228e6fd604100982deb19e1f4807487f7a1cc41ace4f5e73e407763da613286daa2480e5f0f6bd603d380f0c6e43444e08ac3649c3f1188c0a1eff6158eaca
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 2.3
|
3
|
+
Exclude:
|
4
|
+
- 'db/**/*'
|
5
|
+
- 'node_modules/**/*'
|
6
|
+
- 'vendor/**/*'
|
7
|
+
- Guardfile
|
8
|
+
DisplayCopNames: true
|
9
|
+
Rails:
|
10
|
+
Enabled: true
|
11
|
+
Style/Documentation:
|
12
|
+
Enabled: false
|
13
|
+
Style/HashSyntax:
|
14
|
+
Exclude:
|
15
|
+
- Rakefile
|
16
|
+
- '**/*.rake'
|
17
|
+
Style/LambdaCall:
|
18
|
+
Enabled: false
|
19
|
+
Style/MultilineMethodCallIndentation:
|
20
|
+
Enabled: false
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Adam Cuppy
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,317 @@
|
|
1
|
+
# Arborist
|
2
|
+
|
3
|
+
## Usage
|
4
|
+
|
5
|
+
`Arborist::Migration` is meant to run as a drop-in replacement to
|
6
|
+
`ActiveRecord::Migration`. The easiest way to do that is modify your
|
7
|
+
migrations to inherit from `Arborist::Migration`
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
class AddAdminToUser < Arborist::Migration
|
11
|
+
data do
|
12
|
+
# forward
|
13
|
+
end
|
14
|
+
|
15
|
+
def change
|
16
|
+
add_column :users, :admin, :boolean
|
17
|
+
end
|
18
|
+
end
|
19
|
+
```
|
20
|
+
|
21
|
+
By default `data` takes a forward-only approach and assumes by rolling back
|
22
|
+
the schema would automatically revert the data migration. However, you can
|
23
|
+
declare both `up` and `down` migrations with similar corresponding method
|
24
|
+
options:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
class AddAdminToUser < Arborist::Migration
|
28
|
+
data :up do # => default
|
29
|
+
# forward
|
30
|
+
end
|
31
|
+
|
32
|
+
data :down do
|
33
|
+
# rollback
|
34
|
+
end
|
35
|
+
|
36
|
+
def change
|
37
|
+
add_column :users, :admin, :boolean
|
38
|
+
end
|
39
|
+
end
|
40
|
+
```
|
41
|
+
|
42
|
+
### Helpers
|
43
|
+
|
44
|
+
Although all `ActiveRecord::Migration` methods are supported, there are a set
|
45
|
+
of helpers to define one action against another.
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
class AddAdminToUser < Arborist::Migration
|
49
|
+
data do # => :up
|
50
|
+
# data only adjustments
|
51
|
+
end
|
52
|
+
|
53
|
+
schema do # => :change
|
54
|
+
add_column :users, :admin, :boolean
|
55
|
+
end
|
56
|
+
end
|
57
|
+
```
|
58
|
+
|
59
|
+
Optionally, pass a migration message:
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
class AddAdminToUser < ActiveRecord::Migration
|
63
|
+
data say: 'Updating admin flag' do
|
64
|
+
# ...
|
65
|
+
end
|
66
|
+
# ...
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
70
|
+
For more complex data migrations you can provide a class. The only
|
71
|
+
expectation is that the class being referenced includes a public `call`
|
72
|
+
method. And, like other previous implementations, you can provide options such
|
73
|
+
as `say`, `up` and `down` (to name a few).
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
class AddAdminToUser < Arborist::Migration
|
77
|
+
class UpdateAdminFlag
|
78
|
+
def call
|
79
|
+
# custom migration ...
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
data use: UpdateAdminFlag
|
84
|
+
|
85
|
+
def change
|
86
|
+
add_column :users, :admin, :boolean, default: false
|
87
|
+
end
|
88
|
+
end
|
89
|
+
```
|
90
|
+
|
91
|
+
Similar to other uses of `data` additional configuration options can be
|
92
|
+
passed in following the
|
93
|
+
|
94
|
+
### Interchangeable Models
|
95
|
+
|
96
|
+
A common 'best-practice' is to use raw SQL instead of `ActiveRecord` backed
|
97
|
+
classes, which is a totally practical option (see explanation below), but you
|
98
|
+
lose the power of `ActiveRecord`, *so what if we could use `ActiveRecord` to
|
99
|
+
support those changes?*
|
100
|
+
|
101
|
+
Interchangeable models can be powerful tool when tracking object references.
|
102
|
+
|
103
|
+
Instead of using the model directly, set the target model and use `model` in
|
104
|
+
the data migration:
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
class AddAdminToUser < Arborist::Migration
|
108
|
+
model :User
|
109
|
+
|
110
|
+
data do
|
111
|
+
model.find_each do |user|
|
112
|
+
user.admin = true
|
113
|
+
user.save!
|
114
|
+
end
|
115
|
+
end
|
116
|
+
# ...
|
117
|
+
end
|
118
|
+
```
|
119
|
+
|
120
|
+
Further, if you need to reference multiple models, you can do so by setting a
|
121
|
+
method reference for each:
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
class AddAdminToUser < Arborist::Migration
|
125
|
+
model :User, as: :user
|
126
|
+
model :Company, as: :company
|
127
|
+
|
128
|
+
data do
|
129
|
+
user.all # ...
|
130
|
+
company.all # ...
|
131
|
+
end
|
132
|
+
# ...
|
133
|
+
end
|
134
|
+
```
|
135
|
+
|
136
|
+
#### Now why not just reference the model directly?
|
137
|
+
|
138
|
+
Answer, Arborist intelligently detects if a model reference is missing (i.e.
|
139
|
+
removed at a later iteration) and provides a set of fallback options.
|
140
|
+
|
141
|
+
The most common strategy is to forward all model requests to the renamed model:
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
# ...
|
145
|
+
end
|
146
|
+
```
|
147
|
+
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
# ...
|
151
|
+
end
|
152
|
+
```
|
153
|
+
|
154
|
+
If this becomes confusing, that's okay. `Arborist` runs a built in linter
|
155
|
+
(`Arborist::Migration.lint!`) prior to migrating to confirm that all models
|
156
|
+
and attribute dependencies *being referenced* are available. If any failure
|
157
|
+
exists, the migration will fail *prior* to running all the migrations.
|
158
|
+
|
159
|
+
### Failure
|
160
|
+
|
161
|
+
`Arborist` will suggest a data migration for the model reference, either in the
|
162
|
+
form of an addition to the offending migration...
|
163
|
+
|
164
|
+
Add to migration 'db/migrate/1234567890_add_admin_to_person.rb':
|
165
|
+
|
166
|
+
```ruby
|
167
|
+
class AddAdminToPerson < Arborist::Migration
|
168
|
+
model :User => '...'
|
169
|
+
end
|
170
|
+
```
|
171
|
+
|
172
|
+
... or to generate a new data migration to fix the problem:
|
173
|
+
|
174
|
+
`$ rails g data_migration:model User`
|
175
|
+
|
176
|
+
Which generates:
|
177
|
+
|
178
|
+
```ruby
|
179
|
+
class UpdateReferenceForUserModel < Arborist::Migration
|
180
|
+
model :User => '...'
|
181
|
+
end
|
182
|
+
```
|
183
|
+
|
184
|
+
## Testing
|
185
|
+
|
186
|
+
By abstracting all larger migration routines to a nested class, we can test
|
187
|
+
those as Ruby objects.
|
188
|
+
|
189
|
+
With `RSpec` we can use a bank of custom matcher:
|
190
|
+
|
191
|
+
```rspec
|
192
|
+
require 'rails_helper'
|
193
|
+
require_migration 'add_admin_to_user' # Note: Do NOT include the datetime stamp
|
194
|
+
|
195
|
+
describe AddAdminToUser::Data do
|
196
|
+
# ...
|
197
|
+
end
|
198
|
+
```
|
199
|
+
|
200
|
+
## Methodology
|
201
|
+
|
202
|
+
Data migration in a Rails application can be a serious pain. Whether you take
|
203
|
+
the strategy of including the data migration in the schema migrations...
|
204
|
+
|
205
|
+
```ruby
|
206
|
+
class AddAdminToUser < ActiveRecord::Migration
|
207
|
+
def change
|
208
|
+
# Schema migration
|
209
|
+
add_column :users, :admin, :boolean, default: false
|
210
|
+
|
211
|
+
# Data migration
|
212
|
+
User.all.each do |user|
|
213
|
+
user.admin = true;
|
214
|
+
user.save!
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
```
|
219
|
+
|
220
|
+
Which at the time of generating the migration, works without issue; however,
|
221
|
+
down the line, we rename the `User` model and neglect to update this migration.
|
222
|
+
|
223
|
+
In the future, when we run the entire set of migration (vs.
|
224
|
+
`rake db:schema:load`) and the `User` model is missing, the migrations
|
225
|
+
explode - this sucks.
|
226
|
+
|
227
|
+
### Common Solutions
|
228
|
+
|
229
|
+
*Temporary Models*
|
230
|
+
|
231
|
+
```ruby
|
232
|
+
class AddAdminToUser < ActiveRecord::Migration
|
233
|
+
# Temporary class
|
234
|
+
class User < ActiveRecord::Base
|
235
|
+
end
|
236
|
+
|
237
|
+
# ...
|
238
|
+
end
|
239
|
+
```
|
240
|
+
|
241
|
+
And although this is a solution, this course of action results in duplicating
|
242
|
+
the interface. Additional issues can present themselves, because the `User`
|
243
|
+
model is under the `AddAdminToUser` namespace (`AddAdminToUser::User`), which
|
244
|
+
will present issues when setting a polymorphic association or following an
|
245
|
+
Single Table Inheritance (STI) model.
|
246
|
+
|
247
|
+
### Raw SQL
|
248
|
+
|
249
|
+
A Rails independent strategy, you can use straight SQL. Then ActiveRecord
|
250
|
+
models are not needed, and the presence (or lack) of the model is irrelevant.
|
251
|
+
|
252
|
+
```ruby
|
253
|
+
class AddAdminToUser < ActiveRecord::Migration
|
254
|
+
def change
|
255
|
+
# Schema migration
|
256
|
+
add_column :users, :admin, :boolean, default: false
|
257
|
+
|
258
|
+
# Data migration in raw SQL
|
259
|
+
execute <<-SQL
|
260
|
+
UPDATE `users` SET `users`.`admin` = true
|
261
|
+
SQL
|
262
|
+
end
|
263
|
+
end
|
264
|
+
```
|
265
|
+
|
266
|
+
As pointed out by many, this doesn't have many downsides, other than database
|
267
|
+
syntax differences.
|
268
|
+
|
269
|
+
### Rake tasks
|
270
|
+
|
271
|
+
If entirely opposed to including data migrations in the ActiveRecord migrations
|
272
|
+
themselves (all examples above), then it's common to create a one off rake
|
273
|
+
tasks, which would be run directly on the instance.
|
274
|
+
|
275
|
+
`$ rake data:add_admin_flag_to_current_users`
|
276
|
+
|
277
|
+
But it requires the command is run on all application instances and following
|
278
|
+
the appropriate migration (i.e. `AddAdminToUser`); which could be done via
|
279
|
+
the deployment hooks. However, you would be breaking the isolation of your
|
280
|
+
migrations (within `db/migrate`) and polluting `lib/tasks/` with one-off rake
|
281
|
+
tasks; requiring cleanup.
|
282
|
+
|
283
|
+
Other issues: testing a rake task can be challenging; and, in essence we're
|
284
|
+
exposing a production available routine that could cause serious issues, such
|
285
|
+
as adding the admin flag to all users.
|
286
|
+
|
287
|
+
Wouldn't it be nice if you could:
|
288
|
+
|
289
|
+
* Run data migrations side by side with the corresponding schema migration(s);
|
290
|
+
* Test the data migration routine;
|
291
|
+
* Optionally disable data migrations on an environment, such as production?
|
292
|
+
|
293
|
+
Sure, it would.
|
294
|
+
|
295
|
+
## Installation
|
296
|
+
|
297
|
+
Add this line to your application's Gemfile:
|
298
|
+
|
299
|
+
```ruby
|
300
|
+
gem 'arborist-rails'
|
301
|
+
```
|
302
|
+
|
303
|
+
And then execute:
|
304
|
+
|
305
|
+
`$ bundle`
|
306
|
+
|
307
|
+
Or install it yourself as:
|
308
|
+
|
309
|
+
`$ gem install arborist`
|
310
|
+
|
311
|
+
## Contributing
|
312
|
+
|
313
|
+
1. Fork it ( https://github.com/{my-github-username}/arborist/fork )
|
314
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
315
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
316
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
317
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/arborist.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'arborist/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "arborist-rails"
|
8
|
+
spec.version = Arborist::VERSION
|
9
|
+
spec.authors = ["Adam Cuppy"]
|
10
|
+
spec.email = ["adam@codingzeal.com"]
|
11
|
+
spec.summary = %q{Framework for working with data migrations and seeds
|
12
|
+
in a Rails application}
|
13
|
+
spec.homepage = 'https://github.com/CodingZeal/arborist'
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "activerecord", ">= 3.2.0"
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
23
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
24
|
+
spec.add_development_dependency "rspec"
|
25
|
+
spec.add_development_dependency "sqlite3"
|
26
|
+
spec.add_development_dependency "pry-byebug"
|
27
|
+
end
|
data/lib/arborist.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
module Arborist
|
4
|
+
def self.config ns = nil
|
5
|
+
@config ||= Configuration.new
|
6
|
+
@config[ns] ||= Configuration.new if ns
|
7
|
+
|
8
|
+
if block_given?
|
9
|
+
yield ns ? @config[ns] : @config
|
10
|
+
end
|
11
|
+
|
12
|
+
@config
|
13
|
+
end
|
14
|
+
|
15
|
+
class Configuration < OpenStruct
|
16
|
+
def initialize props={}
|
17
|
+
super
|
18
|
+
yield self if block_given?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Arborist
|
2
|
+
class ModelReferenceError < NameError
|
3
|
+
def initialize model_ref
|
4
|
+
super "#{model_ref} is not available"
|
5
|
+
end
|
6
|
+
|
7
|
+
def model
|
8
|
+
raise self
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class UnknownSchemaMethod < ArgumentError
|
13
|
+
def initialize method_name
|
14
|
+
super %Q{Unknown schema migration method: #{method_name}.
|
15
|
+
Use :up, :down or :change}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class InheritanceError < StandardError
|
20
|
+
def initialize method_name
|
21
|
+
super %Q{ Method not available in ActiveRecord::Migration. Inherit from
|
22
|
+
Arborist::Migration to use #{method_name}}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require_relative 'configuration'
|
2
|
+
|
3
|
+
module Arborist
|
4
|
+
|
5
|
+
config :migration do |c|
|
6
|
+
c.fallback = ModelReferenceError
|
7
|
+
c.default_method_name = :model
|
8
|
+
c.default_direction = :up
|
9
|
+
c.default_message = 'Migrating data...'
|
10
|
+
c.reset_column_information = true
|
11
|
+
end
|
12
|
+
|
13
|
+
class Migration < ActiveRecord::Migration
|
14
|
+
require_relative 'migration/collection'
|
15
|
+
require_relative 'migration/data'
|
16
|
+
require_relative 'migration/schema'
|
17
|
+
|
18
|
+
include Data
|
19
|
+
include Schema
|
20
|
+
|
21
|
+
class << self
|
22
|
+
attr_accessor :collection
|
23
|
+
|
24
|
+
def reset!
|
25
|
+
self.collection = Collection.new
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def config
|
31
|
+
Arborist.config.migration
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def exec_migration conn, dir
|
36
|
+
super conn, dir
|
37
|
+
collection[dir].each do |m|
|
38
|
+
m.report { instance_eval(&m.routine) }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def collection
|
45
|
+
self.class.collection
|
46
|
+
end
|
47
|
+
|
48
|
+
def config
|
49
|
+
self.class.config
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Arborist::Migration::Data
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
require_relative 'data_migration'
|
5
|
+
require_relative 'model_arguments'
|
6
|
+
|
7
|
+
Collection = Arborist::Migration::Collection
|
8
|
+
DataMigration = Arborist::Migration::DataMigration
|
9
|
+
ModelArguments = Arborist::Migration::ModelArguments
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
attr_accessor :model_ref
|
13
|
+
|
14
|
+
def data *args, &migration
|
15
|
+
data_migration = DataMigration.new *args, &migration
|
16
|
+
|
17
|
+
self.collection ||= Collection.new
|
18
|
+
self.collection[data_migration.direction] << data_migration
|
19
|
+
end
|
20
|
+
|
21
|
+
def model *args
|
22
|
+
model_args = ModelArguments.new args
|
23
|
+
define_model_reference model_args.model_ref
|
24
|
+
define_model_method model_args
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def define_model_method model_args
|
30
|
+
define_method model_args.method_name do
|
31
|
+
@_ref ||= {}
|
32
|
+
@_ref[model_args] ||= begin
|
33
|
+
ref = self.class.model_ref.fetch model_args.model_ref
|
34
|
+
|
35
|
+
if Arborist.config.migration.reset_column_information
|
36
|
+
ref.tap(&:reset_column_information)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def define_model_reference model_ref
|
43
|
+
self.model_ref ||= {}
|
44
|
+
self.model_ref[model_ref] ||= Object.const_get model_ref
|
45
|
+
rescue NameError
|
46
|
+
config.fallback.new(model_ref).model
|
47
|
+
end
|
48
|
+
|
49
|
+
def config
|
50
|
+
Arborist.config.migration
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class Arborist::Migration::DataMigration
|
2
|
+
attr_reader :direction, :routine
|
3
|
+
|
4
|
+
def initialize *args, &block
|
5
|
+
@options = args.extract_options!
|
6
|
+
@direction = args.first || config.default_direction
|
7
|
+
@routine = @options[:use].new rescue block
|
8
|
+
end
|
9
|
+
|
10
|
+
def report &block
|
11
|
+
puts "~> #{config.default_message} #{options[:say]}"
|
12
|
+
time = Benchmark.measure(&block)
|
13
|
+
puts "~> Completed. Time elapsed: %.4fs" % time.real
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
attr_reader :options
|
19
|
+
|
20
|
+
def config
|
21
|
+
Arborist.config.migration
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class Arborist::Migration::ModelArguments
|
2
|
+
attr_reader :model_ref, :method_name
|
3
|
+
|
4
|
+
def initialize args
|
5
|
+
options = args.extract_options!
|
6
|
+
|
7
|
+
@model_ref = args.first || model_from_options(options)
|
8
|
+
@method_name = options.fetch :as, config.default_method_name
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
RESERVED_OPTIONS = %i( as )
|
14
|
+
|
15
|
+
def model_from_options options
|
16
|
+
options
|
17
|
+
.select { |k, _| ! RESERVED_OPTIONS.include? k }
|
18
|
+
.values
|
19
|
+
.first
|
20
|
+
end
|
21
|
+
|
22
|
+
def config
|
23
|
+
Arborist.config.migration
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Arborist::Migration::Schema
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
module ClassMethods
|
5
|
+
def schema method = :change, &migration
|
6
|
+
if SCHEMA_MIGRATION_METHODS.include? method
|
7
|
+
define_method method, &migration
|
8
|
+
else
|
9
|
+
raise Arborist::UnknownSchemaMethod, method
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
SCHEMA_MIGRATION_METHODS = %i( up down change )
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'arborist/configuration'
|
3
|
+
|
4
|
+
module Arborist
|
5
|
+
RSpec.describe Configuration do
|
6
|
+
context 'when setting a value' do
|
7
|
+
subject(:config) { described_class.new }
|
8
|
+
|
9
|
+
specify do
|
10
|
+
config.foo = :bar
|
11
|
+
expect(config.foo).to eq :bar
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
context 'when setting initial props' do
|
16
|
+
subject(:config) { described_class.new foo: :bar }
|
17
|
+
specify { expect(config.foo).to eq :bar }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'arborist/migration/model_arguments'
|
3
|
+
|
4
|
+
describe Arborist::Migration::ModelArguments do
|
5
|
+
subject(:model_args) { described_class.new args }
|
6
|
+
|
7
|
+
context 'when default' do
|
8
|
+
let(:args) { [:TestModel] }
|
9
|
+
|
10
|
+
it { expect(model_args.model_ref).to eq :TestModel }
|
11
|
+
it { expect(model_args.method_name).to eq :model }
|
12
|
+
end
|
13
|
+
|
14
|
+
context 'when declaring a method name' do
|
15
|
+
let(:args) { [:TestModel, { as: :test_method }] }
|
16
|
+
|
17
|
+
it { expect(model_args.model_ref).to eq :TestModel }
|
18
|
+
it { expect(model_args.method_name).to eq :test_method }
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'when declaring a method name' do
|
22
|
+
let(:args) { [:TestModel, { as: :test_method }] }
|
23
|
+
|
24
|
+
it { expect(model_args.model_ref).to eq :TestModel }
|
25
|
+
it { expect(model_args.method_name).to eq :test_method }
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'when a fallback is defined' do
|
29
|
+
let(:args) { [{ :Unknown => :TestModel }] }
|
30
|
+
|
31
|
+
it { expect(model_args.model_ref).to eq :TestModel }
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Arborist::Migration do
|
4
|
+
describe 'public interface' do
|
5
|
+
%i( collection model_ref data model reset! ).each do |class_method|
|
6
|
+
it { expect(described_class).to respond_to class_method }
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
describe 'configuration' do
|
11
|
+
it 'sets up a migration container' do
|
12
|
+
expect(Arborist.config.migration).to be_a Arborist::Configuration
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '.data' do
|
17
|
+
after { Arborist::Migration.reset! }
|
18
|
+
|
19
|
+
context 'without specifying a direction' do
|
20
|
+
it 'adds a migration routine to the :up collection' do
|
21
|
+
Arborist::Migration.data { :noop }
|
22
|
+
expect(Arborist::Migration.collection[:up].length).to eq 1
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'when specifying a direction' do
|
27
|
+
it 'adds a migration routine to the appropriate collection' do
|
28
|
+
Arborist::Migration.data(:down) { :noop }
|
29
|
+
|
30
|
+
expect(Arborist::Migration.collection[:up].length).to eq 0
|
31
|
+
expect(Arborist::Migration.collection[:down].length).to eq 1
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'when providing a data class' do
|
36
|
+
it 'delegates to the class' do
|
37
|
+
Arborist::Migration.data use: Proc
|
38
|
+
|
39
|
+
expect(Arborist::Migration.collection[:up].length).to eq 1
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe '.schema' do
|
45
|
+
after { Arborist::Migration.reset! }
|
46
|
+
|
47
|
+
context 'when a migration method is passed in' do
|
48
|
+
before do
|
49
|
+
TestMigration.class_eval do
|
50
|
+
schema(:up) {}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
it { expect(TestMigration.new).to respond_to :up }
|
55
|
+
end
|
56
|
+
|
57
|
+
context 'when a migration method being passed in does not exist' do
|
58
|
+
specify do
|
59
|
+
expect {
|
60
|
+
TestMigration.class_eval { schema(:foo) {} }
|
61
|
+
}.to raise_error Arborist::UnknownSchemaMethod
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe '.model' do
|
67
|
+
context 'when the referenced model exists' do
|
68
|
+
before { Arborist::Migration.model :TestModel }
|
69
|
+
|
70
|
+
it 'defines a model reference via #model' do
|
71
|
+
expect(Arborist::Migration.new.model).to eq TestModel
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'resets the column information' do
|
75
|
+
expect(Arborist::Migration.new.model).to eq TestModel
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
context 'when the model being referenced does not exist' do
|
80
|
+
specify do
|
81
|
+
expect { Arborist::Migration.model :UnknownModel }
|
82
|
+
.to raise_error Arborist::ModelReferenceError
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
describe '.reset!' do
|
89
|
+
it 'clears out the collection of migrations' do
|
90
|
+
Arborist::Migration.data { :noop }
|
91
|
+
expect(Arborist::Migration.collection[:up].length).to eq 1
|
92
|
+
Arborist::Migration.reset!
|
93
|
+
expect(Arborist::Migration.collection[:up].length).to eq 0
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
describe TestMigration do
|
99
|
+
before :all do
|
100
|
+
define_schema do
|
101
|
+
create_table(:test) { |t| t.timestamps null: false }
|
102
|
+
end
|
103
|
+
|
104
|
+
TestModel.create!
|
105
|
+
end
|
106
|
+
|
107
|
+
describe 'migrating up' do
|
108
|
+
it 'fills in the missing value' do
|
109
|
+
expect(TestModel.first).to_not respond_to :fullname
|
110
|
+
|
111
|
+
ActiveRecord::Migration.run TestMigration
|
112
|
+
expect(TestModel.first.fullname).to be_present
|
113
|
+
|
114
|
+
ActiveRecord::Migration.run SecondMigration
|
115
|
+
expect(TestModel.first.full_name).to be_present
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Arborist do
|
4
|
+
it 'has a version number' do
|
5
|
+
expect(Arborist::VERSION).not_to be nil
|
6
|
+
end
|
7
|
+
|
8
|
+
describe '.config' do
|
9
|
+
context 'without defining a namespace' do
|
10
|
+
it 'creates a blank container at the root level' do
|
11
|
+
expect(Arborist.config).to be_a Arborist::Configuration
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
context 'when defining a namespaces' do
|
16
|
+
it 'creates an empty container' do
|
17
|
+
Arborist.module_eval { config :test }
|
18
|
+
expect(Arborist.config.test).to be_a Arborist::Configuration
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
context 'when passing a block' do
|
23
|
+
it 'yields a configurable object' do
|
24
|
+
Arborist.module_eval do
|
25
|
+
config { |c| c.foo = :bar } # on the root
|
26
|
+
config(:test) { |c| c.foo2 = :bar2 } # namespaced
|
27
|
+
end
|
28
|
+
|
29
|
+
expect(Arborist.config.foo).to eq :bar
|
30
|
+
expect(Arborist.config.test.foo2).to eq :bar2
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
2
|
+
require 'pry-byebug'
|
3
|
+
require 'arborist'
|
4
|
+
|
5
|
+
RSpec.configure do |c|
|
6
|
+
c.after(:all) { teardown_db }
|
7
|
+
end
|
8
|
+
|
9
|
+
ActiveRecord::Base.establish_connection(
|
10
|
+
adapter: "sqlite3",
|
11
|
+
database: ":memory:"
|
12
|
+
)
|
13
|
+
|
14
|
+
def define_schema(verbose = false, &schema)
|
15
|
+
ActiveRecord::Schema.verbose = verbose
|
16
|
+
ActiveRecord::Schema.define version: 1, &schema
|
17
|
+
end
|
18
|
+
|
19
|
+
def teardown_db
|
20
|
+
conn = ActiveRecord::Base.connection
|
21
|
+
conn.tables.each { |t| conn.drop_table t }
|
22
|
+
end
|
23
|
+
|
24
|
+
class TestModel < ActiveRecord::Base
|
25
|
+
self.table_name = :test
|
26
|
+
end
|
27
|
+
|
28
|
+
class TestMigration < Arborist::Migration
|
29
|
+
model :TestModel
|
30
|
+
|
31
|
+
data say: 'Filling in fullname' do
|
32
|
+
model.find_each do |tm|
|
33
|
+
tm.fullname = 'abc'
|
34
|
+
tm.save!
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def change
|
39
|
+
add_column :test, :fullname, :string
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class SecondMigration < Arborist::Migration
|
44
|
+
model :TestModel
|
45
|
+
data { :noop }
|
46
|
+
|
47
|
+
def change
|
48
|
+
rename_column :test, :fullname, :full_name
|
49
|
+
end
|
50
|
+
end
|
metadata
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: arborist-rails
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Adam Cuppy
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-08-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 3.2.0
|
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.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.7'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.7'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: sqlite3
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: pry-byebug
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description:
|
98
|
+
email:
|
99
|
+
- adam@codingzeal.com
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- ".gitignore"
|
105
|
+
- ".rubocop.yml"
|
106
|
+
- ".travis.yml"
|
107
|
+
- Gemfile
|
108
|
+
- LICENSE.txt
|
109
|
+
- README.md
|
110
|
+
- Rakefile
|
111
|
+
- arborist.gemspec
|
112
|
+
- lib/arborist.rb
|
113
|
+
- lib/arborist/configuration.rb
|
114
|
+
- lib/arborist/exceptions.rb
|
115
|
+
- lib/arborist/migration.rb
|
116
|
+
- lib/arborist/migration/collection.rb
|
117
|
+
- lib/arborist/migration/data.rb
|
118
|
+
- lib/arborist/migration/data_migration.rb
|
119
|
+
- lib/arborist/migration/model_arguments.rb
|
120
|
+
- lib/arborist/migration/schema.rb
|
121
|
+
- lib/arborist/version.rb
|
122
|
+
- spec/arborist/confirguration_spec.rb
|
123
|
+
- spec/arborist/migration/collection_spec.rb
|
124
|
+
- spec/arborist/migration/model_arguments_spec.rb
|
125
|
+
- spec/arborist/migration_spec.rb
|
126
|
+
- spec/arborist_spec.rb
|
127
|
+
- spec/spec_helper.rb
|
128
|
+
homepage: https://github.com/CodingZeal/arborist
|
129
|
+
licenses:
|
130
|
+
- MIT
|
131
|
+
metadata: {}
|
132
|
+
post_install_message:
|
133
|
+
rdoc_options: []
|
134
|
+
require_paths:
|
135
|
+
- lib
|
136
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
137
|
+
requirements:
|
138
|
+
- - ">="
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: '0'
|
141
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
requirements: []
|
147
|
+
rubyforge_project:
|
148
|
+
rubygems_version: 2.2.2
|
149
|
+
signing_key:
|
150
|
+
specification_version: 4
|
151
|
+
summary: Framework for working with data migrations and seeds in a Rails application
|
152
|
+
test_files:
|
153
|
+
- spec/arborist/confirguration_spec.rb
|
154
|
+
- spec/arborist/migration/collection_spec.rb
|
155
|
+
- spec/arborist/migration/model_arguments_spec.rb
|
156
|
+
- spec/arborist/migration_spec.rb
|
157
|
+
- spec/arborist_spec.rb
|
158
|
+
- spec/spec_helper.rb
|
159
|
+
has_rdoc:
|