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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a07f06f1ea7b2b51851ff1490e1233363bc4d13bc6ac8918e7c1d886c9497ffd
4
- data.tar.gz: 79f9bb8c317e906b4d4d6992811156103265d902f82ca0fcc1ac65a6e85327de
3
+ metadata.gz: 1575eecb9ad23c9dae9534b03a31acbcef8c6ed7bf46f4ffca1748ef21fcead4
4
+ data.tar.gz: 141dbbf4576e8b2da641ab32bd377173bbe61717d330c741c0b10c5cf9eff854
5
5
  SHA512:
6
- metadata.gz: 595d53c2b5d25dd34d4212e9db9208b96dccf684ec03e4cd75fa485d5421c400cc0b465e4914d7613a3ef15c08757b93ce163cebf1cb886dccf14e7986ca16bb
7
- data.tar.gz: 95cd329eee002333f976886c2394a538dc1f3e6231f093a4d7155b9330eaf0725dc9ed38e31c6ccc0d9e7fdeb6a32b7f54970dc6114064b6286b96888b0be4d2
6
+ metadata.gz: 119241518c4d8a9a954bd4918a6e2edbc0338938c7173953a406821f34f0f4da735e7819b7cc0e929be45c78a7da88f035f1456d5b98d53d6cb00b0fca1c8362
7
+ data.tar.gz: 5afffe316b08692daeaf2366fab161f23794b80326112a2b4c4ece1a13f0d40f8d61423aa81d4bf28c62274acf8a33dd63c01214295524cd7691c42203a71575
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.1
4
+
5
+ - Fix application order of `request` migrations.
6
+
7
+ ## 1.0.0
8
+
9
+ - Initial release.
10
+
3
11
  ## 0.1.0
4
12
 
5
13
  - Test 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,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 one-off migrations.
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 current version. Here, you can perform
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[combine_user_names_migration],
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
- ### One-off migrations
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 one-off migrations (as well as offering
210
- a nice interface for pattern matching inputs).
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 one-off migration
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
- 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
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. You can also utilize routing constraints
276
- 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.
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 current version. Here, you can perform
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. Must be a tagged logger. Defaults to
328
- # using Rails.logger.
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, and `2.0`. |
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
- ### Tips and tricks
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
- Over the years, we're learned a thing or two about writing request migrations. We'll share tips here.
397
+ expect(data).to include(type: 'user', first_name: 'John', last_name: 'Doe')
398
+ expect(data).to_not include(name: anything)
349
399
 
350
- #### Use pattern matching
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
- #### Route helpers
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
- #### Separate by shape
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
- #### Always check response status
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
- #### Don't match on URL pattern
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
- #### Namespace deprecated controllers
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
- #### Avoid routing contraints
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. You should remove docs for old
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
- config_accessor(:logger) { Rails.logger }
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
- config_accessor(:version_format) { :semver }
10
- config_accessor(:current_version) { nil }
11
- config_accessor(:versions) { [] }
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 = RequestMigrations.logger.tagged(request&.request_id)
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,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RequestMigrations
4
- VERSION = "0.1.0"
4
+ VERSION = "1.0.2"
5
5
  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
@@ -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
@@ -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(&)
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RequestMigrations
4
+ ##
5
+ # @private
4
6
  class Version
5
7
  include Comparable
6
8
 
@@ -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
- SUPPORTED_VERSION_FORMATS = %i[semver date float integer string].freeze
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
- def self.logger = @logger ||= RequestMigrations.config.logger.tagged(:request_migrations)
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: 0.1.0
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-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