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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 03674ecf6a2120accd4fcbbbe4add7f73a94098e08882a65630085b3dcc38ecd
4
- data.tar.gz: 38b32703419f8857737fc0b8fa61d5ada954f69c201247892441291b901ed0fb
3
+ metadata.gz: c2224934d0be51149ebf90a756ae9cfd9172ee83310609c9cfc94c49c4c75166
4
+ data.tar.gz: 3de2f808de4c02d7de89ec9baae0b5d68cadab94d8118e13688a941de791d3cf
5
5
  SHA512:
6
- metadata.gz: ed9e6c8e9cf08938177b687496f83e58e7ecbf0a35cda2f9f84428f83acff381c882d33f53e2ba3bc51b0d533f577f7a93bc1c9fc2533a57d79bd479a80a550f
7
- data.tar.gz: 36cc532f949768e70bf1582a9c652db79e5900811ebe0a4e08bd9a4840442b346c6c93cedd0d6d4c6d2d39bf6f487c321dd4d731c8f8533fa7d64ff65954dd85
6
+ metadata.gz: 944f3e23fe55b3d9eb56cdf0491871bfecea72a5c9a57b15f8827152b7cf5a21ab0e0ac6c1f254a9afabe4fc980562b864e96a2868ec864a314d41876e1af987
7
+ data.tar.gz: e3fa22992ac3aadcd34ce860310f6a496f4aa7f54179090863bcf993582e2c757fa07677862973b817edc2d209b289284e19ec040249c62011ad20ceb41ee0c6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.1
4
+
5
+ - Fix application order of `request` migrations.
6
+
3
7
  ## 1.0.0
4
8
 
5
9
  - Initial release.
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. This is being
7
- used in production by [Keygen](https://keygen.sh) to serve millions of API requests per day.
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://camo.githubusercontent.com/d50a6bd1f31fd4da523b8aa555a54356cc2d3e81eb8bc9123303787e44c5bb07/68747470733a2f2f6b657967656e2e73682f696d616765732f62616467652e706e67)](https://keygen.sh)
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 one-off migrations.
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 current version. Here, you can perform
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
- ### One-off migrations
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 one-off migrations (as well as offering
219
- a nice interface for pattern matching inputs).
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 one-off migration
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
- Now, we've successfully applied a migration to both our API responses, as well
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 one-off migrations, this allows for easier testing.
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. You can also utilize routing constraints
287
- to remove an API endpoint entirely.
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 current version. Here, you can perform
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, and `2.0`. |
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 one-offs allows for easier testing of migrations. For example, using Rspec:
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 writing request migrations. We'll share tips here.
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. You should remove docs for old
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 migration. Try to utilize the current data you have
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<String, Integer, Float>>] past versions.
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,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RequestMigrations
4
- VERSION = "1.0.0"
4
+ VERSION = "1.1.0"
5
5
  end
@@ -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 [request] the block containing the migration.
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 [data] the block containing the migration.
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 [response] the block containing the migration.
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 [*] the data to be migrated.
18
+ # @param data [Any] the data to be migrated.
19
19
  #
20
- # @return [*] the migrated data.
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
@@ -1,4 +1,6 @@
1
1
  module RequestMigrations
2
+ ##
3
+ # @private
2
4
  class Railtie < ::Rails::Railtie
3
5
  ActionDispatch::Routing::Mapper.send(:include, Router::Constraints)
4
6
  end
@@ -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
@@ -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.0.0
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-06-23 00:00:00.000000000 Z
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: