taro 1.0.0 → 1.2.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 +35 -1
 - data/README.md +77 -14
 - data/lib/taro/errors.rb +7 -1
 - data/lib/taro/export/open_api_v3.rb +54 -23
 - data/lib/taro/rails/active_declarations.rb +1 -1
 - data/lib/taro/rails/declaration.rb +50 -9
 - data/lib/taro/rails/generators/install_generator.rb +1 -1
 - data/lib/taro/rails/generators/templates/errors_type.erb +15 -10
 - data/lib/taro/rails/normalized_route.rb +8 -0
 - data/lib/taro/rails/response_validation.rb +7 -57
 - data/lib/taro/rails/response_validator.rb +109 -0
 - data/lib/taro/rails/tasks/export.rake +5 -1
 - data/lib/taro/rails.rb +1 -2
 - data/lib/taro/types/base_type.rb +2 -0
 - data/lib/taro/types/coercion.rb +28 -17
 - data/lib/taro/types/enum_type.rb +2 -2
 - data/lib/taro/types/field.rb +8 -16
 - data/lib/taro/types/field_validation.rb +1 -1
 - data/lib/taro/types/list_type.rb +4 -6
 - data/lib/taro/types/object_types/free_form_type.rb +1 -0
 - data/lib/taro/types/object_types/no_content_type.rb +1 -0
 - data/lib/taro/types/object_types/page_info_type.rb +2 -0
 - data/lib/taro/types/object_types/page_type.rb +15 -25
 - data/lib/taro/types/scalar/iso8601_date_type.rb +1 -0
 - data/lib/taro/types/scalar/iso8601_datetime_type.rb +1 -0
 - data/lib/taro/types/scalar/timestamp_type.rb +1 -0
 - data/lib/taro/types/scalar/uuid_v4_type.rb +1 -0
 - data/lib/taro/types/shared/deprecation.rb +3 -0
 - data/lib/taro/types/shared/derived_types.rb +27 -0
 - data/lib/taro/types/shared/errors.rb +3 -1
 - data/lib/taro/types/shared/fields.rb +6 -5
 - data/lib/taro/types/shared/item_type.rb +1 -0
 - data/lib/taro/types/shared/object_coercion.rb +13 -0
 - data/lib/taro/types/shared/openapi_name.rb +8 -6
 - data/lib/taro/types/shared/rendering.rb +11 -25
 - data/lib/taro/version.rb +1 -1
 - data/tasks/benchmark.rake +1 -1
 - metadata +7 -5
 - data/lib/taro/types/shared/derivable_types.rb +0 -9
 
    
        checksums.yaml
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ---
         
     | 
| 
       2 
2 
     | 
    
         
             
            SHA256:
         
     | 
| 
       3 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       4 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 3 
     | 
    
         
            +
              metadata.gz: 5007f07dcb3230a1a45011138c19cc0f233e0a0e0fa81d64d554ee3d5cc4a82e
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: 99dde57ec71bb2724a29005e644a0bb23d642df11fe4555bd9203340169814df
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: abb5fb481da01a4a50b19e891c3373b42372c97993788ed24c51b07146e087e25d040bc4ea077606a14013633b30f6254f6ced2e939ba9b039b341d95f918211
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: b5dd6c2222598ad3d8ee64c64fd0fc6c40299c3f071b08317d17aa29c3f786924ae4b064f587b1e548ba102e0cc802b8a5adf7ccbadf033795974bfdaae5329a
         
     | 
    
        data/CHANGELOG.md
    CHANGED
    
    | 
         @@ -1,5 +1,39 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ## [Unreleased]
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
       3 
     | 
    
         
            -
            ## [ 
     | 
| 
      
 3 
     | 
    
         
            +
            ## [1.2.0] - 2024-11-18
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            ### Added
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            - Improved error messages
         
     | 
| 
      
 8 
     | 
    
         
            +
            - Option to define custom derived types
         
     | 
| 
      
 9 
     | 
    
         
            +
            - Option to use custom keys in paginated content
         
     | 
| 
      
 10 
     | 
    
         
            +
            - Option to deprecate individual fields, params, and types
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
            ### Fixed
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
            - Fixed nullable enum fields raising for null input
         
     | 
| 
      
 15 
     | 
    
         
            +
            - Fixed auto-loading of return types
         
     | 
| 
      
 16 
     | 
    
         
            +
            - Fixed console spam when inspecting declarations
         
     | 
| 
      
 17 
     | 
    
         
            +
            - Fixed resolver method not being used when rendering a Hash
         
     | 
| 
      
 18 
     | 
    
         
            +
            - Fixed the ErrorsType template
         
     | 
| 
      
 19 
     | 
    
         
            +
            - Many fixes for OpenAPI export
         
     | 
| 
      
 20 
     | 
    
         
            +
              - Fixed export of parameters for http methods without body
         
     | 
| 
      
 21 
     | 
    
         
            +
              - Fixed export for PageType
         
     | 
| 
      
 22 
     | 
    
         
            +
              - Fixed export for arrays of UUIDs, Dates, and Times
         
     | 
| 
      
 23 
     | 
    
         
            +
              - Fixed export YML keys for namespaced controllers
         
     | 
| 
      
 24 
     | 
    
         
            +
              - Reference plain types for repeated flat return types
         
     | 
| 
      
 25 
     | 
    
         
            +
              - Made order of paths, verbs, responses and schemas deterministic
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
            ## [1.1.0] - 2024-11-16
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
            ### Added
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
            - Response validation refined
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
            ### Fixed
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
            - Bugfix for openapi export
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
            ## [1.0.0] - 2024-11-14
         
     | 
| 
       4 
38 
     | 
    
         | 
| 
       5 
39 
     | 
    
         
             
            - Initial release
         
     | 
    
        data/README.md
    CHANGED
    
    | 
         @@ -10,11 +10,6 @@ It is inspired by [`apipie-rails`](https://github.com/Apipie/apipie-rails) and [ 
     | 
|
| 
       10 
10 
     | 
    
         
             
            - conveniently check request and response data against the declaration
         
     | 
| 
       11 
11 
     | 
    
         
             
            - offer an up-to-date OpenAPI export with minimal configuration
         
     | 
| 
       12 
12 
     | 
    
         | 
| 
       13 
     | 
    
         
            -
            ## ⚠️ This is a work in progress - TODO:
         
     | 
| 
       14 
     | 
    
         
            -
             
     | 
| 
       15 
     | 
    
         
            -
            - ISO8601Time, ISO8601Date types
         
     | 
| 
       16 
     | 
    
         
            -
            - ResponseValidation: allow rendering scalars directly (e.g. `render json: 42`)
         
     | 
| 
       17 
     | 
    
         
            -
             
     | 
| 
       18 
13 
     | 
    
         
             
            ## Installation
         
     | 
| 
       19 
14 
     | 
    
         | 
| 
       20 
15 
     | 
    
         
             
            ```bash
         
     | 
| 
         @@ -136,16 +131,18 @@ Taro.config.validate_responses = false 
     | 
|
| 
       136 
131 
     | 
    
         
             
            The following type names are available by default and can be used as `type:`/`array_of:`/`page_of:` arguments:
         
     | 
| 
       137 
132 
     | 
    
         | 
| 
       138 
133 
     | 
    
         
             
            - `'Boolean'` - accepts and renders `true` or `false`
         
     | 
| 
      
 134 
     | 
    
         
            +
            - `'Date'` - accepts and renders a date string in ISO8601 format
         
     | 
| 
      
 135 
     | 
    
         
            +
            - `'DateTime'` - an alias for `'Time'`
         
     | 
| 
       139 
136 
     | 
    
         
             
            - `'Float'`
         
     | 
| 
       140 
137 
     | 
    
         
             
            - `'FreeForm'` - accepts and renders any JSON-serializable object, use with care
         
     | 
| 
       141 
138 
     | 
    
         
             
            - `'Integer'`
         
     | 
| 
       142 
     | 
    
         
            -
            - `' 
     | 
| 
      
 139 
     | 
    
         
            +
            - `'NoContent'` - renders an empty object, for use with `status: :no_content`
         
     | 
| 
       143 
140 
     | 
    
         
             
            - `'String'`
         
     | 
| 
       144 
     | 
    
         
            -
            - `'Timestamp'` - renders a `Time` as unix timestamp integer and turns into incoming integers into a `Time`
         
     | 
| 
       145 
     | 
    
         
            -
            - `'UUID'` - accepts and renders UUIDs
         
     | 
| 
       146 
     | 
    
         
            -
            - `'Date'` - accepts and renders a date string in ISO8601 format
         
     | 
| 
       147 
141 
     | 
    
         
             
            - `'Time'` - accepts and renders a time string in ISO8601 format
         
     | 
| 
       148 
     | 
    
         
            -
            - `' 
     | 
| 
      
 142 
     | 
    
         
            +
            - `'Timestamp'` - renders a `Time` as unix timestamp integer and turns incoming integers into a `Time`
         
     | 
| 
      
 143 
     | 
    
         
            +
            - `'UUID'` - accepts and renders UUIDs
         
     | 
| 
      
 144 
     | 
    
         
            +
             
     | 
| 
      
 145 
     | 
    
         
            +
            Also, when using the generator, `ErrorsType` and `ErrorDetailsType` are generated as a starting point for unified error presentation. `ErrorsType` can render invalid `ActiveRecord` instances, `ActiveModel::Errors` and other data structures.
         
     | 
| 
       149 
146 
     | 
    
         | 
| 
       150 
147 
     | 
    
         
             
            ### Enums
         
     | 
| 
       151 
148 
     | 
    
         | 
| 
         @@ -173,7 +170,7 @@ end 
     | 
|
| 
       173 
170 
     | 
    
         | 
| 
       174 
171 
     | 
    
         
             
            ### FAQ
         
     | 
| 
       175 
172 
     | 
    
         | 
| 
       176 
     | 
    
         
            -
            #### How  
     | 
| 
      
 173 
     | 
    
         
            +
            #### How do I avoid repeating common error declarations?
         
     | 
| 
       177 
174 
     | 
    
         | 
| 
       178 
175 
     | 
    
         
             
            Hook into the DSL in your base controller(s):
         
     | 
| 
       179 
176 
     | 
    
         | 
| 
         @@ -202,7 +199,7 @@ class AuthenticatedApiController < ApiBaseController 
     | 
|
| 
       202 
199 
     | 
    
         
             
            end
         
     | 
| 
       203 
200 
     | 
    
         
             
            ```
         
     | 
| 
       204 
201 
     | 
    
         | 
| 
       205 
     | 
    
         
            -
            #### How  
     | 
| 
      
 202 
     | 
    
         
            +
            #### How do I use context in my types?
         
     | 
| 
       206 
203 
     | 
    
         | 
| 
       207 
204 
     | 
    
         
             
            Use [ActiveSupport::CurrentAttributes](https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html).
         
     | 
| 
       208 
205 
     | 
    
         | 
| 
         @@ -216,13 +213,80 @@ class BikeType < ObjectType 
     | 
|
| 
       216 
213 
     | 
    
         
             
            end
         
     | 
| 
       217 
214 
     | 
    
         
             
            ```
         
     | 
| 
       218 
215 
     | 
    
         | 
| 
      
 216 
     | 
    
         
            +
            #### How do I migrate from apipie-rails?
         
     | 
| 
      
 217 
     | 
    
         
            +
             
     | 
| 
      
 218 
     | 
    
         
            +
            First of all, if you don't need a better OpenAPI export, or better support for hashes and arrays, it might not be worth it.
         
     | 
| 
      
 219 
     | 
    
         
            +
             
     | 
| 
      
 220 
     | 
    
         
            +
            If you do:
         
     | 
| 
      
 221 
     | 
    
         
            +
             
     | 
| 
      
 222 
     | 
    
         
            +
            - note that `taro` currently only supports the latest OpenAPI standard (instead of v2 like `apipie-rails`)
         
     | 
| 
      
 223 
     | 
    
         
            +
            - extract complex param declarations into InputTypes
         
     | 
| 
      
 224 
     | 
    
         
            +
            - extract complex response declarations into ObjectTypes
         
     | 
| 
      
 225 
     | 
    
         
            +
            - replace `required: true` with `null: false` and `required: false` with `null: true`
         
     | 
| 
      
 226 
     | 
    
         
            +
             
     | 
| 
      
 227 
     | 
    
         
            +
            For a step-by-step migration, you might want to make `taro` use a different DSL then `apipie`:
         
     | 
| 
      
 228 
     | 
    
         
            +
             
     | 
| 
      
 229 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 230 
     | 
    
         
            +
            # config/initializers/taro.rb
         
     | 
| 
      
 231 
     | 
    
         
            +
            %i[api param returns].each do |m|
         
     | 
| 
      
 232 
     | 
    
         
            +
              Taro::Rails::DSL.alias_method("taro_#{m}", m) # `taro_api` etc.
         
     | 
| 
      
 233 
     | 
    
         
            +
              Taro::Rails::DSL.define_method(m) { |*a, **k, &b| super(*a, **k, &b) }
         
     | 
| 
      
 234 
     | 
    
         
            +
            end
         
     | 
| 
      
 235 
     | 
    
         
            +
            ```
         
     | 
| 
      
 236 
     | 
    
         
            +
             
     | 
| 
      
 237 
     | 
    
         
            +
            #### How do I keep lengthy API descriptions out of my controller?
         
     | 
| 
      
 238 
     | 
    
         
            +
             
     | 
| 
      
 239 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 240 
     | 
    
         
            +
            module BikeUpdateDesc
         
     | 
| 
      
 241 
     | 
    
         
            +
              extend ActiveSupport::Concern
         
     | 
| 
      
 242 
     | 
    
         
            +
             
     | 
| 
      
 243 
     | 
    
         
            +
              included do
         
     | 
| 
      
 244 
     | 
    
         
            +
                api 'Update a bike', description: 'Long description', tags: ['Bikes']
         
     | 
| 
      
 245 
     | 
    
         
            +
                # lots of params and returns ...
         
     | 
| 
      
 246 
     | 
    
         
            +
              end
         
     | 
| 
      
 247 
     | 
    
         
            +
            end
         
     | 
| 
      
 248 
     | 
    
         
            +
             
     | 
| 
      
 249 
     | 
    
         
            +
            class BikesController < ApplicationController
         
     | 
| 
      
 250 
     | 
    
         
            +
              include BikeUpdateDesc
         
     | 
| 
      
 251 
     | 
    
         
            +
              def update # ...
         
     | 
| 
      
 252 
     | 
    
         
            +
            end
         
     | 
| 
      
 253 
     | 
    
         
            +
            ```
         
     | 
| 
      
 254 
     | 
    
         
            +
             
     | 
| 
       219 
255 
     | 
    
         
             
            #### Why do I have to use type name strings instead of the type constants?
         
     | 
| 
       220 
256 
     | 
    
         | 
| 
       221 
257 
     | 
    
         
             
            Why e.g. `field :id, type: 'UUID'` instead of `field :id, type: UUID`?
         
     | 
| 
       222 
258 
     | 
    
         | 
| 
       223 
259 
     | 
    
         
             
            The purpose of this is to reduce unnecessary autoloading of the whole type dependency tree in dev and test environments.
         
     | 
| 
       224 
260 
     | 
    
         | 
| 
       225 
     | 
    
         
            -
             
     | 
| 
      
 261 
     | 
    
         
            +
            #### Can I define my own derived types like `page_of` or `array_of`?
         
     | 
| 
      
 262 
     | 
    
         
            +
             
     | 
| 
      
 263 
     | 
    
         
            +
            Yes.
         
     | 
| 
      
 264 
     | 
    
         
            +
             
     | 
| 
      
 265 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 266 
     | 
    
         
            +
            # Implement ::derive_from in your custom type.
         
     | 
| 
      
 267 
     | 
    
         
            +
            class PreviewType < Taro::Types::Scalar::StringType
         
     | 
| 
      
 268 
     | 
    
         
            +
              singleton_class.attr_reader :type_to_preview
         
     | 
| 
      
 269 
     | 
    
         
            +
             
     | 
| 
      
 270 
     | 
    
         
            +
              def self.derive_from(other_type)
         
     | 
| 
      
 271 
     | 
    
         
            +
                self.type_to_preview = other_type
         
     | 
| 
      
 272 
     | 
    
         
            +
              end
         
     | 
| 
      
 273 
     | 
    
         
            +
             
     | 
| 
      
 274 
     | 
    
         
            +
              def coerce_response
         
     | 
| 
      
 275 
     | 
    
         
            +
                type_to_preview.new(object).coerce_response.to_s.truncate(100)
         
     | 
| 
      
 276 
     | 
    
         
            +
              end
         
     | 
| 
      
 277 
     | 
    
         
            +
            end
         
     | 
| 
      
 278 
     | 
    
         
            +
             
     | 
| 
      
 279 
     | 
    
         
            +
            # Make it available in the DSL, e.g. in an initializer.
         
     | 
| 
      
 280 
     | 
    
         
            +
            Taro::Types::BaseType.define_derived_type :preview, 'PreviewType'
         
     | 
| 
      
 281 
     | 
    
         
            +
             
     | 
| 
      
 282 
     | 
    
         
            +
            # Usage:
         
     | 
| 
      
 283 
     | 
    
         
            +
            class MyController < ApplicationController
         
     | 
| 
      
 284 
     | 
    
         
            +
              returns code: :ok, preview_of: 'BikeType'
         
     | 
| 
      
 285 
     | 
    
         
            +
              def show
         
     | 
| 
      
 286 
     | 
    
         
            +
                render json: BikeType.preview.render(Bike.find(params[:id]))
         
     | 
| 
      
 287 
     | 
    
         
            +
              end
         
     | 
| 
      
 288 
     | 
    
         
            +
            end
         
     | 
| 
      
 289 
     | 
    
         
            +
            ```
         
     | 
| 
       226 
290 
     | 
    
         | 
| 
       227 
291 
     | 
    
         
             
            ## Possible future features
         
     | 
| 
       228 
292 
     | 
    
         | 
| 
         @@ -232,7 +296,6 @@ This already works fo type classes – they don't trigger loading of referenced 
     | 
|
| 
       232 
296 
     | 
    
         
             
            - sum types
         
     | 
| 
       233 
297 
     | 
    
         
             
            - api doc rendering based on export (e.g. rails engine with web ui)
         
     | 
| 
       234 
298 
     | 
    
         
             
            - [query logs metadata](https://github.com/rmosolgo/graphql-ruby/blob/dcaaed1cea47394fad61fceadf291ff3cb5f2932/lib/generators/graphql/install_generator.rb#L48-L52)
         
     | 
| 
       235 
     | 
    
         
            -
            - deprecation feature
         
     | 
| 
       236 
299 
     | 
    
         
             
            - maybe make `type:` optional for path params as they're always strings anyway
         
     | 
| 
       237 
300 
     | 
    
         
             
            - various openapi features
         
     | 
| 
       238 
301 
     | 
    
         
             
              - non-JSON content types (e.g. for file uploads)
         
     | 
    
        data/lib/taro/errors.rb
    CHANGED
    
    | 
         @@ -1,4 +1,10 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            class Taro::Error < StandardError 
     | 
| 
      
 1 
     | 
    
         
            +
            class Taro::Error < StandardError
         
     | 
| 
      
 2 
     | 
    
         
            +
              def message
         
     | 
| 
      
 3 
     | 
    
         
            +
                # clean up newlines introduced when setting the message with a heredoc
         
     | 
| 
      
 4 
     | 
    
         
            +
                super.chomp.sub(/\n(?=\S)/, ' ')
         
     | 
| 
      
 5 
     | 
    
         
            +
              end
         
     | 
| 
      
 6 
     | 
    
         
            +
            end
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
       2 
8 
     | 
    
         
             
            class Taro::ArgumentError < Taro::Error; end
         
     | 
| 
       3 
9 
     | 
    
         
             
            class Taro::RuntimeError < Taro::Error; end
         
     | 
| 
       4 
10 
     | 
    
         
             
            class Taro::ValidationError < Taro::RuntimeError; end # not to be used directly
         
     | 
| 
         @@ -11,15 +11,16 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla 
     | 
|
| 
       11 
11 
     | 
    
         
             
              def call(declarations:, title:, version:)
         
     | 
| 
       12 
12 
     | 
    
         
             
                @result = { openapi: '3.1.0', info: { title:, version: } }
         
     | 
| 
       13 
13 
     | 
    
         
             
                paths = export_paths(declarations)
         
     | 
| 
       14 
     | 
    
         
            -
                @result[:paths] = paths if paths.any?
         
     | 
| 
       15 
     | 
    
         
            -
                @result[:components] = { schemas: } if schemas.any?
         
     | 
| 
      
 14 
     | 
    
         
            +
                @result[:paths] = paths.sort.to_h if paths.any?
         
     | 
| 
      
 15 
     | 
    
         
            +
                @result[:components] = { schemas: schemas.sort.to_h } if schemas.any?
         
     | 
| 
       16 
16 
     | 
    
         
             
                self
         
     | 
| 
       17 
17 
     | 
    
         
             
              end
         
     | 
| 
       18 
18 
     | 
    
         | 
| 
       19 
19 
     | 
    
         
             
              def export_paths(declarations)
         
     | 
| 
       20 
     | 
    
         
            -
                declarations.each_with_object({}) do |declaration, paths|
         
     | 
| 
      
 20 
     | 
    
         
            +
                declarations.sort.each_with_object({}) do |declaration, paths|
         
     | 
| 
       21 
21 
     | 
    
         
             
                  declaration.routes.each do |route|
         
     | 
| 
       22 
     | 
    
         
            -
                    paths[route.openapi_path]  
     | 
| 
      
 22 
     | 
    
         
            +
                    paths[route.openapi_path] ||= {}
         
     | 
| 
      
 23 
     | 
    
         
            +
                    paths[route.openapi_path].merge! export_route(route, declaration)
         
     | 
| 
       23 
24 
     | 
    
         
             
                  end
         
     | 
| 
       24 
25 
     | 
    
         
             
                end
         
     | 
| 
       25 
26 
     | 
    
         
             
              end
         
     | 
| 
         @@ -30,30 +31,51 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla 
     | 
|
| 
       30 
31 
     | 
    
         
             
                    description: declaration.desc,
         
     | 
| 
       31 
32 
     | 
    
         
             
                    summary: declaration.summary,
         
     | 
| 
       32 
33 
     | 
    
         
             
                    tags: declaration.tags,
         
     | 
| 
       33 
     | 
    
         
            -
                    parameters:  
     | 
| 
      
 34 
     | 
    
         
            +
                    parameters: route_parameters(declaration, route),
         
     | 
| 
       34 
35 
     | 
    
         
             
                    requestBody: request_body(declaration, route),
         
     | 
| 
       35 
36 
     | 
    
         
             
                    responses: responses(declaration),
         
     | 
| 
       36 
37 
     | 
    
         
             
                  }.compact,
         
     | 
| 
       37 
38 
     | 
    
         
             
                }
         
     | 
| 
       38 
39 
     | 
    
         
             
              end
         
     | 
| 
       39 
40 
     | 
    
         | 
| 
      
 41 
     | 
    
         
            +
              def route_parameters(declaration, route)
         
     | 
| 
      
 42 
     | 
    
         
            +
                path_parameters(declaration, route) + query_parameters(declaration, route)
         
     | 
| 
      
 43 
     | 
    
         
            +
              end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
       40 
45 
     | 
    
         
             
              def path_parameters(declaration, route)
         
     | 
| 
       41 
46 
     | 
    
         
             
                route.path_params.map do |param_name|
         
     | 
| 
       42 
47 
     | 
    
         
             
                  param_field = declaration.params.fields[param_name] || raise(<<~MSG)
         
     | 
| 
       43 
48 
     | 
    
         
             
                    Declaration missing for path param #{param_name} of route #{route.endpoint}
         
     | 
| 
       44 
49 
     | 
    
         
             
                  MSG
         
     | 
| 
       45 
50 
     | 
    
         | 
| 
       46 
     | 
    
         
            -
                   
     | 
| 
       47 
     | 
    
         
            -
             
     | 
| 
       48 
     | 
    
         
            -
                    in: 'path',
         
     | 
| 
       49 
     | 
    
         
            -
                    description: param_field.desc,
         
     | 
| 
       50 
     | 
    
         
            -
                    required: true, # path params are always required in rails
         
     | 
| 
       51 
     | 
    
         
            -
                    schema: { type: param_field.openapi_type },
         
     | 
| 
       52 
     | 
    
         
            -
                  }.compact
         
     | 
| 
      
 51 
     | 
    
         
            +
                  # path params are always required in rails
         
     | 
| 
      
 52 
     | 
    
         
            +
                  export_parameter(param_field).merge(in: 'path', required: true)
         
     | 
| 
       53 
53 
     | 
    
         
             
                end
         
     | 
| 
       54 
54 
     | 
    
         
             
              end
         
     | 
| 
       55 
55 
     | 
    
         | 
| 
      
 56 
     | 
    
         
            +
              def query_parameters(declaration, route)
         
     | 
| 
      
 57 
     | 
    
         
            +
                return [] if route.can_have_request_body?
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
                declaration.params.fields.filter_map do |name, param_field|
         
     | 
| 
      
 60 
     | 
    
         
            +
                  next if route.path_params.include?(name)
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
                  export_parameter(param_field).merge(in: 'query')
         
     | 
| 
      
 63 
     | 
    
         
            +
                end
         
     | 
| 
      
 64 
     | 
    
         
            +
              end
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
              def export_parameter(field)
         
     | 
| 
      
 67 
     | 
    
         
            +
                {
         
     | 
| 
      
 68 
     | 
    
         
            +
                  name: field.name,
         
     | 
| 
      
 69 
     | 
    
         
            +
                  deprecated: field.deprecated,
         
     | 
| 
      
 70 
     | 
    
         
            +
                  description: field.desc,
         
     | 
| 
      
 71 
     | 
    
         
            +
                  required: !field.null,
         
     | 
| 
      
 72 
     | 
    
         
            +
                  schema: { type: field.openapi_type },
         
     | 
| 
      
 73 
     | 
    
         
            +
                }.compact
         
     | 
| 
      
 74 
     | 
    
         
            +
              end
         
     | 
| 
      
 75 
     | 
    
         
            +
             
     | 
| 
       56 
76 
     | 
    
         
             
              def request_body(declaration, route)
         
     | 
| 
      
 77 
     | 
    
         
            +
                return unless route.can_have_request_body?
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
       57 
79 
     | 
    
         
             
                params = declaration.params
         
     | 
| 
       58 
80 
     | 
    
         
             
                body_param_fields = params.fields.reject do |name, _field|
         
     | 
| 
       59 
81 
     | 
    
         
             
                  route.path_params.include?(name)
         
     | 
| 
         @@ -81,7 +103,7 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla 
     | 
|
| 
       81 
103 
     | 
    
         
             
              end
         
     | 
| 
       82 
104 
     | 
    
         | 
| 
       83 
105 
     | 
    
         
             
              def responses(declaration)
         
     | 
| 
       84 
     | 
    
         
            -
                declaration.returns.to_h do |code, type|
         
     | 
| 
      
 106 
     | 
    
         
            +
                declaration.returns.sort.to_h do |code, type|
         
     | 
| 
       85 
107 
     | 
    
         
             
                  [
         
     | 
| 
       86 
108 
     | 
    
         
             
                    code.to_s,
         
     | 
| 
       87 
109 
     | 
    
         
             
                    {
         
     | 
| 
         @@ -114,23 +136,29 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla 
     | 
|
| 
       114 
136 
     | 
    
         
             
                # as it puts props like format together with the main type.
         
     | 
| 
       115 
137 
     | 
    
         
             
                # https://github.com/OAI/OpenAPI-Specification/issues/3148
         
     | 
| 
       116 
138 
     | 
    
         
             
                base = { oneOf: [base, { type: 'null' }] } if field.null
         
     | 
| 
       117 
     | 
    
         
            -
                base 
     | 
| 
       118 
     | 
    
         
            -
                base[:default] = field.default if field.default_specified?
         
     | 
| 
       119 
     | 
    
         
            -
                base[:enum] = field.enum if field.enum
         
     | 
| 
       120 
     | 
    
         
            -
                base
         
     | 
| 
      
 139 
     | 
    
         
            +
                base.merge(field_metadata(field))
         
     | 
| 
       121 
140 
     | 
    
         
             
              end
         
     | 
| 
       122 
141 
     | 
    
         | 
| 
       123 
142 
     | 
    
         
             
              def export_complex_field_ref(field)
         
     | 
| 
       124 
143 
     | 
    
         
             
                ref = extract_component_ref(field.type)
         
     | 
| 
      
 144 
     | 
    
         
            +
                return ref if field_metadata(field).empty? && !field.null
         
     | 
| 
      
 145 
     | 
    
         
            +
             
     | 
| 
       125 
146 
     | 
    
         
             
                if field.null
         
     | 
| 
       126 
147 
     | 
    
         
             
                  # RE nullable: https://stackoverflow.com/a/70658334
         
     | 
| 
       127 
     | 
    
         
            -
                  {  
     | 
| 
       128 
     | 
    
         
            -
                 
     | 
| 
      
 148 
     | 
    
         
            +
                  { oneOf: [ref, { type: 'null' }] }
         
     | 
| 
      
 149 
     | 
    
         
            +
                else # i.e. with metadata such as description or deprecated
         
     | 
| 
       129 
150 
     | 
    
         
             
                  # https://github.com/OAI/OpenAPI-Specification/issues/2033
         
     | 
| 
       130 
     | 
    
         
            -
                  {  
     | 
| 
       131 
     | 
    
         
            -
                 
     | 
| 
       132 
     | 
    
         
            -
             
     | 
| 
       133 
     | 
    
         
            -
             
     | 
| 
      
 151 
     | 
    
         
            +
                  { allOf: [ref] }
         
     | 
| 
      
 152 
     | 
    
         
            +
                end.merge(field_metadata(field))
         
     | 
| 
      
 153 
     | 
    
         
            +
              end
         
     | 
| 
      
 154 
     | 
    
         
            +
             
     | 
| 
      
 155 
     | 
    
         
            +
              def field_metadata(field)
         
     | 
| 
      
 156 
     | 
    
         
            +
                meta = {}
         
     | 
| 
      
 157 
     | 
    
         
            +
                meta[:description] = field.desc if field.desc
         
     | 
| 
      
 158 
     | 
    
         
            +
                meta[:deprecated] = field.deprecated unless field.deprecated.nil?
         
     | 
| 
      
 159 
     | 
    
         
            +
                meta[:default] = field.default if field.default_specified?
         
     | 
| 
      
 160 
     | 
    
         
            +
                meta[:enum] = field.enum if field.enum
         
     | 
| 
      
 161 
     | 
    
         
            +
                meta
         
     | 
| 
       134 
162 
     | 
    
         
             
              end
         
     | 
| 
       135 
163 
     | 
    
         | 
| 
       136 
164 
     | 
    
         
             
              def extract_component_ref(type)
         
     | 
| 
         @@ -155,6 +183,7 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla 
     | 
|
| 
       155 
183 
     | 
    
         
             
                required = type.fields.values.reject(&:null).map(&:name)
         
     | 
| 
       156 
184 
     | 
    
         
             
                {
         
     | 
| 
       157 
185 
     | 
    
         
             
                  type: type.openapi_type,
         
     | 
| 
      
 186 
     | 
    
         
            +
                  deprecated: type.deprecated,
         
     | 
| 
       158 
187 
     | 
    
         
             
                  description: type.desc,
         
     | 
| 
       159 
188 
     | 
    
         
             
                  required: (required if required.any?),
         
     | 
| 
       160 
189 
     | 
    
         
             
                  properties: type.fields.to_h { |name, f| [name, export_field(f)] },
         
     | 
| 
         @@ -165,6 +194,7 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla 
     | 
|
| 
       165 
194 
     | 
    
         
             
              def enum_type_details(enum)
         
     | 
| 
       166 
195 
     | 
    
         
             
                {
         
     | 
| 
       167 
196 
     | 
    
         
             
                  type: enum.item_type.openapi_type,
         
     | 
| 
      
 197 
     | 
    
         
            +
                  deprecated: enum.deprecated,
         
     | 
| 
       168 
198 
     | 
    
         
             
                  description: enum.desc,
         
     | 
| 
       169 
199 
     | 
    
         
             
                  enum: enum.values,
         
     | 
| 
       170 
200 
     | 
    
         
             
                }.compact
         
     | 
| 
         @@ -173,6 +203,7 @@ class Taro::Export::OpenAPIv3 < Taro::Export::Base # rubocop:disable Metrics/Cla 
     | 
|
| 
       173 
203 
     | 
    
         
             
              def list_type_details(list)
         
     | 
| 
       174 
204 
     | 
    
         
             
                {
         
     | 
| 
       175 
205 
     | 
    
         
             
                  type: 'array',
         
     | 
| 
      
 206 
     | 
    
         
            +
                  deprecated: list.deprecated,
         
     | 
| 
       176 
207 
     | 
    
         
             
                  description: list.desc,
         
     | 
| 
       177 
208 
     | 
    
         
             
                  items: export_type(list.item_type),
         
     | 
| 
       178 
209 
     | 
    
         
             
                }.compact
         
     | 
| 
         @@ -2,7 +2,7 @@ module Taro::Rails::ActiveDeclarations 
     | 
|
| 
       2 
2 
     | 
    
         
             
              def apply(declaration:, controller_class:, action_name:)
         
     | 
| 
       3 
3 
     | 
    
         
             
                (declarations_map[controller_class] ||= {})[action_name] = declaration
         
     | 
| 
       4 
4 
     | 
    
         
             
                Taro::Rails::ParamParsing.install(controller_class:, action_name:)
         
     | 
| 
       5 
     | 
    
         
            -
                Taro::Rails::ResponseValidation.install(controller_class 
     | 
| 
      
 5 
     | 
    
         
            +
                Taro::Rails::ResponseValidation.install(controller_class:)
         
     | 
| 
       6 
6 
     | 
    
         
             
              end
         
     | 
| 
       7 
7 
     | 
    
         | 
| 
       8 
8 
     | 
    
         
             
              def declarations_map
         
     | 
| 
         @@ -1,9 +1,9 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            class Taro::Rails::Declaration
         
     | 
| 
       2 
     | 
    
         
            -
              attr_reader :desc, :summary, :params, : 
     | 
| 
      
 2 
     | 
    
         
            +
              attr_reader :desc, :summary, :params, :return_defs, :return_descriptions, :return_nestings, :routes, :tags
         
     | 
| 
       3 
3 
     | 
    
         | 
| 
       4 
4 
     | 
    
         
             
              def initialize
         
     | 
| 
       5 
5 
     | 
    
         
             
                @params = Class.new(Taro::Types::InputType)
         
     | 
| 
       6 
     | 
    
         
            -
                @ 
     | 
| 
      
 6 
     | 
    
         
            +
                @return_defs = {}
         
     | 
| 
       7 
7 
     | 
    
         
             
                @return_descriptions = {}
         
     | 
| 
       8 
8 
     | 
    
         
             
                @return_nestings = {}
         
     | 
| 
       9 
9 
     | 
    
         
             
              end
         
     | 
| 
         @@ -24,8 +24,11 @@ class Taro::Rails::Declaration 
     | 
|
| 
       24 
24 
     | 
    
         
             
                status = self.class.coerce_status_to_int(code)
         
     | 
| 
       25 
25 
     | 
    
         
             
                raise_if_already_declared(status)
         
     | 
| 
       26 
26 
     | 
    
         | 
| 
      
 27 
     | 
    
         
            +
                kwargs[:nesting] = nesting
         
     | 
| 
      
 28 
     | 
    
         
            +
                check_return_kwargs(kwargs)
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
       27 
30 
     | 
    
         
             
                kwargs[:defined_at] = caller_locations(1..2)[1]
         
     | 
| 
       28 
     | 
    
         
            -
                 
     | 
| 
      
 31 
     | 
    
         
            +
                return_defs[status] = kwargs
         
     | 
| 
       29 
32 
     | 
    
         | 
| 
       30 
33 
     | 
    
         
             
                # response desc is required in openapi 3 – fall back to status code
         
     | 
| 
       31 
34 
     | 
    
         
             
                return_descriptions[status] = desc || code.to_s
         
     | 
| 
         @@ -34,14 +37,19 @@ class Taro::Rails::Declaration 
     | 
|
| 
       34 
37 
     | 
    
         
             
                return_nestings[status] = nesting if nesting
         
     | 
| 
       35 
38 
     | 
    
         
             
              end
         
     | 
| 
       36 
39 
     | 
    
         | 
| 
      
 40 
     | 
    
         
            +
              # Return types are evaluated lazily to avoid unnecessary autoloading
         
     | 
| 
      
 41 
     | 
    
         
            +
              # of all types in dev/test envs.
         
     | 
| 
      
 42 
     | 
    
         
            +
              def returns
         
     | 
| 
      
 43 
     | 
    
         
            +
                @returns ||= evaluate_return_defs
         
     | 
| 
      
 44 
     | 
    
         
            +
              end
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
       37 
46 
     | 
    
         
             
              def raise_if_already_declared(status)
         
     | 
| 
       38 
     | 
    
         
            -
                 
     | 
| 
      
 47 
     | 
    
         
            +
                return_defs[status] &&
         
     | 
| 
       39 
48 
     | 
    
         
             
                  raise(Taro::ArgumentError, "response for status #{status} already declared")
         
     | 
| 
       40 
49 
     | 
    
         
             
              end
         
     | 
| 
       41 
50 
     | 
    
         | 
| 
       42 
51 
     | 
    
         
             
              def parse_params(rails_params)
         
     | 
| 
       43 
     | 
    
         
            -
                 
     | 
| 
       44 
     | 
    
         
            -
                hash
         
     | 
| 
      
 52 
     | 
    
         
            +
                params.new(rails_params.to_unsafe_h).coerce_input
         
     | 
| 
       45 
53 
     | 
    
         
             
              end
         
     | 
| 
       46 
54 
     | 
    
         | 
| 
       47 
55 
     | 
    
         
             
              def finalize(controller_class:, action_name:)
         
     | 
| 
         @@ -67,10 +75,15 @@ class Taro::Rails::Declaration 
     | 
|
| 
       67 
75 
     | 
    
         
             
              # TODO: these change when the controller class is renamed.
         
     | 
| 
       68 
76 
     | 
    
         
             
              # We might need a way to set `base`. Perhaps as a kwarg to `::api`?
         
     | 
| 
       69 
77 
     | 
    
         
             
              def add_openapi_names(controller_class:, action_name:)
         
     | 
| 
       70 
     | 
    
         
            -
                base = "#{controller_class.name.chomp('Controller'). 
     | 
| 
      
 78 
     | 
    
         
            +
                base = "#{controller_class.name.chomp('Controller').gsub('::', '_')}_#{action_name}"
         
     | 
| 
       71 
79 
     | 
    
         
             
                params.openapi_name = "#{base}_Input"
         
     | 
| 
      
 80 
     | 
    
         
            +
                params.define_singleton_method(:name) { openapi_name }
         
     | 
| 
      
 81 
     | 
    
         
            +
             
     | 
| 
       72 
82 
     | 
    
         
             
                returns.each do |status, return_type|
         
     | 
| 
      
 83 
     | 
    
         
            +
                  next if return_type.openapi_name? # only set for ad-hoc / nested return types
         
     | 
| 
      
 84 
     | 
    
         
            +
             
     | 
| 
       73 
85 
     | 
    
         
             
                  return_type.openapi_name = "#{base}_#{status}_Response"
         
     | 
| 
      
 86 
     | 
    
         
            +
                  return_type.define_singleton_method(:name) { openapi_name }
         
     | 
| 
       74 
87 
     | 
    
         
             
                end
         
     | 
| 
       75 
88 
     | 
    
         
             
              end
         
     | 
| 
       76 
89 
     | 
    
         | 
| 
         @@ -86,10 +99,34 @@ class Taro::Rails::Declaration 
     | 
|
| 
       86 
99 
     | 
    
         | 
| 
       87 
100 
     | 
    
         
             
              private
         
     | 
| 
       88 
101 
     | 
    
         | 
| 
       89 
     | 
    
         
            -
              def  
     | 
| 
      
 102 
     | 
    
         
            +
              def check_return_kwargs(kwargs)
         
     | 
| 
      
 103 
     | 
    
         
            +
                # For nested returns, evaluate_return_def calls ::field, which validates
         
     | 
| 
      
 104 
     | 
    
         
            +
                # field options, but does not trigger type autoloading.
         
     | 
| 
      
 105 
     | 
    
         
            +
                return evaluate_return_def(**kwargs) if kwargs[:nesting]
         
     | 
| 
      
 106 
     | 
    
         
            +
             
     | 
| 
      
 107 
     | 
    
         
            +
                if kwargs.key?(:null)
         
     | 
| 
      
 108 
     | 
    
         
            +
                  raise Taro::ArgumentError, <<~MSG
         
     | 
| 
      
 109 
     | 
    
         
            +
                    `null:` is not supported for top-level returns. If you want a nullable return
         
     | 
| 
      
 110 
     | 
    
         
            +
                    value, nest it, e.g. `returns :str, type: 'String', null: true`.
         
     | 
| 
      
 111 
     | 
    
         
            +
                  MSG
         
     | 
| 
      
 112 
     | 
    
         
            +
                end
         
     | 
| 
      
 113 
     | 
    
         
            +
             
     | 
| 
      
 114 
     | 
    
         
            +
                bad_keys = kwargs.keys - (Taro::Types::Coercion.keys + %i[code desc nesting])
         
     | 
| 
      
 115 
     | 
    
         
            +
                return if bad_keys.empty?
         
     | 
| 
      
 116 
     | 
    
         
            +
             
     | 
| 
      
 117 
     | 
    
         
            +
                raise Taro::ArgumentError, "Invalid `returns` options: #{bad_keys.join(', ')}"
         
     | 
| 
      
 118 
     | 
    
         
            +
              end
         
     | 
| 
      
 119 
     | 
    
         
            +
             
     | 
| 
      
 120 
     | 
    
         
            +
              def evaluate_return_defs
         
     | 
| 
      
 121 
     | 
    
         
            +
                return_defs.transform_values { |defi| evaluate_return_def(**defi) }
         
     | 
| 
      
 122 
     | 
    
         
            +
              end
         
     | 
| 
      
 123 
     | 
    
         
            +
             
     | 
| 
      
 124 
     | 
    
         
            +
              def evaluate_return_def(nesting:, **kwargs)
         
     | 
| 
       90 
125 
     | 
    
         
             
                if nesting
         
     | 
| 
       91 
126 
     | 
    
         
             
                  # ad-hoc return type, requiring the actual return type to be nested
         
     | 
| 
       92 
     | 
    
         
            -
                  Class.new(Taro::Types::ObjectType).tap  
     | 
| 
      
 127 
     | 
    
         
            +
                  Class.new(Taro::Types::ObjectType).tap do |type|
         
     | 
| 
      
 128 
     | 
    
         
            +
                    type.field(nesting, null: false, **kwargs)
         
     | 
| 
      
 129 
     | 
    
         
            +
                  end
         
     | 
| 
       93 
130 
     | 
    
         
             
                else
         
     | 
| 
       94 
131 
     | 
    
         
             
                  Taro::Types::Coercion.call(kwargs)
         
     | 
| 
       95 
132 
     | 
    
         
             
                end
         
     | 
| 
         @@ -98,4 +135,8 @@ class Taro::Rails::Declaration 
     | 
|
| 
       98 
135 
     | 
    
         
             
              def raise_missing_route(controller_class, action_name)
         
     | 
| 
       99 
136 
     | 
    
         
             
                raise(Taro::ArgumentError, "No route found for #{controller_class}##{action_name}")
         
     | 
| 
       100 
137 
     | 
    
         
             
              end
         
     | 
| 
      
 138 
     | 
    
         
            +
             
     | 
| 
      
 139 
     | 
    
         
            +
              def <=>(other)
         
     | 
| 
      
 140 
     | 
    
         
            +
                params.openapi_name <=> other.params.openapi_name
         
     | 
| 
      
 141 
     | 
    
         
            +
              end
         
     | 
| 
       101 
142 
     | 
    
         
             
            end
         
     | 
| 
         @@ -12,7 +12,7 @@ class Taro::Rails::Generators::InstallGenerator < ::Rails::Generators::Base 
     | 
|
| 
       12 
12 
     | 
    
         
             
              def create_type_files
         
     | 
| 
       13 
13 
     | 
    
         
             
                Dir["#{self.class.source_root}/**/*.erb"].each do |tmpl|
         
     | 
| 
       14 
14 
     | 
    
         
             
                  dest_dir = options[:dir].chomp('/')
         
     | 
| 
       15 
     | 
    
         
            -
                   
     | 
| 
      
 15 
     | 
    
         
            +
                  template tmpl, "#{dest_dir}/#{File.basename(tmpl).sub('erb', 'rb')}"
         
     | 
| 
       16 
16 
     | 
    
         
             
                end
         
     | 
| 
       17 
17 
     | 
    
         
             
              end
         
     | 
| 
       18 
18 
     | 
    
         
             
              # :nocov:
         
     | 
| 
         @@ -11,15 +11,20 @@ class ErrorsType < Taro::Types::ListType 
     | 
|
| 
       11 
11 
     | 
    
         
             
              end
         
     | 
| 
       12 
12 
     | 
    
         | 
| 
       13 
13 
     | 
    
         
             
              def coerce_response
         
     | 
| 
       14 
     | 
    
         
            -
                 
     | 
| 
       15 
     | 
    
         
            -
             
     | 
| 
       16 
     | 
    
         
            -
                   
     | 
| 
       17 
     | 
    
         
            -
             
     | 
| 
       18 
     | 
    
         
            -
                   
     | 
| 
       19 
     | 
    
         
            -
             
     | 
| 
       20 
     | 
    
         
            -
                   
     | 
| 
       21 
     | 
    
         
            -
             
     | 
| 
       22 
     | 
    
         
            -
                   
     | 
| 
       23 
     | 
    
         
            -
             
     | 
| 
      
 14 
     | 
    
         
            +
                list =
         
     | 
| 
      
 15 
     | 
    
         
            +
                  case object<%- if defined?(ActiveRecord) %>
         
     | 
| 
      
 16 
     | 
    
         
            +
                  when ActiveModel::Errors
         
     | 
| 
      
 17 
     | 
    
         
            +
                    object.errors
         
     | 
| 
      
 18 
     | 
    
         
            +
                  when ActiveRecord::Base
         
     | 
| 
      
 19 
     | 
    
         
            +
                    object.errors.errors<%- end %>
         
     | 
| 
      
 20 
     | 
    
         
            +
                  when Array
         
     | 
| 
      
 21 
     | 
    
         
            +
                    object
         
     | 
| 
      
 22 
     | 
    
         
            +
                  when Hash<%- if defined?(Interactor::Context) %>, Interactor::Context<%- end %>
         
     | 
| 
      
 23 
     | 
    
         
            +
                    object.to_h.fetch(:errors)
         
     | 
| 
      
 24 
     | 
    
         
            +
                  else
         
     | 
| 
      
 25 
     | 
    
         
            +
                    response_error("must be an Enumerable or an object with errors")
         
     | 
| 
      
 26 
     | 
    
         
            +
                  end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                list.map { |el| self.class.item_type.new(el).coerce_response }
         
     | 
| 
       24 
29 
     | 
    
         
             
              end
         
     | 
| 
       25 
30 
     | 
    
         
             
            end
         
     | 
| 
         @@ -26,4 +26,12 @@ Taro::Rails::NormalizedRoute = Data.define(:rails_route) do 
     | 
|
| 
       26 
26 
     | 
    
         
             
                controller, action = rails_route.requirements.values_at(:controller, :action)
         
     | 
| 
       27 
27 
     | 
    
         
             
                "#{controller}##{action}"
         
     | 
| 
       28 
28 
     | 
    
         
             
              end
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
              def can_have_request_body?
         
     | 
| 
      
 31 
     | 
    
         
            +
                %w[patch post put].include?(verb)
         
     | 
| 
      
 32 
     | 
    
         
            +
              end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
              def inspect
         
     | 
| 
      
 35 
     | 
    
         
            +
                %(#<#{self.class} "#{verb} #{openapi_path}">)
         
     | 
| 
      
 36 
     | 
    
         
            +
              end
         
     | 
| 
       29 
37 
     | 
    
         
             
            end
         
     | 
| 
         @@ -1,63 +1,13 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            module Taro::Rails::ResponseValidation
         
     | 
| 
       2 
     | 
    
         
            -
              def self.install(controller_class 
     | 
| 
       3 
     | 
    
         
            -
                 
     | 
| 
       4 
     | 
    
         
            -
             
     | 
| 
       5 
     | 
    
         
            -
                key = [controller_class, action_name]
         
     | 
| 
       6 
     | 
    
         
            -
                return if installed[key]
         
     | 
| 
       7 
     | 
    
         
            -
             
     | 
| 
       8 
     | 
    
         
            -
                installed[key] = true
         
     | 
| 
       9 
     | 
    
         
            -
             
     | 
| 
       10 
     | 
    
         
            -
                controller_class.around_action(only: action_name) do |_, block|
         
     | 
| 
       11 
     | 
    
         
            -
                  Taro::Types::BaseType.rendering = nil
         
     | 
| 
       12 
     | 
    
         
            -
                  block.call
         
     | 
| 
       13 
     | 
    
         
            -
                  Taro::Rails::ResponseValidation.call(self)
         
     | 
| 
       14 
     | 
    
         
            -
                ensure
         
     | 
| 
       15 
     | 
    
         
            -
                  Taro::Types::BaseType.rendering = nil
         
     | 
| 
       16 
     | 
    
         
            -
                end
         
     | 
| 
       17 
     | 
    
         
            -
              end
         
     | 
| 
       18 
     | 
    
         
            -
             
     | 
| 
       19 
     | 
    
         
            -
              def self.installed
         
     | 
| 
       20 
     | 
    
         
            -
                @installed ||= {}
         
     | 
| 
       21 
     | 
    
         
            -
              end
         
     | 
| 
       22 
     | 
    
         
            -
             
     | 
| 
       23 
     | 
    
         
            -
              def self.call(controller)
         
     | 
| 
       24 
     | 
    
         
            -
                declaration = Taro::Rails.declaration_for(controller)
         
     | 
| 
       25 
     | 
    
         
            -
                nesting = declaration.return_nestings[controller.status]
         
     | 
| 
       26 
     | 
    
         
            -
                expected = declaration.returns[controller.status]
         
     | 
| 
       27 
     | 
    
         
            -
                if nesting
         
     | 
| 
       28 
     | 
    
         
            -
                  # case: `returns :some_nesting, type: 'SomeType'` (ad-hoc return type)
         
     | 
| 
       29 
     | 
    
         
            -
                  check_nesting(controller.response, nesting)
         
     | 
| 
       30 
     | 
    
         
            -
                  expected = expected.fields[nesting].type
         
     | 
| 
       31 
     | 
    
         
            -
                end
         
     | 
| 
       32 
     | 
    
         
            -
             
     | 
| 
       33 
     | 
    
         
            -
                check_expected_type_was_used(controller, expected)
         
     | 
| 
       34 
     | 
    
         
            -
              end
         
     | 
| 
       35 
     | 
    
         
            -
             
     | 
| 
       36 
     | 
    
         
            -
              def self.check_nesting(response, nesting)
         
     | 
| 
       37 
     | 
    
         
            -
                return unless /json/.match?(response.media_type)
         
     | 
| 
       38 
     | 
    
         
            -
             
     | 
| 
       39 
     | 
    
         
            -
                first_key = response.body.to_s[/\A{\s*"([^"]+)"/, 1]
         
     | 
| 
       40 
     | 
    
         
            -
                first_key == nesting.to_s || raise(Taro::ResponseError, <<~MSG)
         
     | 
| 
       41 
     | 
    
         
            -
                  Expected response to be nested in "#{nesting}" key, but it was not.
         
     | 
| 
       42 
     | 
    
         
            -
                  (First JSON key in response: "#{first_key}".)
         
     | 
| 
       43 
     | 
    
         
            -
                MSG
         
     | 
| 
      
 2 
     | 
    
         
            +
              def self.install(controller_class:)
         
     | 
| 
      
 3 
     | 
    
         
            +
                controller_class.prepend(self) if Taro.config.validate_response
         
     | 
| 
       44 
4 
     | 
    
         
             
              end
         
     | 
| 
       45 
5 
     | 
    
         | 
| 
       46 
     | 
    
         
            -
              def  
     | 
| 
       47 
     | 
    
         
            -
                 
     | 
| 
       48 
     | 
    
         
            -
             
     | 
| 
       49 
     | 
    
         
            -
             
     | 
| 
       50 
     | 
    
         
            -
                  raise(Taro::ResponseError, <<~MSG)
         
     | 
| 
       51 
     | 
    
         
            -
                    No matching return type declared in #{controller.class}##{controller.action_name}\
         
     | 
| 
       52 
     | 
    
         
            -
                    for status #{controller.status}.
         
     | 
| 
       53 
     | 
    
         
            -
                  MSG
         
     | 
| 
      
 6 
     | 
    
         
            +
              def render(*, **kwargs, &)
         
     | 
| 
      
 7 
     | 
    
         
            +
                result = super
         
     | 
| 
      
 8 
     | 
    
         
            +
                if (declaration = Taro::Rails.declaration_for(self))
         
     | 
| 
      
 9 
     | 
    
         
            +
                  Taro::Rails::ResponseValidator.call(self, declaration, kwargs[:json])
         
     | 
| 
       54 
10 
     | 
    
         
             
                end
         
     | 
| 
       55 
     | 
    
         
            -
             
     | 
| 
       56 
     | 
    
         
            -
                used&.<=(expected) || raise(Taro::ResponseError, <<~MSG)
         
     | 
| 
       57 
     | 
    
         
            -
                  Expected #{controller.class}##{controller.action_name} to use #{expected}.render,
         
     | 
| 
       58 
     | 
    
         
            -
                  but #{used ? "#{used}.render" : 'no type render method'} was called.
         
     | 
| 
       59 
     | 
    
         
            -
                MSG
         
     | 
| 
       60 
     | 
    
         
            -
             
     | 
| 
       61 
     | 
    
         
            -
                Taro::Types::BaseType.used_in_response = used # for comparisons in specs
         
     | 
| 
      
 11 
     | 
    
         
            +
                result
         
     | 
| 
       62 
12 
     | 
    
         
             
              end
         
     | 
| 
       63 
13 
     | 
    
         
             
            end
         
     |