request_migrations 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +91 -32
- data/lib/request_migrations/configuration.rb +3 -3
- data/lib/request_migrations/controller.rb +1 -1
- data/lib/request_migrations/gem.rb +1 -1
- data/lib/request_migrations/migration.rb +26 -3
- data/lib/request_migrations/migrator.rb +4 -3
- data/lib/request_migrations/railtie.rb +2 -0
- data/lib/request_migrations/testing.rb +24 -0
- data/lib/request_migrations.rb +12 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c2224934d0be51149ebf90a756ae9cfd9172ee83310609c9cfc94c49c4c75166
|
4
|
+
data.tar.gz: 3de2f808de4c02d7de89ec9baae0b5d68cadab94d8118e13688a941de791d3cf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 944f3e23fe55b3d9eb56cdf0491871bfecea72a5c9a57b15f8827152b7cf5a21ab0e0ac6c1f254a9afabe4fc980562b864e96a2868ec864a314d41876e1af987
|
7
|
+
data.tar.gz: e3fa22992ac3aadcd34ce860310f6a496f4aa7f54179090863bcf993582e2c757fa07677862973b817edc2d209b289284e19ec040249c62011ad20ceb41ee0c6
|
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,7 +52,7 @@ _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.
|
52
57
|
- It's fast.
|
53
58
|
|
@@ -127,7 +132,7 @@ Next, we'll need to configure `request_migrations` via an initializer under
|
|
127
132
|
|
128
133
|
```ruby
|
129
134
|
RequestMigrations.configure do |config|
|
130
|
-
# Define a resolver to determine the
|
135
|
+
# Define a resolver to determine the target version. Here, you can perform
|
131
136
|
# a lookup on the current user using request parameters, or simply use
|
132
137
|
# a header like we are here, defaulting to the latest version.
|
133
138
|
config.request_version_resolver = -> request {
|
@@ -186,7 +191,12 @@ end
|
|
186
191
|
|
187
192
|
The `response` method accepts an `:if` keyword, which should be a lambda
|
188
193
|
that evaluates to a boolean, which determines whether or not the migration
|
189
|
-
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.
|
190
200
|
|
191
201
|
### Request migrations
|
192
202
|
|
@@ -206,19 +216,25 @@ end
|
|
206
216
|
|
207
217
|
The `request` method accepts an `:if` keyword, which should be a lambda
|
208
218
|
that evaluates to a boolean, which determines whether or not the migration
|
209
|
-
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.
|
210
225
|
|
211
|
-
|
226
|
+
Request migrations should [avoid using the `migrate` method](#avoid-migrate-for-request-migrations).
|
227
|
+
|
228
|
+
### Data migrations
|
212
229
|
|
213
230
|
In our first scenario, where we combined our user's name attributes, we defined
|
214
231
|
our migration using the `migrate` class method. At this point, you may be wondering
|
215
232
|
why we did that, since we didn't use that method for the 2 previous request and
|
216
233
|
response migrations above.
|
217
234
|
|
218
|
-
Well, it comes down to support for
|
219
|
-
|
220
|
-
|
221
|
-
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`.
|
222
238
|
|
223
239
|
```ruby
|
224
240
|
class CombineNamesForUserMigration < RequestMigrations::Migration
|
@@ -248,7 +264,7 @@ end
|
|
248
264
|
```
|
249
265
|
|
250
266
|
What if we had [a webhook system](https://keygen.sh/blog/how-to-build-a-webhook-system-in-rails-using-sidekiq/)
|
251
|
-
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
|
252
268
|
here, via the `Migrator` class:
|
253
269
|
|
254
270
|
```ruby
|
@@ -274,17 +290,19 @@ class WebhookWorker
|
|
274
290
|
end
|
275
291
|
```
|
276
292
|
|
277
|
-
|
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
|
278
295
|
as to the webhook events we send. In this case, if our `event` matches the
|
279
296
|
our user shape, e.g. `type: 'user'`, then the migration will be applied.
|
280
297
|
|
281
|
-
In addition to
|
298
|
+
In addition to data migrations, this allows for easier testing.
|
282
299
|
|
283
300
|
### Routing constraints
|
284
301
|
|
285
302
|
When you want to encourage API clients to upgrade, you can utilize a routing `version_constraint`
|
286
|
-
to define routes only available for certain versions.
|
287
|
-
|
303
|
+
to define routes only available for certain versions.
|
304
|
+
|
305
|
+
You can also utilize routing constraints to remove an API endpoint entirely.
|
288
306
|
|
289
307
|
```ruby
|
290
308
|
Rails.application.routes.draw do
|
@@ -302,13 +320,13 @@ Rails.application.routes.draw do
|
|
302
320
|
end
|
303
321
|
```
|
304
322
|
|
305
|
-
Currently, routing constraints only work for the `:semver` version format.
|
323
|
+
Currently, routing constraints only work for the `:semver` version format. (PRs welcome!)
|
306
324
|
|
307
325
|
### Configuration
|
308
326
|
|
309
327
|
```ruby
|
310
328
|
RequestMigrations.configure do |config|
|
311
|
-
# Define a resolver to determine the
|
329
|
+
# Define a resolver to determine the target version. Here, you can perform
|
312
330
|
# a lookup on the current user using request parameters, or simply use
|
313
331
|
# a header like we are here, defaulting to the latest version.
|
314
332
|
config.request_version_resolver = -> request {
|
@@ -345,17 +363,19 @@ end
|
|
345
363
|
By default, `request_migrations` uses a `:semver` version format, but it can be configured
|
346
364
|
to instead use one of the following, set via `config.version_format=`.
|
347
365
|
|
348
|
-
| Format |
|
349
|
-
|
350
|
-
| `:semver` | Use semantic versions, e.g. `1.0`, `1.1
|
351
|
-
| `:date` | Use date versions, e.g. `2020-09-02`, `2021-01-01`.
|
352
|
-
| `:integer` | Use integer versions, e.g. `1`, `2`, and `3`.
|
353
|
-
| `:float` | Use float versions, e.g. `1.0`, `1.1`, and `2.0`.
|
354
|
-
| `: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.
|
355
375
|
|
356
376
|
## Testing
|
357
377
|
|
358
|
-
Using
|
378
|
+
Using data migrations allows for easier testing of migrations. For example, using Rspec:
|
359
379
|
|
360
380
|
```ruby
|
361
381
|
describe CombineNamesForUserMigration do
|
@@ -385,9 +405,24 @@ describe CombineNamesForUserMigration do
|
|
385
405
|
end
|
386
406
|
```
|
387
407
|
|
408
|
+
To avoid polluting the global configuration, you can use `RequestMigrations::Testing`
|
409
|
+
within your application's `spec/rails_helper.rb` (or similar).
|
410
|
+
|
411
|
+
```ruby
|
412
|
+
Rspec.configure do |config|
|
413
|
+
config.before :each do
|
414
|
+
RequestMigrations::Testing.setup!
|
415
|
+
end
|
416
|
+
|
417
|
+
config.after :each do
|
418
|
+
RequestMigrations::Testing.teardown!
|
419
|
+
end
|
420
|
+
end
|
421
|
+
```
|
422
|
+
|
388
423
|
## Tips and tricks
|
389
424
|
|
390
|
-
Over the years, we're learned a thing or two about
|
425
|
+
Over the years, we're learned a thing or two about versioning an API. We'll share tips here.
|
391
426
|
|
392
427
|
### Use pattern matching
|
393
428
|
|
@@ -539,12 +574,36 @@ class V1x0::UsersController
|
|
539
574
|
end
|
540
575
|
```
|
541
576
|
|
577
|
+
### Avoid migrate for request migrations
|
578
|
+
|
579
|
+
Avoid using `migrate` for request migrations. If you do, then data migrations, e.g. for
|
580
|
+
webhooks, will attempt to apply the request migrations. This may erroneously produce bad
|
581
|
+
output, or even undo a response migration. Instead, keep all request migration logic,
|
582
|
+
e.g. transforming params, inside of the `request` block.
|
583
|
+
|
584
|
+
```ruby
|
585
|
+
class SomeMigration < RequestMigrations::Migration
|
586
|
+
# Bad (side-effects for data migrations)
|
587
|
+
migrate do |params|
|
588
|
+
params[:foo] = params.delete(:bar)
|
589
|
+
end
|
590
|
+
|
591
|
+
request do |req|
|
592
|
+
migrate!(req.params)
|
593
|
+
end
|
594
|
+
|
595
|
+
# Good
|
596
|
+
request do |req|
|
597
|
+
req.params[:foo] = req.params.delete(:bar)
|
598
|
+
end
|
599
|
+
end
|
600
|
+
```
|
542
601
|
|
543
602
|
### Avoid routing contraints
|
544
603
|
|
545
604
|
Avoid using routing version constraints that remove functionality. They can be a headache
|
546
|
-
during upgrades. Consider only making _additive_ changes.
|
547
|
-
or deprecated endpoints to limit any new usage.
|
605
|
+
during upgrades. Consider only making _additive_ changes. Instead, consider removing or
|
606
|
+
hiding the documenation for old or deprecated endpoints, to limit any new usage.
|
548
607
|
|
549
608
|
```ruby
|
550
609
|
Rails.application.routes.draw do
|
@@ -564,7 +623,7 @@ end
|
|
564
623
|
|
565
624
|
### Avoid n+1s
|
566
625
|
|
567
|
-
Avoid introducing n+1 queries in your
|
626
|
+
Avoid introducing n+1 queries in your migrations. Try to utilize the current data you have
|
568
627
|
to perform more meaningful queries, returning only the data needed for the migration.
|
569
628
|
|
570
629
|
```ruby
|
@@ -615,7 +674,7 @@ end
|
|
615
674
|
```
|
616
675
|
|
617
676
|
Instead of potentially tens or hundreds of queries, we make a single purposeful query
|
618
|
-
to get the data we need to complete the migration.
|
677
|
+
to get the data we need in order to complete the migration.
|
619
678
|
|
620
679
|
---
|
621
680
|
|
@@ -26,13 +26,13 @@ module RequestMigrations
|
|
26
26
|
##
|
27
27
|
# current_version defines the latest version.
|
28
28
|
#
|
29
|
-
# @return [String, Integer, Float] the current version.
|
29
|
+
# @return [String, Integer, Float, nil] the current version.
|
30
30
|
config_accessor(:current_version) { nil }
|
31
31
|
|
32
32
|
##
|
33
33
|
# versions defines past versions and their migrations.
|
34
34
|
#
|
35
|
-
# @return [Hash<String, Array<
|
36
|
-
config_accessor(:versions) {
|
35
|
+
# @return [Hash<String, Array<Symbol, String, Class>>] past versions.
|
36
|
+
config_accessor(:versions) { {} }
|
37
37
|
end
|
38
38
|
end
|
@@ -15,7 +15,7 @@ module RequestMigrations
|
|
15
15
|
def migrate!
|
16
16
|
logger.debug { "Migrating from #{current_version} to #{target_version} (#{migrations.size} potential migrations)" }
|
17
17
|
|
18
|
-
migrations.each_with_index { |migration, i|
|
18
|
+
migrations.reverse.each_with_index { |migration, i|
|
19
19
|
logger.debug { "Applying migration #{migration} (#{i + 1}/#{migrations.size})" }
|
20
20
|
|
21
21
|
migration.new.migrate_request!(request)
|
@@ -1,6 +1,29 @@
|
|
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
|
5
28
|
##
|
6
29
|
# @private
|
@@ -54,7 +77,7 @@ module RequestMigrations
|
|
54
77
|
#
|
55
78
|
# @param if [Proc] the proc which determines if the migration should run.
|
56
79
|
#
|
57
|
-
# @yield [
|
80
|
+
# @yield [ActionDispatch::Request] the current request.
|
58
81
|
def request(if: nil, &block)
|
59
82
|
self.request_blocks << ConditionalBlock.new(if:, &block)
|
60
83
|
end
|
@@ -64,7 +87,7 @@ module RequestMigrations
|
|
64
87
|
#
|
65
88
|
# @param if [Proc] the proc which determines if the migration should run.
|
66
89
|
#
|
67
|
-
# @yield [
|
90
|
+
# @yield [Any] the provided data.
|
68
91
|
def migrate(if: nil, &block)
|
69
92
|
self.migration_blocks << ConditionalBlock.new(if:, &block)
|
70
93
|
end
|
@@ -74,7 +97,7 @@ module RequestMigrations
|
|
74
97
|
#
|
75
98
|
# @param if [Proc] the proc which determines if the migration should run.
|
76
99
|
#
|
77
|
-
# @yield [
|
100
|
+
# @yield [ActionDispatch::Response] the current response.
|
78
101
|
def response(if: nil, &block)
|
79
102
|
self.response_blocks << ConditionalBlock.new(if:, &block)
|
80
103
|
end
|
@@ -15,9 +15,9 @@ module RequestMigrations
|
|
15
15
|
##
|
16
16
|
# migrate! attempts to apply all matching migrations on data.
|
17
17
|
#
|
18
|
-
# @param data [
|
18
|
+
# @param data [Any] the data to be migrated.
|
19
19
|
#
|
20
|
-
# @return [
|
20
|
+
# @return [void]
|
21
21
|
def migrate!(data:)
|
22
22
|
logger.debug { "Migrating from #{current_version} to #{target_version} (#{migrations.size} potential migrations)" }
|
23
23
|
|
@@ -37,11 +37,12 @@ module RequestMigrations
|
|
37
37
|
|
38
38
|
def logger = RequestMigrations.logger
|
39
39
|
|
40
|
-
# TODO(ezekg) These should be sorted.
|
41
40
|
def migrations
|
42
41
|
@migrations ||=
|
43
42
|
RequestMigrations.config.versions
|
44
43
|
.filter { |(version, _)| Version.new(version).between?(target_version, current_version) }
|
44
|
+
.sort
|
45
|
+
.reverse
|
45
46
|
.flat_map { |(_, migrations)| migrations }
|
46
47
|
.map { |migration|
|
47
48
|
case migration
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RequestMigrations
|
4
|
+
class Testing
|
5
|
+
@@config = RequestMigrations::Configuration.new
|
6
|
+
|
7
|
+
##
|
8
|
+
# setup! stores the original config and replaces it with a clone for testing.
|
9
|
+
def self.setup!
|
10
|
+
@@config = RequestMigrations.config
|
11
|
+
|
12
|
+
RequestMigrations.reset!
|
13
|
+
RequestMigrations.configure do |config|
|
14
|
+
@@config.config.each { |(k, v)| config.config[k] = v }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
##
|
19
|
+
# teardown! restores the original config.
|
20
|
+
def self.teardown!
|
21
|
+
RequestMigrations.config = @@config
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/request_migrations.rb
CHANGED
@@ -43,6 +43,18 @@ module RequestMigrations
|
|
43
43
|
# config returns the current config.
|
44
44
|
def self.config = @config ||= Configuration.new
|
45
45
|
|
46
|
+
##
|
47
|
+
# @private
|
48
|
+
def self.config=(cfg)
|
49
|
+
@config = cfg
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# @private
|
54
|
+
def self.reset!
|
55
|
+
@config = RequestMigrations::Configuration.new
|
56
|
+
end
|
57
|
+
|
46
58
|
##
|
47
59
|
# logger returns the configured logger.
|
48
60
|
#
|
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: 1.
|
4
|
+
version: 1.1.0
|
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
|
@@ -74,6 +74,7 @@ files:
|
|
74
74
|
- lib/request_migrations/migrator.rb
|
75
75
|
- lib/request_migrations/railtie.rb
|
76
76
|
- lib/request_migrations/router.rb
|
77
|
+
- lib/request_migrations/testing.rb
|
77
78
|
- lib/request_migrations/version.rb
|
78
79
|
homepage: https://github.com/keygen-sh/request_migrations
|
79
80
|
licenses:
|