request_migrations 0.1.0 → 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +183 -40
- data/lib/request_migrations/configuration.rb +29 -4
- data/lib/request_migrations/controller.rb +20 -3
- data/lib/request_migrations/gem.rb +1 -1
- data/lib/request_migrations/migration.rb +53 -0
- data/lib/request_migrations/migrator.rb +13 -1
- data/lib/request_migrations/railtie.rb +2 -0
- data/lib/request_migrations/router.rb +16 -0
- data/lib/request_migrations/version.rb +2 -0
- data/lib/request_migrations.rb +43 -2
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1575eecb9ad23c9dae9534b03a31acbcef8c6ed7bf46f4ffca1748ef21fcead4
|
4
|
+
data.tar.gz: 141dbbf4576e8b2da641ab32bd377173bbe61717d330c741c0b10c5cf9eff854
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 119241518c4d8a9a954bd4918a6e2edbc0338938c7173953a406821f34f0f4da735e7819b7cc0e929be45c78a7da88f035f1456d5b98d53d6cb00b0fca1c8362
|
7
|
+
data.tar.gz: 5afffe316b08692daeaf2366fab161f23794b80326112a2b4c4ece1a13f0d40f8d61423aa81d4bf28c62274acf8a33dd63c01214295524cd7691c42203a71575
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -3,12 +3,17 @@
|
|
3
3
|
[![Gem Version](https://badge.fury.io/rb/request_migrations.svg)](https://badge.fury.io/rb/request_migrations)
|
4
4
|
|
5
5
|
**Make breaking API changes without breaking things!** Use `request_migrations` to craft
|
6
|
-
backwards-compatible migrations for API requests, responses, and more.
|
7
|
-
|
6
|
+
backwards-compatible migrations for API requests, responses, and more. Read [the blog
|
7
|
+
post](https://keygen.sh/blog/breaking-things-without-breaking-things/).
|
8
|
+
|
9
|
+
This gem was extracted from [Keygen](https://keygen.sh) and is being used in production
|
10
|
+
to serve millions of API requests per day.
|
11
|
+
|
12
|
+
![request_migrations diagram](https://user-images.githubusercontent.com/6979737/175964358-a2d8951d-46c6-4962-9f5e-0569cbf5972e.png)
|
8
13
|
|
9
14
|
Sponsored by:
|
10
15
|
|
11
|
-
[![Keygen logo](https://
|
16
|
+
[![Keygen logo](https://user-images.githubusercontent.com/6979737/175406169-bd8bf064-7343-4bd1-94b7-a773ecec07b8.png)](https://keygen.sh)
|
12
17
|
|
13
18
|
_A software licensing and distribution API built for developers._
|
14
19
|
|
@@ -47,8 +52,9 @@ _We're working on improving the docs._
|
|
47
52
|
|
48
53
|
- Define migrations for migrating a response between versions.
|
49
54
|
- Define migrations for migrating a request between versions.
|
50
|
-
- Define migrations for applying
|
55
|
+
- Define migrations for applying data migrations.
|
51
56
|
- Define version-based routing constraints.
|
57
|
+
- It's fast.
|
52
58
|
|
53
59
|
## Usage
|
54
60
|
|
@@ -126,7 +132,7 @@ Next, we'll need to configure `request_migrations` via an initializer under
|
|
126
132
|
|
127
133
|
```ruby
|
128
134
|
RequestMigrations.configure do |config|
|
129
|
-
# Define a resolver to determine the
|
135
|
+
# Define a resolver to determine the target version. Here, you can perform
|
130
136
|
# a lookup on the current user using request parameters, or simply use
|
131
137
|
# a header like we are here, defaulting to the latest version.
|
132
138
|
config.request_version_resolver = -> request {
|
@@ -138,7 +144,7 @@ RequestMigrations.configure do |config|
|
|
138
144
|
|
139
145
|
# Define previous versions and their migrations, in descending order.
|
140
146
|
config.versions = {
|
141
|
-
'1.0' => %i[
|
147
|
+
'1.0' => %i[combine_names_for_user_migration],
|
142
148
|
}
|
143
149
|
end
|
144
150
|
```
|
@@ -149,6 +155,14 @@ are applied:
|
|
149
155
|
```ruby
|
150
156
|
class ApplicationController < ActionController::API
|
151
157
|
include RequestMigrations::Controller::Migrations
|
158
|
+
|
159
|
+
# Optionally rescue from requests for unsupported versions
|
160
|
+
rescue_from RequestMigrations::UnsupportedVersionError, with: -> {
|
161
|
+
render(
|
162
|
+
json: { error: 'unsupported API version requested', code: 'INVALID_API_VERSION' },
|
163
|
+
status: :bad_request,
|
164
|
+
)
|
165
|
+
}
|
152
166
|
end
|
153
167
|
```
|
154
168
|
|
@@ -177,7 +191,12 @@ end
|
|
177
191
|
|
178
192
|
The `response` method accepts an `:if` keyword, which should be a lambda
|
179
193
|
that evaluates to a boolean, which determines whether or not the migration
|
180
|
-
should be applied.
|
194
|
+
should be applied. An `ActionDispatch::Response` will be yielded, the
|
195
|
+
current response (calls `controller#response`).
|
196
|
+
|
197
|
+
The gem makes no assumption on a response's content type or what the migration
|
198
|
+
will do. You could, for example, migrate the response body, or mutate the
|
199
|
+
headers, or even change the response's status code.
|
181
200
|
|
182
201
|
### Request migrations
|
183
202
|
|
@@ -197,19 +216,25 @@ end
|
|
197
216
|
|
198
217
|
The `request` method accepts an `:if` keyword, which should be a lambda
|
199
218
|
that evaluates to a boolean, which determines whether or not the migration
|
200
|
-
should be applied.
|
219
|
+
should be applied. An `ActionDispatch::Request` object will be yielded,
|
220
|
+
the current request (calls `controller#request`).
|
221
|
+
|
222
|
+
Again, like with response migrations, the gem makes no assumption on what
|
223
|
+
a migration does. A migration could mutate a request's params, or mutate
|
224
|
+
headers. It's up to you, all it does is provide the request.
|
225
|
+
|
226
|
+
Request migrations should [avoid using the `migrate` method](#avoid-migrate-for-request-migrations).
|
201
227
|
|
202
|
-
###
|
228
|
+
### Data migrations
|
203
229
|
|
204
230
|
In our first scenario, where we combined our user's name attributes, we defined
|
205
231
|
our migration using the `migrate` class method. At this point, you may be wondering
|
206
232
|
why we did that, since we didn't use that method for the 2 previous request and
|
207
233
|
response migrations above.
|
208
234
|
|
209
|
-
Well, it comes down to support for
|
210
|
-
|
211
|
-
|
212
|
-
Let's go back to our first example, `CombineNamesForUserMigration`.
|
235
|
+
Well, it comes down to support for data migrations (as well as offering a nice
|
236
|
+
interface for pattern matching inputs). Let's go back to our first example,
|
237
|
+
`CombineNamesForUserMigration`.
|
213
238
|
|
214
239
|
```ruby
|
215
240
|
class CombineNamesForUserMigration < RequestMigrations::Migration
|
@@ -239,7 +264,7 @@ end
|
|
239
264
|
```
|
240
265
|
|
241
266
|
What if we had [a webhook system](https://keygen.sh/blog/how-to-build-a-webhook-system-in-rails-using-sidekiq/)
|
242
|
-
that we also needed to apply these migrations to? Well, we can use a
|
267
|
+
that we also needed to apply these migrations to? Well, we can use a data migration
|
243
268
|
here, via the `Migrator` class:
|
244
269
|
|
245
270
|
```ruby
|
@@ -265,15 +290,19 @@ class WebhookWorker
|
|
265
290
|
end
|
266
291
|
```
|
267
292
|
|
268
|
-
|
293
|
+
This will apply the block defined in `migrate` onto our data. With that,
|
294
|
+
we've successfully applied a migration to both our API responses, as well
|
269
295
|
as to the webhook events we send. In this case, if our `event` matches the
|
270
296
|
our user shape, e.g. `type: 'user'`, then the migration will be applied.
|
271
297
|
|
298
|
+
In addition to data migrations, this allows for easier testing.
|
299
|
+
|
272
300
|
### Routing constraints
|
273
301
|
|
274
302
|
When you want to encourage API clients to upgrade, you can utilize a routing `version_constraint`
|
275
|
-
to define routes only available for certain versions.
|
276
|
-
|
303
|
+
to define routes only available for certain versions.
|
304
|
+
|
305
|
+
You can also utilize routing constraints to remove an API endpoint entirely.
|
277
306
|
|
278
307
|
```ruby
|
279
308
|
Rails.application.routes.draw do
|
@@ -291,13 +320,13 @@ Rails.application.routes.draw do
|
|
291
320
|
end
|
292
321
|
```
|
293
322
|
|
294
|
-
Currently, routing constraints only work for the `:semver` version format.
|
323
|
+
Currently, routing constraints only work for the `:semver` version format. (PRs welcome!)
|
295
324
|
|
296
325
|
### Configuration
|
297
326
|
|
298
327
|
```ruby
|
299
328
|
RequestMigrations.configure do |config|
|
300
|
-
# Define a resolver to determine the
|
329
|
+
# Define a resolver to determine the target version. Here, you can perform
|
301
330
|
# a lookup on the current user using request parameters, or simply use
|
302
331
|
# a header like we are here, defaulting to the latest version.
|
303
332
|
config.request_version_resolver = -> request {
|
@@ -324,9 +353,8 @@ RequestMigrations.configure do |config|
|
|
324
353
|
],
|
325
354
|
}
|
326
355
|
|
327
|
-
# Use a custom logger.
|
328
|
-
|
329
|
-
config.logger = ActiveSupport::TaggedLogging.new(...)
|
356
|
+
# Use a custom logger. Supports ActiveSupport::TaggedLogging.
|
357
|
+
config.logger = Rails.logger
|
330
358
|
end
|
331
359
|
```
|
332
360
|
|
@@ -335,19 +363,53 @@ end
|
|
335
363
|
By default, `request_migrations` uses a `:semver` version format, but it can be configured
|
336
364
|
to instead use one of the following, set via `config.version_format=`.
|
337
365
|
|
338
|
-
| Format |
|
339
|
-
|
340
|
-
| `:semver` | Use semantic versions, e.g. `1.0`, `1.1
|
341
|
-
| `:date` | Use date versions, e.g. `2020-09-02`, `2021-01-01`.
|
342
|
-
| `:integer` | Use integer versions, e.g. `1`, `2`, and `3`.
|
343
|
-
| `:float` | Use float versions, e.g. `1.0`, `1.1`, and `2.0`.
|
344
|
-
| `:string` | Use string versions, e.g. `a`, `b`, and `z`.
|
366
|
+
| Format | |
|
367
|
+
|:-----------|:-----------------------------------------------------|
|
368
|
+
| `:semver` | Use semantic versions, e.g. `1.0`, `1.1`, and `2.0`. |
|
369
|
+
| `:date` | Use date versions, e.g. `2020-09-02`, `2021-01-01`. |
|
370
|
+
| `:integer` | Use integer versions, e.g. `1`, `2`, and `3`. |
|
371
|
+
| `:float` | Use float versions, e.g. `1.0`, `1.1`, and `2.0`. |
|
372
|
+
| `:string` | Use string versions, e.g. `a`, `b`, and `z`. |
|
373
|
+
|
374
|
+
All versions will be sorted according to the format's type.
|
375
|
+
|
376
|
+
## Testing
|
377
|
+
|
378
|
+
Using data migrations allows for easier testing of migrations. For example, using Rspec:
|
379
|
+
|
380
|
+
```ruby
|
381
|
+
describe CombineNamesForUserMigration do
|
382
|
+
before do
|
383
|
+
RequestMigrations.configure do |config|
|
384
|
+
config.current_version = '1.1'
|
385
|
+
config.versions = {
|
386
|
+
'1.0' => [CombineNamesForUserMigration],
|
387
|
+
}
|
388
|
+
end
|
389
|
+
end
|
345
390
|
|
346
|
-
|
391
|
+
it 'should migrate user name attributes' do
|
392
|
+
migrator = RequestMigrations::Migrator.new(from: '1.1', to: '1.0')
|
393
|
+
data = serialize(
|
394
|
+
create(:user, first_name: 'John', last_name: 'Doe'),
|
395
|
+
)
|
347
396
|
|
348
|
-
|
397
|
+
expect(data).to include(type: 'user', first_name: 'John', last_name: 'Doe')
|
398
|
+
expect(data).to_not include(name: anything)
|
349
399
|
|
350
|
-
|
400
|
+
migrator.migrate!(data:)
|
401
|
+
|
402
|
+
expect(data).to include(type: 'user', name: 'John Doe')
|
403
|
+
expect(data).to_not include(first_name: 'John', last_name: 'Doe')
|
404
|
+
end
|
405
|
+
end
|
406
|
+
```
|
407
|
+
|
408
|
+
## Tips and tricks
|
409
|
+
|
410
|
+
Over the years, we're learned a thing or two about versioning an API. We'll share tips here.
|
411
|
+
|
412
|
+
### Use pattern matching
|
351
413
|
|
352
414
|
Pattern matching really cleans up the `:if` conditions, and overall makes migrations more readable.
|
353
415
|
|
@@ -382,7 +444,7 @@ end
|
|
382
444
|
|
383
445
|
Just be sure to remember your `else` block when `case` pattern matching. :)
|
384
446
|
|
385
|
-
|
447
|
+
### Route helpers
|
386
448
|
|
387
449
|
If you need to use route helpers in a migration, include them in your migration:
|
388
450
|
|
@@ -392,7 +454,7 @@ class SomeMigration < RequestMigrations::Migration
|
|
392
454
|
end
|
393
455
|
```
|
394
456
|
|
395
|
-
|
457
|
+
### Separate by shape
|
396
458
|
|
397
459
|
Define separate migrations for different input shapes, e.g. define a migration for an `#index`
|
398
460
|
to migrate an array of objects, and define another migration that handles the singular object
|
@@ -454,7 +516,7 @@ end
|
|
454
516
|
|
455
517
|
Note that the `migrate` method now migrates an array input, and matches on the `#index` route.
|
456
518
|
|
457
|
-
|
519
|
+
### Always check response status
|
458
520
|
|
459
521
|
Always check a response's status. You don't want to unintentionally apply migrations to error
|
460
522
|
responses.
|
@@ -467,7 +529,9 @@ class SomeMigration < RequestMigrations::Migration
|
|
467
529
|
end
|
468
530
|
```
|
469
531
|
|
470
|
-
|
532
|
+
Also mind `204 No Content`, since the response body will be `nil`.
|
533
|
+
|
534
|
+
### Don't match on URL pattern
|
471
535
|
|
472
536
|
Don't match on URL pattern. Instead, use `response.request.params` to access the request params
|
473
537
|
in a `response` migration, and use the `:controller` and `:action` params to determine route.
|
@@ -482,7 +546,7 @@ class SomeMigration < RequestMigrations::Migration
|
|
482
546
|
end
|
483
547
|
```
|
484
548
|
|
485
|
-
|
549
|
+
### Namespace deprecated controllers
|
486
550
|
|
487
551
|
When you need to entirely change a controller or service class, use a `V1x0::UsersController`-style
|
488
552
|
namespace to keep the old deprecated classes tidy.
|
@@ -495,12 +559,36 @@ class V1x0::UsersController
|
|
495
559
|
end
|
496
560
|
```
|
497
561
|
|
562
|
+
### Avoid migrate for request migrations
|
498
563
|
|
499
|
-
|
564
|
+
Avoid using `migrate` for request migrations. If you do, then data migrations, e.g. for
|
565
|
+
webhooks, will attempt to apply the request migrations. This may erroneously produce bad
|
566
|
+
output, or even undo a response migration. Instead, keep all request migration logic,
|
567
|
+
e.g. transforming params, inside of the `request` block.
|
568
|
+
|
569
|
+
```ruby
|
570
|
+
class SomeMigration < RequestMigrations::Migration
|
571
|
+
# Bad (side-effects for data migrations)
|
572
|
+
migrate do |params|
|
573
|
+
params[:foo] = params.delete(:bar)
|
574
|
+
end
|
575
|
+
|
576
|
+
request do |req|
|
577
|
+
migrate!(req.params)
|
578
|
+
end
|
579
|
+
|
580
|
+
# Good
|
581
|
+
request do |req|
|
582
|
+
req.params[:foo] = req.params.delete(:bar)
|
583
|
+
end
|
584
|
+
end
|
585
|
+
```
|
586
|
+
|
587
|
+
### Avoid routing contraints
|
500
588
|
|
501
589
|
Avoid using routing version constraints that remove functionality. They can be a headache
|
502
|
-
during upgrades. Consider only making _additive_ changes.
|
503
|
-
or deprecated endpoints to limit any new usage.
|
590
|
+
during upgrades. Consider only making _additive_ changes. Instead, consider removing or
|
591
|
+
hiding the documenation for old or deprecated endpoints, to limit any new usage.
|
504
592
|
|
505
593
|
```ruby
|
506
594
|
Rails.application.routes.draw do
|
@@ -518,6 +606,61 @@ Rails.application.routes.draw do
|
|
518
606
|
end
|
519
607
|
```
|
520
608
|
|
609
|
+
### Avoid n+1s
|
610
|
+
|
611
|
+
Avoid introducing n+1 queries in your migrations. Try to utilize the current data you have
|
612
|
+
to perform more meaningful queries, returning only the data needed for the migration.
|
613
|
+
|
614
|
+
```ruby
|
615
|
+
class AddRecentPostToUsersMigration < RequestMigrations::Migration
|
616
|
+
description %(adds :recent_post association to a collection of users)
|
617
|
+
|
618
|
+
# Bad (n+1)
|
619
|
+
migrate if: -> data { data in [*, { type: 'user' }, *] do |data|
|
620
|
+
data.each do |record|
|
621
|
+
case record
|
622
|
+
in type: 'user', id:
|
623
|
+
recent_post = Post.reorder(created_at: :desc)
|
624
|
+
.find_by(user_id: id)
|
625
|
+
|
626
|
+
record[:recent_post] = recent_post&.id
|
627
|
+
else
|
628
|
+
end
|
629
|
+
end
|
630
|
+
end
|
631
|
+
|
632
|
+
# Good
|
633
|
+
migrate if: -> data { data in [*, { type: 'user' }, *] do |data|
|
634
|
+
user_ids = data.collect { _1[:id] }
|
635
|
+
post_ids = Post.select(:id, :user_id)
|
636
|
+
.distinct_on(:user_id)
|
637
|
+
.where(user_id: user_ids)
|
638
|
+
.reorder(created_at: :desc)
|
639
|
+
.group_by(&:user_id)
|
640
|
+
|
641
|
+
data.each do |record|
|
642
|
+
case record
|
643
|
+
in type: 'user', id: user_id
|
644
|
+
record[:recent_post] = post_ids[user_id]&.id
|
645
|
+
else
|
646
|
+
end
|
647
|
+
end
|
648
|
+
end
|
649
|
+
|
650
|
+
response if: -> res { res.successful? && res.request.params in controller: 'api/v1/users',
|
651
|
+
action: 'index' } do |res|
|
652
|
+
data = JSON.parse(res.body, symbolize_names: true)
|
653
|
+
|
654
|
+
migrate!(data)
|
655
|
+
|
656
|
+
res.body = JSON.generate(data)
|
657
|
+
end
|
658
|
+
end
|
659
|
+
```
|
660
|
+
|
661
|
+
Instead of potentially tens or hundreds of queries, we make a single purposeful query
|
662
|
+
to get the data we need in order to complete the migration.
|
663
|
+
|
521
664
|
---
|
522
665
|
|
523
666
|
Have a tip of your own? Open a pull request!
|
@@ -4,10 +4,35 @@ module RequestMigrations
|
|
4
4
|
class Configuration
|
5
5
|
include ActiveSupport::Configurable
|
6
6
|
|
7
|
-
|
7
|
+
##
|
8
|
+
# logger defines the logger used by request_migrations.
|
9
|
+
#
|
10
|
+
# @return [Logger] the logger.
|
11
|
+
config_accessor(:logger) { Logger.new("/dev/null") }
|
12
|
+
|
13
|
+
##
|
14
|
+
# request_version_resolver defines how request_migrations should resolve the
|
15
|
+
# current version of a request.
|
16
|
+
#
|
17
|
+
# @return [Proc] the request version resolver.
|
8
18
|
config_accessor(:request_version_resolver) { -> req { self.current_version } }
|
9
|
-
|
10
|
-
|
11
|
-
|
19
|
+
|
20
|
+
##
|
21
|
+
# version_format defines the version format.
|
22
|
+
#
|
23
|
+
# @return [Symbol] format
|
24
|
+
config_accessor(:version_format) { :semver }
|
25
|
+
|
26
|
+
##
|
27
|
+
# current_version defines the latest version.
|
28
|
+
#
|
29
|
+
# @return [String, Integer, Float, nil] the current version.
|
30
|
+
config_accessor(:current_version) { nil }
|
31
|
+
|
32
|
+
##
|
33
|
+
# versions defines past versions and their migrations.
|
34
|
+
#
|
35
|
+
# @return [Hash<String, Array<Symbol, String, Class>>] past versions.
|
36
|
+
config_accessor(:versions) { {} }
|
12
37
|
end
|
13
38
|
end
|
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
module RequestMigrations
|
4
4
|
module Controller
|
5
|
+
##
|
6
|
+
# @private
|
5
7
|
class Migrator < Migrator
|
6
8
|
def initialize(request:, response:, **kwargs)
|
7
9
|
super(**kwargs)
|
@@ -13,7 +15,7 @@ module RequestMigrations
|
|
13
15
|
def migrate!
|
14
16
|
logger.debug { "Migrating from #{current_version} to #{target_version} (#{migrations.size} potential migrations)" }
|
15
17
|
|
16
|
-
migrations.each_with_index { |migration, i|
|
18
|
+
migrations.reverse.each_with_index { |migration, i|
|
17
19
|
logger.debug { "Applying migration #{migration} (#{i + 1}/#{migrations.size})" }
|
18
20
|
|
19
21
|
migration.new.migrate_request!(request)
|
@@ -35,9 +37,24 @@ module RequestMigrations
|
|
35
37
|
attr_accessor :request,
|
36
38
|
:response
|
37
39
|
|
38
|
-
def logger
|
40
|
+
def logger
|
41
|
+
if RequestMigrations.config.logger.respond_to?(:tagged)
|
42
|
+
RequestMigrations.logger.tagged(request&.request_id)
|
43
|
+
else
|
44
|
+
RequestMigrations.logger
|
45
|
+
end
|
46
|
+
end
|
39
47
|
end
|
40
48
|
|
49
|
+
##
|
50
|
+
# Migrations is controller middleware that automatically applies migrations.
|
51
|
+
#
|
52
|
+
# @example
|
53
|
+
# class ApplicationController < ActionController::API
|
54
|
+
# include RequestMigrations::Controller::Migrations
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# @raise [RequestMigrations::UnsupportedVersionError]
|
41
58
|
module Migrations
|
42
59
|
extend ActiveSupport::Concern
|
43
60
|
|
@@ -60,7 +77,7 @@ module RequestMigrations
|
|
60
77
|
extend ActiveSupport::Concern
|
61
78
|
|
62
79
|
included do
|
63
|
-
# TODO(ezekg) Implement controller-level version constraints
|
80
|
+
# TODO(ezekg) Implement controller-level version constraints.
|
64
81
|
end
|
65
82
|
end
|
66
83
|
end
|
@@ -1,7 +1,32 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module RequestMigrations
|
4
|
+
##
|
5
|
+
# Migration represents a migration for a specific version.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# class CombineNamesForUserMigration < RequestMigrations::Migration
|
9
|
+
# description %(transforms a user's first and last name to a combined name attribute)
|
10
|
+
#
|
11
|
+
# migrate if: -> data { data in type: 'user' } do |data|
|
12
|
+
# first_name = data.delete(:first_name)
|
13
|
+
# last_name = data.delete(:last_name)
|
14
|
+
#
|
15
|
+
# data[:name] = "#{first_name} #{last_name}"
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# response if: -> res { res.successful? && res.request.params in controller: 'api/v1/users',
|
19
|
+
# action: 'show' } do |res|
|
20
|
+
# data = JSON.parse(res.body, symbolize_names: true)
|
21
|
+
#
|
22
|
+
# migrate!(data)
|
23
|
+
#
|
24
|
+
# res.body = JSON.generate(data)
|
25
|
+
# end
|
26
|
+
# end
|
4
27
|
class Migration
|
28
|
+
##
|
29
|
+
# @private
|
5
30
|
class ConditionalBlock
|
6
31
|
def initialize(if: nil, &block)
|
7
32
|
@if = binding.local_variable_get(:if)
|
@@ -39,18 +64,40 @@ module RequestMigrations
|
|
39
64
|
klass.response_blocks = response_blocks.dup
|
40
65
|
end
|
41
66
|
|
67
|
+
##
|
68
|
+
# description sets the description.
|
69
|
+
#
|
70
|
+
# @param desc [String] the description
|
42
71
|
def description(desc)
|
43
72
|
self.description_value = desc
|
44
73
|
end
|
45
74
|
|
75
|
+
##
|
76
|
+
# request sets the request migration.
|
77
|
+
#
|
78
|
+
# @param if [Proc] the proc which determines if the migration should run.
|
79
|
+
#
|
80
|
+
# @yield [ActionDispatch::Request] the current request.
|
46
81
|
def request(if: nil, &block)
|
47
82
|
self.request_blocks << ConditionalBlock.new(if:, &block)
|
48
83
|
end
|
49
84
|
|
85
|
+
##
|
86
|
+
# migrate sets the data migration.
|
87
|
+
#
|
88
|
+
# @param if [Proc] the proc which determines if the migration should run.
|
89
|
+
#
|
90
|
+
# @yield [Any] the provided data.
|
50
91
|
def migrate(if: nil, &block)
|
51
92
|
self.migration_blocks << ConditionalBlock.new(if:, &block)
|
52
93
|
end
|
53
94
|
|
95
|
+
##
|
96
|
+
# response sets the response migration.
|
97
|
+
#
|
98
|
+
# @param if [Proc] the proc which determines if the migration should run.
|
99
|
+
#
|
100
|
+
# @yield [ActionDispatch::Response] the current response.
|
54
101
|
def response(if: nil, &block)
|
55
102
|
self.response_blocks << ConditionalBlock.new(if:, &block)
|
56
103
|
end
|
@@ -58,18 +105,24 @@ module RequestMigrations
|
|
58
105
|
|
59
106
|
extend DSL
|
60
107
|
|
108
|
+
##
|
109
|
+
# @private
|
61
110
|
def migrate_request!(request)
|
62
111
|
self.class.request_blocks.each { |block|
|
63
112
|
instance_exec(request) { block.call(self, _1) }
|
64
113
|
}
|
65
114
|
end
|
66
115
|
|
116
|
+
##
|
117
|
+
# @private
|
67
118
|
def migrate!(data)
|
68
119
|
self.class.migration_blocks.each { |block|
|
69
120
|
instance_exec(data) { block.call(self, _1) }
|
70
121
|
}
|
71
122
|
end
|
72
123
|
|
124
|
+
##
|
125
|
+
# @private
|
73
126
|
def migrate_response!(response)
|
74
127
|
self.class.response_blocks.each { |block|
|
75
128
|
instance_exec(response) { block.call(self, _1) }
|
@@ -2,11 +2,22 @@
|
|
2
2
|
|
3
3
|
module RequestMigrations
|
4
4
|
class Migrator
|
5
|
+
##
|
6
|
+
# Migrator represents a versioned migration from one version to another.
|
7
|
+
#
|
8
|
+
# @param from [String, Integer, Float] the current version.
|
9
|
+
# @param to [String, Integer, Float] the target version.
|
5
10
|
def initialize(from:, to:)
|
6
11
|
@current_version = Version.new(from)
|
7
12
|
@target_version = Version.new(to)
|
8
13
|
end
|
9
14
|
|
15
|
+
##
|
16
|
+
# migrate! attempts to apply all matching migrations on data.
|
17
|
+
#
|
18
|
+
# @param data [Any] the data to be migrated.
|
19
|
+
#
|
20
|
+
# @return [void]
|
10
21
|
def migrate!(data:)
|
11
22
|
logger.debug { "Migrating from #{current_version} to #{target_version} (#{migrations.size} potential migrations)" }
|
12
23
|
|
@@ -26,11 +37,12 @@ module RequestMigrations
|
|
26
37
|
|
27
38
|
def logger = RequestMigrations.logger
|
28
39
|
|
29
|
-
# TODO(ezekg) These should be sorted
|
30
40
|
def migrations
|
31
41
|
@migrations ||=
|
32
42
|
RequestMigrations.config.versions
|
33
43
|
.filter { |(version, _)| Version.new(version).between?(target_version, current_version) }
|
44
|
+
.sort
|
45
|
+
.reverse
|
34
46
|
.flat_map { |(_, migrations)| migrations }
|
35
47
|
.map { |migration|
|
36
48
|
case migration
|
@@ -3,6 +3,8 @@
|
|
3
3
|
module RequestMigrations
|
4
4
|
module Router
|
5
5
|
module Constraints
|
6
|
+
##
|
7
|
+
# @private
|
6
8
|
class VersionConstraint
|
7
9
|
def initialize(constraint:)
|
8
10
|
@constraint = Version::Constraint.new(constraint)
|
@@ -19,6 +21,20 @@ module RequestMigrations
|
|
19
21
|
def resolver = RequestMigrations.config.request_version_resolver
|
20
22
|
end
|
21
23
|
|
24
|
+
##
|
25
|
+
# version_constraint is a router constraint that resolves routes for
|
26
|
+
# specific versions.
|
27
|
+
#
|
28
|
+
# @param constraint [String] the version constraint.
|
29
|
+
#
|
30
|
+
# @yield the block when the constraint is satisfied.
|
31
|
+
#
|
32
|
+
# @example
|
33
|
+
# Rails.application.routes.draw do
|
34
|
+
# version_constraint '> 1.0' do
|
35
|
+
# resources :some_new_resource
|
36
|
+
# end
|
37
|
+
# end
|
22
38
|
def version_constraint(constraint, &)
|
23
39
|
constraints VersionConstraint.new(constraint:) do
|
24
40
|
instance_eval(&)
|
data/lib/request_migrations.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require "active_support/concern"
|
4
4
|
require "semverse"
|
5
|
+
require "logger"
|
5
6
|
require "request_migrations/gem"
|
6
7
|
require "request_migrations/configuration"
|
7
8
|
require "request_migrations/version"
|
@@ -12,20 +13,60 @@ require "request_migrations/router"
|
|
12
13
|
require "request_migrations/railtie"
|
13
14
|
|
14
15
|
module RequestMigrations
|
16
|
+
##
|
17
|
+
# UnsupportedMigrationError is raised when an invalid migration is provided.
|
15
18
|
class UnsupportedMigrationError < StandardError; end
|
19
|
+
|
20
|
+
##
|
21
|
+
# InvalidVersionFormatError is raised when an badly formatted version is provided.
|
16
22
|
class InvalidVersionFormatError < StandardError; end
|
23
|
+
|
24
|
+
##
|
25
|
+
# UnsupportedVersionError is raised when an unsupported version is requested.
|
17
26
|
class UnsupportedVersionError < StandardError; end
|
27
|
+
|
28
|
+
##
|
29
|
+
# InvalidVersionError is raised when an invalid version is provided.
|
18
30
|
class InvalidVersionError < StandardError; end
|
19
31
|
|
20
|
-
|
32
|
+
##
|
33
|
+
# SUPPORTED_VERSION_FORMATS is a list of supported version formats.
|
34
|
+
SUPPORTED_VERSION_FORMATS = %i[
|
35
|
+
semver
|
36
|
+
date
|
37
|
+
float
|
38
|
+
integer
|
39
|
+
string
|
40
|
+
].freeze
|
21
41
|
|
22
|
-
|
42
|
+
##
|
43
|
+
# config returns the current config.
|
23
44
|
def self.config = @config ||= Configuration.new
|
24
45
|
|
46
|
+
##
|
47
|
+
# logger returns the configured logger.
|
48
|
+
#
|
49
|
+
# @return [Logger]
|
50
|
+
def self.logger
|
51
|
+
@logger ||= if RequestMigrations.config.logger.respond_to?(:tagged)
|
52
|
+
RequestMigrations.config.logger.tagged(:request_migrations)
|
53
|
+
else
|
54
|
+
RequestMigrations.config.logger
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
##
|
59
|
+
# configure yields the config.
|
60
|
+
#
|
61
|
+
# @yield [config]
|
25
62
|
def self.configure
|
26
63
|
yield config
|
27
64
|
end
|
28
65
|
|
66
|
+
##
|
67
|
+
# supported_versions returns an array of supported versions.
|
68
|
+
#
|
69
|
+
# @return [Array<String, Integer, Float>]
|
29
70
|
def self.supported_versions
|
30
71
|
[RequestMigrations.config.current_version, *RequestMigrations.config.versions.keys].uniq.freeze
|
31
72
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: request_migrations
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Zeke Gabrielse
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-07-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|