request_migrations 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
 - data/CHANGELOG.md +4 -0
 - data/README.md +111 -12
 - data/lib/request_migrations/configuration.rb +29 -4
 - data/lib/request_migrations/controller.rb +19 -2
 - data/lib/request_migrations/gem.rb +1 -1
 - data/lib/request_migrations/migration.rb +30 -0
 - data/lib/request_migrations/migrator.rb +12 -1
 - data/lib/request_migrations/router.rb +16 -0
 - data/lib/request_migrations/version.rb +2 -0
 - data/lib/request_migrations.rb +43 -2
 - metadata +1 -1
 
    
        checksums.yaml
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ---
         
     | 
| 
       2 
2 
     | 
    
         
             
            SHA256:
         
     | 
| 
       3 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       4 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 3 
     | 
    
         
            +
              metadata.gz: 03674ecf6a2120accd4fcbbbe4add7f73a94098e08882a65630085b3dcc38ecd
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: 38b32703419f8857737fc0b8fa61d5ada954f69c201247892441291b901ed0fb
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: ed9e6c8e9cf08938177b687496f83e58e7ecbf0a35cda2f9f84428f83acff381c882d33f53e2ba3bc51b0d533f577f7a93bc1c9fc2533a57d79bd479a80a550f
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: 36cc532f949768e70bf1582a9c652db79e5900811ebe0a4e08bd9a4840442b346c6c93cedd0d6d4c6d2d39bf6f487c321dd4d731c8f8533fa7d64ff65954dd85
         
     | 
    
        data/CHANGELOG.md
    CHANGED
    
    
    
        data/README.md
    CHANGED
    
    | 
         @@ -49,6 +49,7 @@ _We're working on improving the docs._ 
     | 
|
| 
       49 
49 
     | 
    
         
             
            - Define migrations for migrating a request between versions.
         
     | 
| 
       50 
50 
     | 
    
         
             
            - Define migrations for applying one-off migrations.
         
     | 
| 
       51 
51 
     | 
    
         
             
            - Define version-based routing constraints.
         
     | 
| 
      
 52 
     | 
    
         
            +
            - It's fast.
         
     | 
| 
       52 
53 
     | 
    
         | 
| 
       53 
54 
     | 
    
         
             
            ## Usage
         
     | 
| 
       54 
55 
     | 
    
         | 
| 
         @@ -138,7 +139,7 @@ RequestMigrations.configure do |config| 
     | 
|
| 
       138 
139 
     | 
    
         | 
| 
       139 
140 
     | 
    
         
             
              # Define previous versions and their migrations, in descending order.
         
     | 
| 
       140 
141 
     | 
    
         
             
              config.versions = {
         
     | 
| 
       141 
     | 
    
         
            -
                '1.0' => %i[ 
     | 
| 
      
 142 
     | 
    
         
            +
                '1.0' => %i[combine_names_for_user_migration],
         
     | 
| 
       142 
143 
     | 
    
         
             
              }
         
     | 
| 
       143 
144 
     | 
    
         
             
            end
         
     | 
| 
       144 
145 
     | 
    
         
             
            ```
         
     | 
| 
         @@ -149,6 +150,14 @@ are applied: 
     | 
|
| 
       149 
150 
     | 
    
         
             
            ```ruby
         
     | 
| 
       150 
151 
     | 
    
         
             
            class ApplicationController < ActionController::API
         
     | 
| 
       151 
152 
     | 
    
         
             
              include RequestMigrations::Controller::Migrations
         
     | 
| 
      
 153 
     | 
    
         
            +
             
     | 
| 
      
 154 
     | 
    
         
            +
              # Optionally rescue from requests for unsupported versions
         
     | 
| 
      
 155 
     | 
    
         
            +
              rescue_from RequestMigrations::UnsupportedVersionError, with: -> {
         
     | 
| 
      
 156 
     | 
    
         
            +
                render(
         
     | 
| 
      
 157 
     | 
    
         
            +
                  json: { error: 'unsupported API version requested', code: 'INVALID_API_VERSION' },
         
     | 
| 
      
 158 
     | 
    
         
            +
                  status: :bad_request,
         
     | 
| 
      
 159 
     | 
    
         
            +
                )
         
     | 
| 
      
 160 
     | 
    
         
            +
              }
         
     | 
| 
       152 
161 
     | 
    
         
             
            end
         
     | 
| 
       153 
162 
     | 
    
         
             
            ```
         
     | 
| 
       154 
163 
     | 
    
         | 
| 
         @@ -269,6 +278,8 @@ Now, we've successfully applied a migration to both our API responses, as well 
     | 
|
| 
       269 
278 
     | 
    
         
             
            as to the webhook events we send. In this case, if our `event` matches the
         
     | 
| 
       270 
279 
     | 
    
         
             
            our user shape, e.g. `type: 'user'`, then the migration will be applied.
         
     | 
| 
       271 
280 
     | 
    
         | 
| 
      
 281 
     | 
    
         
            +
            In addition to one-off migrations, this allows for easier testing.
         
     | 
| 
      
 282 
     | 
    
         
            +
             
     | 
| 
       272 
283 
     | 
    
         
             
            ### Routing constraints
         
     | 
| 
       273 
284 
     | 
    
         | 
| 
       274 
285 
     | 
    
         
             
            When you want to encourage API clients to upgrade, you can utilize a routing `version_constraint`
         
     | 
| 
         @@ -324,9 +335,8 @@ RequestMigrations.configure do |config| 
     | 
|
| 
       324 
335 
     | 
    
         
             
                ],
         
     | 
| 
       325 
336 
     | 
    
         
             
              }
         
     | 
| 
       326 
337 
     | 
    
         | 
| 
       327 
     | 
    
         
            -
              # Use a custom logger.  
     | 
| 
       328 
     | 
    
         
            -
               
     | 
| 
       329 
     | 
    
         
            -
              config.logger = ActiveSupport::TaggedLogging.new(...)
         
     | 
| 
      
 338 
     | 
    
         
            +
              # Use a custom logger. Supports ActiveSupport::TaggedLogging.
         
     | 
| 
      
 339 
     | 
    
         
            +
              config.logger = Rails.logger
         
     | 
| 
       330 
340 
     | 
    
         
             
            end
         
     | 
| 
       331 
341 
     | 
    
         
             
            ```
         
     | 
| 
       332 
342 
     | 
    
         | 
| 
         @@ -343,11 +353,43 @@ to instead use one of the following, set via `config.version_format=`. 
     | 
|
| 
       343 
353 
     | 
    
         
             
            | `:float`   | Use float versions, e.g. `1.0`, `1.1`, and `2.0`.   |
         
     | 
| 
       344 
354 
     | 
    
         
             
            | `:string`  | Use string versions, e.g. `a`, `b`, and `z`.        |
         
     | 
| 
       345 
355 
     | 
    
         | 
| 
       346 
     | 
    
         
            -
             
     | 
| 
      
 356 
     | 
    
         
            +
            ## Testing
         
     | 
| 
      
 357 
     | 
    
         
            +
             
     | 
| 
      
 358 
     | 
    
         
            +
            Using one-offs allows for easier testing of migrations. For example, using Rspec:
         
     | 
| 
      
 359 
     | 
    
         
            +
             
     | 
| 
      
 360 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 361 
     | 
    
         
            +
            describe CombineNamesForUserMigration do
         
     | 
| 
      
 362 
     | 
    
         
            +
              before do
         
     | 
| 
      
 363 
     | 
    
         
            +
                RequestMigrations.configure do |config|
         
     | 
| 
      
 364 
     | 
    
         
            +
                  config.current_version = '1.1'
         
     | 
| 
      
 365 
     | 
    
         
            +
                  config.versions        = {
         
     | 
| 
      
 366 
     | 
    
         
            +
                    '1.0' => [CombineNamesForUserMigration],
         
     | 
| 
      
 367 
     | 
    
         
            +
                  }
         
     | 
| 
      
 368 
     | 
    
         
            +
                end
         
     | 
| 
      
 369 
     | 
    
         
            +
              end
         
     | 
| 
      
 370 
     | 
    
         
            +
             
     | 
| 
      
 371 
     | 
    
         
            +
              it 'should migrate user name attributes' do
         
     | 
| 
      
 372 
     | 
    
         
            +
                migrator = RequestMigrations::Migrator.new(from: '1.1', to: '1.0')
         
     | 
| 
      
 373 
     | 
    
         
            +
                data     = serialize(
         
     | 
| 
      
 374 
     | 
    
         
            +
                  create(:user, first_name: 'John', last_name: 'Doe'),
         
     | 
| 
      
 375 
     | 
    
         
            +
                )
         
     | 
| 
      
 376 
     | 
    
         
            +
             
     | 
| 
      
 377 
     | 
    
         
            +
                expect(data).to include(type: 'user', first_name: 'John', last_name: 'Doe')
         
     | 
| 
      
 378 
     | 
    
         
            +
                expect(data).to_not include(name: anything)
         
     | 
| 
      
 379 
     | 
    
         
            +
             
     | 
| 
      
 380 
     | 
    
         
            +
                migrator.migrate!(data:)
         
     | 
| 
      
 381 
     | 
    
         
            +
             
     | 
| 
      
 382 
     | 
    
         
            +
                expect(data).to include(type: 'user', name: 'John Doe')
         
     | 
| 
      
 383 
     | 
    
         
            +
                expect(data).to_not include(first_name: 'John', last_name: 'Doe')
         
     | 
| 
      
 384 
     | 
    
         
            +
              end
         
     | 
| 
      
 385 
     | 
    
         
            +
            end
         
     | 
| 
      
 386 
     | 
    
         
            +
            ```
         
     | 
| 
      
 387 
     | 
    
         
            +
             
     | 
| 
      
 388 
     | 
    
         
            +
            ## Tips and tricks
         
     | 
| 
       347 
389 
     | 
    
         | 
| 
       348 
390 
     | 
    
         
             
            Over the years, we're learned a thing or two about writing request migrations. We'll share tips here.
         
     | 
| 
       349 
391 
     | 
    
         | 
| 
       350 
     | 
    
         
            -
             
     | 
| 
      
 392 
     | 
    
         
            +
            ### Use pattern matching
         
     | 
| 
       351 
393 
     | 
    
         | 
| 
       352 
394 
     | 
    
         
             
            Pattern matching really cleans up the `:if` conditions, and overall makes migrations more readable.
         
     | 
| 
       353 
395 
     | 
    
         | 
| 
         @@ -382,7 +424,7 @@ end 
     | 
|
| 
       382 
424 
     | 
    
         | 
| 
       383 
425 
     | 
    
         
             
            Just be sure to remember your `else` block when `case` pattern matching. :)
         
     | 
| 
       384 
426 
     | 
    
         | 
| 
       385 
     | 
    
         
            -
             
     | 
| 
      
 427 
     | 
    
         
            +
            ### Route helpers
         
     | 
| 
       386 
428 
     | 
    
         | 
| 
       387 
429 
     | 
    
         
             
            If you need to use route helpers in a migration, include them in your migration:
         
     | 
| 
       388 
430 
     | 
    
         | 
| 
         @@ -392,7 +434,7 @@ class SomeMigration < RequestMigrations::Migration 
     | 
|
| 
       392 
434 
     | 
    
         
             
            end
         
     | 
| 
       393 
435 
     | 
    
         
             
            ```
         
     | 
| 
       394 
436 
     | 
    
         | 
| 
       395 
     | 
    
         
            -
             
     | 
| 
      
 437 
     | 
    
         
            +
            ### Separate by shape
         
     | 
| 
       396 
438 
     | 
    
         | 
| 
       397 
439 
     | 
    
         
             
            Define separate migrations for different input shapes, e.g. define a migration for an `#index`
         
     | 
| 
       398 
440 
     | 
    
         
             
            to migrate an array of objects, and define another migration that handles the singular object
         
     | 
| 
         @@ -454,7 +496,7 @@ end 
     | 
|
| 
       454 
496 
     | 
    
         | 
| 
       455 
497 
     | 
    
         
             
            Note that the `migrate` method now migrates an array input, and matches on the `#index` route.
         
     | 
| 
       456 
498 
     | 
    
         | 
| 
       457 
     | 
    
         
            -
             
     | 
| 
      
 499 
     | 
    
         
            +
            ### Always check response status
         
     | 
| 
       458 
500 
     | 
    
         | 
| 
       459 
501 
     | 
    
         
             
            Always check a response's status. You don't want to unintentionally apply migrations to error
         
     | 
| 
       460 
502 
     | 
    
         
             
            responses.
         
     | 
| 
         @@ -467,7 +509,9 @@ class SomeMigration < RequestMigrations::Migration 
     | 
|
| 
       467 
509 
     | 
    
         
             
            end
         
     | 
| 
       468 
510 
     | 
    
         
             
            ```
         
     | 
| 
       469 
511 
     | 
    
         | 
| 
       470 
     | 
    
         
            -
             
     | 
| 
      
 512 
     | 
    
         
            +
            Also mind `204 No Content`, since the response body will be `nil`.
         
     | 
| 
      
 513 
     | 
    
         
            +
             
     | 
| 
      
 514 
     | 
    
         
            +
            ### Don't match on URL pattern
         
     | 
| 
       471 
515 
     | 
    
         | 
| 
       472 
516 
     | 
    
         
             
            Don't match on URL pattern. Instead, use `response.request.params` to access the request params
         
     | 
| 
       473 
517 
     | 
    
         
             
            in a `response` migration, and use the `:controller` and `:action` params to determine route.
         
     | 
| 
         @@ -482,7 +526,7 @@ class SomeMigration < RequestMigrations::Migration 
     | 
|
| 
       482 
526 
     | 
    
         
             
            end
         
     | 
| 
       483 
527 
     | 
    
         
             
            ```
         
     | 
| 
       484 
528 
     | 
    
         | 
| 
       485 
     | 
    
         
            -
             
     | 
| 
      
 529 
     | 
    
         
            +
            ### Namespace deprecated controllers
         
     | 
| 
       486 
530 
     | 
    
         | 
| 
       487 
531 
     | 
    
         
             
            When you need to entirely change a controller or service class, use a `V1x0::UsersController`-style
         
     | 
| 
       488 
532 
     | 
    
         
             
            namespace to keep the old deprecated classes tidy.
         
     | 
| 
         @@ -496,7 +540,7 @@ end 
     | 
|
| 
       496 
540 
     | 
    
         
             
            ```
         
     | 
| 
       497 
541 
     | 
    
         | 
| 
       498 
542 
     | 
    
         | 
| 
       499 
     | 
    
         
            -
             
     | 
| 
      
 543 
     | 
    
         
            +
            ### Avoid routing contraints
         
     | 
| 
       500 
544 
     | 
    
         | 
| 
       501 
545 
     | 
    
         
             
            Avoid using routing version constraints that remove functionality. They can be a headache
         
     | 
| 
       502 
546 
     | 
    
         
             
            during upgrades. Consider only making _additive_ changes. You should remove docs for old
         
     | 
| 
         @@ -518,6 +562,61 @@ Rails.application.routes.draw do 
     | 
|
| 
       518 
562 
     | 
    
         
             
            end
         
     | 
| 
       519 
563 
     | 
    
         
             
            ```
         
     | 
| 
       520 
564 
     | 
    
         | 
| 
      
 565 
     | 
    
         
            +
            ### Avoid n+1s
         
     | 
| 
      
 566 
     | 
    
         
            +
             
     | 
| 
      
 567 
     | 
    
         
            +
            Avoid introducing n+1 queries in your migration. Try to utilize the current data you have
         
     | 
| 
      
 568 
     | 
    
         
            +
            to perform more meaningful queries, returning only the data needed for the migration.
         
     | 
| 
      
 569 
     | 
    
         
            +
             
     | 
| 
      
 570 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 571 
     | 
    
         
            +
            class AddRecentPostToUsersMigration < RequestMigrations::Migration
         
     | 
| 
      
 572 
     | 
    
         
            +
              description %(adds :recent_post association to a collection of users)
         
     | 
| 
      
 573 
     | 
    
         
            +
             
     | 
| 
      
 574 
     | 
    
         
            +
              # Bad (n+1)
         
     | 
| 
      
 575 
     | 
    
         
            +
              migrate if: -> data { data in [*, { type: 'user' }, *] do |data|
         
     | 
| 
      
 576 
     | 
    
         
            +
                data.each do |record|
         
     | 
| 
      
 577 
     | 
    
         
            +
                  case record
         
     | 
| 
      
 578 
     | 
    
         
            +
                  in type: 'user', id:
         
     | 
| 
      
 579 
     | 
    
         
            +
                    recent_post = Post.reorder(created_at: :desc)
         
     | 
| 
      
 580 
     | 
    
         
            +
                                      .find_by(user_id: id)
         
     | 
| 
      
 581 
     | 
    
         
            +
             
     | 
| 
      
 582 
     | 
    
         
            +
                    record[:recent_post] = recent_post&.id
         
     | 
| 
      
 583 
     | 
    
         
            +
                  else
         
     | 
| 
      
 584 
     | 
    
         
            +
                  end
         
     | 
| 
      
 585 
     | 
    
         
            +
                end
         
     | 
| 
      
 586 
     | 
    
         
            +
              end
         
     | 
| 
      
 587 
     | 
    
         
            +
             
     | 
| 
      
 588 
     | 
    
         
            +
              # Good
         
     | 
| 
      
 589 
     | 
    
         
            +
              migrate if: -> data { data in [*, { type: 'user' }, *] do |data|
         
     | 
| 
      
 590 
     | 
    
         
            +
                user_ids = data.collect { _1[:id] }
         
     | 
| 
      
 591 
     | 
    
         
            +
                post_ids = Post.select(:id, :user_id)
         
     | 
| 
      
 592 
     | 
    
         
            +
                               .distinct_on(:user_id)
         
     | 
| 
      
 593 
     | 
    
         
            +
                               .where(user_id: user_ids)
         
     | 
| 
      
 594 
     | 
    
         
            +
                               .reorder(created_at: :desc)
         
     | 
| 
      
 595 
     | 
    
         
            +
                               .group_by(&:user_id)
         
     | 
| 
      
 596 
     | 
    
         
            +
             
     | 
| 
      
 597 
     | 
    
         
            +
                data.each do |record|
         
     | 
| 
      
 598 
     | 
    
         
            +
                  case record
         
     | 
| 
      
 599 
     | 
    
         
            +
                  in type: 'user', id: user_id
         
     | 
| 
      
 600 
     | 
    
         
            +
                    record[:recent_post] = post_ids[user_id]&.id
         
     | 
| 
      
 601 
     | 
    
         
            +
                  else
         
     | 
| 
      
 602 
     | 
    
         
            +
                  end
         
     | 
| 
      
 603 
     | 
    
         
            +
                end
         
     | 
| 
      
 604 
     | 
    
         
            +
              end
         
     | 
| 
      
 605 
     | 
    
         
            +
             
     | 
| 
      
 606 
     | 
    
         
            +
              response if: -> res { res.successful? && res.request.params in controller: 'api/v1/users',
         
     | 
| 
      
 607 
     | 
    
         
            +
                                                                             action: 'index' } do |res|
         
     | 
| 
      
 608 
     | 
    
         
            +
                data = JSON.parse(res.body, symbolize_names: true)
         
     | 
| 
      
 609 
     | 
    
         
            +
             
     | 
| 
      
 610 
     | 
    
         
            +
                migrate!(data)
         
     | 
| 
      
 611 
     | 
    
         
            +
             
     | 
| 
      
 612 
     | 
    
         
            +
                res.body = JSON.generate(data)
         
     | 
| 
      
 613 
     | 
    
         
            +
              end
         
     | 
| 
      
 614 
     | 
    
         
            +
            end
         
     | 
| 
      
 615 
     | 
    
         
            +
            ```
         
     | 
| 
      
 616 
     | 
    
         
            +
             
     | 
| 
      
 617 
     | 
    
         
            +
            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.
         
     | 
| 
      
 619 
     | 
    
         
            +
             
     | 
| 
       521 
620 
     | 
    
         
             
            ---
         
     | 
| 
       522 
621 
     | 
    
         | 
| 
       523 
622 
     | 
    
         
             
            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] 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<String, Integer, Float>>] 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)
         
     | 
| 
         @@ -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
         
     | 
| 
         @@ -2,6 +2,8 @@ 
     | 
|
| 
       2 
2 
     | 
    
         | 
| 
       3 
3 
     | 
    
         
             
            module RequestMigrations
         
     | 
| 
       4 
4 
     | 
    
         
             
              class Migration
         
     | 
| 
      
 5 
     | 
    
         
            +
                ##
         
     | 
| 
      
 6 
     | 
    
         
            +
                # @private
         
     | 
| 
       5 
7 
     | 
    
         
             
                class ConditionalBlock
         
     | 
| 
       6 
8 
     | 
    
         
             
                  def initialize(if: nil, &block)
         
     | 
| 
       7 
9 
     | 
    
         
             
                    @if    = binding.local_variable_get(:if)
         
     | 
| 
         @@ -39,18 +41,40 @@ module RequestMigrations 
     | 
|
| 
       39 
41 
     | 
    
         
             
                    klass.response_blocks   = response_blocks.dup
         
     | 
| 
       40 
42 
     | 
    
         
             
                  end
         
     | 
| 
       41 
43 
     | 
    
         | 
| 
      
 44 
     | 
    
         
            +
                  ##
         
     | 
| 
      
 45 
     | 
    
         
            +
                  # description sets the description.
         
     | 
| 
      
 46 
     | 
    
         
            +
                  #
         
     | 
| 
      
 47 
     | 
    
         
            +
                  # @param desc [String] the description
         
     | 
| 
       42 
48 
     | 
    
         
             
                  def description(desc)
         
     | 
| 
       43 
49 
     | 
    
         
             
                    self.description_value = desc
         
     | 
| 
       44 
50 
     | 
    
         
             
                  end
         
     | 
| 
       45 
51 
     | 
    
         | 
| 
      
 52 
     | 
    
         
            +
                  ##
         
     | 
| 
      
 53 
     | 
    
         
            +
                  # request sets the request migration.
         
     | 
| 
      
 54 
     | 
    
         
            +
                  #
         
     | 
| 
      
 55 
     | 
    
         
            +
                  # @param if [Proc] the proc which determines if the migration should run.
         
     | 
| 
      
 56 
     | 
    
         
            +
                  #
         
     | 
| 
      
 57 
     | 
    
         
            +
                  # @yield [request] the block containing the migration.
         
     | 
| 
       46 
58 
     | 
    
         
             
                  def request(if: nil, &block)
         
     | 
| 
       47 
59 
     | 
    
         
             
                    self.request_blocks << ConditionalBlock.new(if:, &block)
         
     | 
| 
       48 
60 
     | 
    
         
             
                  end
         
     | 
| 
       49 
61 
     | 
    
         | 
| 
      
 62 
     | 
    
         
            +
                  ##
         
     | 
| 
      
 63 
     | 
    
         
            +
                  # migrate sets the data migration.
         
     | 
| 
      
 64 
     | 
    
         
            +
                  #
         
     | 
| 
      
 65 
     | 
    
         
            +
                  # @param if [Proc] the proc which determines if the migration should run.
         
     | 
| 
      
 66 
     | 
    
         
            +
                  #
         
     | 
| 
      
 67 
     | 
    
         
            +
                  # @yield [data] the block containing the migration.
         
     | 
| 
       50 
68 
     | 
    
         
             
                  def migrate(if: nil, &block)
         
     | 
| 
       51 
69 
     | 
    
         
             
                    self.migration_blocks << ConditionalBlock.new(if:, &block)
         
     | 
| 
       52 
70 
     | 
    
         
             
                  end
         
     | 
| 
       53 
71 
     | 
    
         | 
| 
      
 72 
     | 
    
         
            +
                  ##
         
     | 
| 
      
 73 
     | 
    
         
            +
                  # response sets the response migration.
         
     | 
| 
      
 74 
     | 
    
         
            +
                  #
         
     | 
| 
      
 75 
     | 
    
         
            +
                  # @param if [Proc] the proc which determines if the migration should run.
         
     | 
| 
      
 76 
     | 
    
         
            +
                  #
         
     | 
| 
      
 77 
     | 
    
         
            +
                  # @yield [response] the block containing the migration.
         
     | 
| 
       54 
78 
     | 
    
         
             
                  def response(if: nil, &block)
         
     | 
| 
       55 
79 
     | 
    
         
             
                    self.response_blocks << ConditionalBlock.new(if:, &block)
         
     | 
| 
       56 
80 
     | 
    
         
             
                  end
         
     | 
| 
         @@ -58,18 +82,24 @@ module RequestMigrations 
     | 
|
| 
       58 
82 
     | 
    
         | 
| 
       59 
83 
     | 
    
         
             
                extend DSL
         
     | 
| 
       60 
84 
     | 
    
         | 
| 
      
 85 
     | 
    
         
            +
                ##
         
     | 
| 
      
 86 
     | 
    
         
            +
                # @private
         
     | 
| 
       61 
87 
     | 
    
         
             
                def migrate_request!(request)
         
     | 
| 
       62 
88 
     | 
    
         
             
                  self.class.request_blocks.each { |block|
         
     | 
| 
       63 
89 
     | 
    
         
             
                    instance_exec(request) { block.call(self, _1) }
         
     | 
| 
       64 
90 
     | 
    
         
             
                  }
         
     | 
| 
       65 
91 
     | 
    
         
             
                end
         
     | 
| 
       66 
92 
     | 
    
         | 
| 
      
 93 
     | 
    
         
            +
                ##
         
     | 
| 
      
 94 
     | 
    
         
            +
                # @private
         
     | 
| 
       67 
95 
     | 
    
         
             
                def migrate!(data)
         
     | 
| 
       68 
96 
     | 
    
         
             
                  self.class.migration_blocks.each { |block|
         
     | 
| 
       69 
97 
     | 
    
         
             
                    instance_exec(data) { block.call(self, _1) }
         
     | 
| 
       70 
98 
     | 
    
         
             
                  }
         
     | 
| 
       71 
99 
     | 
    
         
             
                end
         
     | 
| 
       72 
100 
     | 
    
         | 
| 
      
 101 
     | 
    
         
            +
                ##
         
     | 
| 
      
 102 
     | 
    
         
            +
                # @private
         
     | 
| 
       73 
103 
     | 
    
         
             
                def migrate_response!(response)
         
     | 
| 
       74 
104 
     | 
    
         
             
                  self.class.response_blocks.each { |block|
         
     | 
| 
       75 
105 
     | 
    
         
             
                    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 [*] the data to be migrated.
         
     | 
| 
      
 19 
     | 
    
         
            +
                #
         
     | 
| 
      
 20 
     | 
    
         
            +
                # @return [*] the migrated data.
         
     | 
| 
       10 
21 
     | 
    
         
             
                def migrate!(data:)
         
     | 
| 
       11 
22 
     | 
    
         
             
                  logger.debug { "Migrating from #{current_version} to #{target_version} (#{migrations.size} potential migrations)" }
         
     | 
| 
       12 
23 
     | 
    
         | 
| 
         @@ -26,7 +37,7 @@ module RequestMigrations 
     | 
|
| 
       26 
37 
     | 
    
         | 
| 
       27 
38 
     | 
    
         
             
                def logger = RequestMigrations.logger
         
     | 
| 
       28 
39 
     | 
    
         | 
| 
       29 
     | 
    
         
            -
                # TODO(ezekg) These should be sorted
         
     | 
| 
      
 40 
     | 
    
         
            +
                # TODO(ezekg) These should be sorted.
         
     | 
| 
       30 
41 
     | 
    
         
             
                def migrations
         
     | 
| 
       31 
42 
     | 
    
         
             
                  @migrations ||=
         
     | 
| 
       32 
43 
     | 
    
         
             
                    RequestMigrations.config.versions
         
     | 
| 
         @@ -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
         
     |