openapi_first 1.0.0.beta5 → 1.0.0.beta6
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/.github/workflows/ruby.yml +2 -1
- data/CHANGELOG.md +8 -0
- data/Gemfile +2 -1
- data/Gemfile.lock +6 -9
- data/Gemfile.rack2 +15 -0
- data/lib/openapi_first/{body_parser_middleware.rb → body_parser.rb} +3 -15
- data/lib/openapi_first/definition/cookie_parameters.rb +12 -0
- data/lib/openapi_first/definition/has_content.rb +37 -0
- data/lib/openapi_first/definition/header_parameters.rb +12 -0
- data/lib/openapi_first/definition/operation.rb +103 -0
- data/lib/openapi_first/definition/parameters.rb +47 -0
- data/lib/openapi_first/definition/path_item.rb +23 -0
- data/lib/openapi_first/definition/path_parameters.rb +13 -0
- data/lib/openapi_first/definition/query_parameters.rb +12 -0
- data/lib/openapi_first/definition/request_body.rb +32 -0
- data/lib/openapi_first/definition/response.rb +37 -0
- data/lib/openapi_first/{json_schema → definition/schema}/result.rb +1 -1
- data/lib/openapi_first/{json_schema.rb → definition/schema.rb} +2 -2
- data/lib/openapi_first/definition.rb +26 -6
- data/lib/openapi_first/error_response.rb +2 -0
- data/lib/openapi_first/request_body_validator.rb +17 -21
- data/lib/openapi_first/request_validation.rb +34 -30
- data/lib/openapi_first/response_validation.rb +31 -11
- data/lib/openapi_first/router.rb +19 -53
- data/lib/openapi_first/version.rb +1 -1
- data/openapi_first.gemspec +7 -4
- metadata +32 -52
- data/.rspec +0 -3
- data/.rubocop.yml +0 -14
- data/Rakefile +0 -15
- data/benchmarks/Gemfile +0 -16
- data/benchmarks/Gemfile.lock +0 -142
- data/benchmarks/README.md +0 -29
- data/benchmarks/apps/committee_with_hanami_api.ru +0 -26
- data/benchmarks/apps/committee_with_response_validation.ru +0 -29
- data/benchmarks/apps/committee_with_sinatra.ru +0 -31
- data/benchmarks/apps/grape.ru +0 -21
- data/benchmarks/apps/hanami_api.ru +0 -21
- data/benchmarks/apps/hanami_router.ru +0 -14
- data/benchmarks/apps/openapi.yaml +0 -268
- data/benchmarks/apps/openapi_first_with_hanami_api.ru +0 -24
- data/benchmarks/apps/openapi_first_with_plain_rack.ru +0 -32
- data/benchmarks/apps/openapi_first_with_response_validation.ru +0 -25
- data/benchmarks/apps/openapi_first_with_sinatra.ru +0 -29
- data/benchmarks/apps/roda.ru +0 -27
- data/benchmarks/apps/sinatra.ru +0 -26
- data/benchmarks/apps/syro.ru +0 -25
- data/benchmarks/benchmark-wrk.sh +0 -3
- data/benchmarks/benchmarks.rb +0 -48
- data/benchmarks/post.lua +0 -3
- data/bin/console +0 -15
- data/bin/setup +0 -8
- data/examples/README.md +0 -13
- data/examples/app.rb +0 -18
- data/examples/config.ru +0 -7
- data/examples/openapi.yaml +0 -29
- data/lib/openapi_first/operation.rb +0 -170
- data/lib/openapi_first/string_keyed_hash.rb +0 -20
    
        data/benchmarks/post.lua
    DELETED
    
    
    
        data/bin/console
    DELETED
    
    | @@ -1,15 +0,0 @@ | |
| 1 | 
            -
            #!/usr/bin/env ruby
         | 
| 2 | 
            -
            # frozen_string_literal: true
         | 
| 3 | 
            -
             | 
| 4 | 
            -
            require 'bundler/setup'
         | 
| 5 | 
            -
            require 'openapi_first'
         | 
| 6 | 
            -
             | 
| 7 | 
            -
            # You can add fixtures and/or initialization code here to make experimenting
         | 
| 8 | 
            -
            # with your gem easier. You can also use a different console, if you like.
         | 
| 9 | 
            -
             | 
| 10 | 
            -
            # (If you use this, don't forget to add pry to your Gemfile!)
         | 
| 11 | 
            -
            # require "pry"
         | 
| 12 | 
            -
            # Pry.start
         | 
| 13 | 
            -
             | 
| 14 | 
            -
            require 'irb'
         | 
| 15 | 
            -
            IRB.start(__FILE__)
         | 
    
        data/bin/setup
    DELETED
    
    
    
        data/examples/README.md
    DELETED
    
    
    
        data/examples/app.rb
    DELETED
    
    | @@ -1,18 +0,0 @@ | |
| 1 | 
            -
            # frozen_string_literal: true
         | 
| 2 | 
            -
             | 
| 3 | 
            -
            require 'openapi_first'
         | 
| 4 | 
            -
            require 'rack'
         | 
| 5 | 
            -
             | 
| 6 | 
            -
            # This example is a bit contrived, but it shows what you could do with the middlewares
         | 
| 7 | 
            -
             | 
| 8 | 
            -
            App = Rack::Builder.new do
         | 
| 9 | 
            -
              use OpenapiFirst::RequestValidation, raise_error: true, spec: File.expand_path('./openapi.yaml', __dir__)
         | 
| 10 | 
            -
              use OpenapiFirst::ResponseValidation
         | 
| 11 | 
            -
             | 
| 12 | 
            -
              handlers = {
         | 
| 13 | 
            -
                'things#index' => ->(_env) { [200, { 'Content-Type' => 'application/json' }, ['{"hello": "world"}']] }
         | 
| 14 | 
            -
              }
         | 
| 15 | 
            -
              not_found = ->(_env) { [404, {}, []] }
         | 
| 16 | 
            -
             | 
| 17 | 
            -
              run ->(env) { handlers.fetch(env[OpenapiFirst::OPERATION].operation_id, not_found).call(env) }
         | 
| 18 | 
            -
            end
         | 
    
        data/examples/config.ru
    DELETED
    
    
    
        data/examples/openapi.yaml
    DELETED
    
    | @@ -1,29 +0,0 @@ | |
| 1 | 
            -
            openapi: 3.0.0
         | 
| 2 | 
            -
            info:
         | 
| 3 | 
            -
              title: "API"
         | 
| 4 | 
            -
              version: "1.0.0"
         | 
| 5 | 
            -
              contact:
         | 
| 6 | 
            -
                name: Contact Name
         | 
| 7 | 
            -
                email: contact@example.com
         | 
| 8 | 
            -
                url: https://example.com/
         | 
| 9 | 
            -
            tags:
         | 
| 10 | 
            -
              - name: Metadata
         | 
| 11 | 
            -
                description: Metadata related requests
         | 
| 12 | 
            -
            paths:
         | 
| 13 | 
            -
              /:
         | 
| 14 | 
            -
                get:
         | 
| 15 | 
            -
                  operationId: things#index
         | 
| 16 | 
            -
                  summary: Get metadata from the root of the API
         | 
| 17 | 
            -
                  tags: ["Metadata"]
         | 
| 18 | 
            -
                  responses:
         | 
| 19 | 
            -
                    "200":
         | 
| 20 | 
            -
                      description: OK
         | 
| 21 | 
            -
                      content:
         | 
| 22 | 
            -
                        application/json:
         | 
| 23 | 
            -
                          schema:
         | 
| 24 | 
            -
                            type: object
         | 
| 25 | 
            -
                            required: [hello]
         | 
| 26 | 
            -
                            properties:
         | 
| 27 | 
            -
                              hello:
         | 
| 28 | 
            -
                                type: string
         | 
| 29 | 
            -
             | 
| @@ -1,170 +0,0 @@ | |
| 1 | 
            -
            # frozen_string_literal: true
         | 
| 2 | 
            -
             | 
| 3 | 
            -
            require 'forwardable'
         | 
| 4 | 
            -
            require 'set'
         | 
| 5 | 
            -
            require_relative 'json_schema'
         | 
| 6 | 
            -
             | 
| 7 | 
            -
            module OpenapiFirst
         | 
| 8 | 
            -
              class Operation # rubocop:disable Metrics/ClassLength
         | 
| 9 | 
            -
                extend Forwardable
         | 
| 10 | 
            -
                def_delegators :operation_object,
         | 
| 11 | 
            -
                               :[],
         | 
| 12 | 
            -
                               :dig
         | 
| 13 | 
            -
             | 
| 14 | 
            -
                WRITE_METHODS = Set.new(%w[post put patch delete]).freeze
         | 
| 15 | 
            -
                private_constant :WRITE_METHODS
         | 
| 16 | 
            -
             | 
| 17 | 
            -
                attr_reader :path, :method, :openapi_version
         | 
| 18 | 
            -
             | 
| 19 | 
            -
                def initialize(path, request_method, path_item_object, openapi_version:)
         | 
| 20 | 
            -
                  @path = path
         | 
| 21 | 
            -
                  @method = request_method
         | 
| 22 | 
            -
                  @path_item_object = path_item_object
         | 
| 23 | 
            -
                  @openapi_version = openapi_version
         | 
| 24 | 
            -
                end
         | 
| 25 | 
            -
             | 
| 26 | 
            -
                def operation_id
         | 
| 27 | 
            -
                  operation_object['operationId']
         | 
| 28 | 
            -
                end
         | 
| 29 | 
            -
             | 
| 30 | 
            -
                def read?
         | 
| 31 | 
            -
                  !write?
         | 
| 32 | 
            -
                end
         | 
| 33 | 
            -
             | 
| 34 | 
            -
                def write?
         | 
| 35 | 
            -
                  WRITE_METHODS.include?(method)
         | 
| 36 | 
            -
                end
         | 
| 37 | 
            -
             | 
| 38 | 
            -
                def request_body
         | 
| 39 | 
            -
                  operation_object['requestBody']
         | 
| 40 | 
            -
                end
         | 
| 41 | 
            -
             | 
| 42 | 
            -
                def response_body_schema(status, content_type)
         | 
| 43 | 
            -
                  content = response_for(status)['content']
         | 
| 44 | 
            -
                  return if content.nil? || content.empty?
         | 
| 45 | 
            -
             | 
| 46 | 
            -
                  raise ResponseInvalid, "Response has no content-type for '#{name}'" unless content_type
         | 
| 47 | 
            -
             | 
| 48 | 
            -
                  media_type = find_content_for_content_type(content, content_type)
         | 
| 49 | 
            -
             | 
| 50 | 
            -
                  unless media_type
         | 
| 51 | 
            -
                    message = "Response content type not found '#{content_type}' for '#{name}'"
         | 
| 52 | 
            -
                    raise ResponseContentTypeNotFoundError, message
         | 
| 53 | 
            -
                  end
         | 
| 54 | 
            -
                  schema = media_type['schema']
         | 
| 55 | 
            -
                  return unless schema
         | 
| 56 | 
            -
             | 
| 57 | 
            -
                  JsonSchema.new(schema, write: false, openapi_version:)
         | 
| 58 | 
            -
                end
         | 
| 59 | 
            -
             | 
| 60 | 
            -
                def request_body_schema(request_content_type)
         | 
| 61 | 
            -
                  (@request_body_schema ||= {})[request_content_type] ||= begin
         | 
| 62 | 
            -
                    content = operation_object.dig('requestBody', 'content')
         | 
| 63 | 
            -
                    media_type = find_content_for_content_type(content, request_content_type)
         | 
| 64 | 
            -
                    schema = media_type&.fetch('schema', nil)
         | 
| 65 | 
            -
                    JsonSchema.new(schema, write: write?, openapi_version:) if schema
         | 
| 66 | 
            -
                  end
         | 
| 67 | 
            -
                end
         | 
| 68 | 
            -
             | 
| 69 | 
            -
                def response_for(status)
         | 
| 70 | 
            -
                  response_content = response_by_code(status)
         | 
| 71 | 
            -
                  return response_content if response_content
         | 
| 72 | 
            -
             | 
| 73 | 
            -
                  message = "Response status code or default not found: #{status} for '#{name}'"
         | 
| 74 | 
            -
                  raise OpenapiFirst::ResponseCodeNotFoundError, message
         | 
| 75 | 
            -
                end
         | 
| 76 | 
            -
             | 
| 77 | 
            -
                def name
         | 
| 78 | 
            -
                  @name ||= "#{method.upcase} #{path} (#{operation_id})"
         | 
| 79 | 
            -
                end
         | 
| 80 | 
            -
             | 
| 81 | 
            -
                def valid_request_content_type?(request_content_type)
         | 
| 82 | 
            -
                  content = operation_object.dig('requestBody', 'content')
         | 
| 83 | 
            -
                  return false unless content
         | 
| 84 | 
            -
             | 
| 85 | 
            -
                  !!find_content_for_content_type(content, request_content_type)
         | 
| 86 | 
            -
                end
         | 
| 87 | 
            -
             | 
| 88 | 
            -
                def query_parameters
         | 
| 89 | 
            -
                  @query_parameters ||= all_parameters.filter { |p| p['in'] == 'query' }
         | 
| 90 | 
            -
                end
         | 
| 91 | 
            -
             | 
| 92 | 
            -
                def path_parameters
         | 
| 93 | 
            -
                  @path_parameters ||= all_parameters.filter { |p| p['in'] == 'path' }
         | 
| 94 | 
            -
                end
         | 
| 95 | 
            -
             | 
| 96 | 
            -
                IGNORED_HEADERS = Set['Content-Type', 'Accept', 'Authorization'].freeze
         | 
| 97 | 
            -
                private_constant :IGNORED_HEADERS
         | 
| 98 | 
            -
             | 
| 99 | 
            -
                def header_parameters
         | 
| 100 | 
            -
                  @header_parameters ||= all_parameters.filter { |p| p['in'] == 'header' && !IGNORED_HEADERS.include?(p['name']) }
         | 
| 101 | 
            -
                end
         | 
| 102 | 
            -
             | 
| 103 | 
            -
                def cookie_parameters
         | 
| 104 | 
            -
                  @cookie_parameters ||= all_parameters.filter { |p| p['in'] == 'cookie' }
         | 
| 105 | 
            -
                end
         | 
| 106 | 
            -
             | 
| 107 | 
            -
                def all_parameters
         | 
| 108 | 
            -
                  @all_parameters ||= begin
         | 
| 109 | 
            -
                    parameters = @path_item_object['parameters']&.dup || []
         | 
| 110 | 
            -
                    parameters_on_operation = operation_object['parameters']
         | 
| 111 | 
            -
                    parameters.concat(parameters_on_operation) if parameters_on_operation
         | 
| 112 | 
            -
                    parameters
         | 
| 113 | 
            -
                  end
         | 
| 114 | 
            -
                end
         | 
| 115 | 
            -
             | 
| 116 | 
            -
                # Return JSON Schema of for all query parameters
         | 
| 117 | 
            -
                def query_parameters_schema
         | 
| 118 | 
            -
                  @query_parameters_schema ||= build_json_schema(query_parameters)
         | 
| 119 | 
            -
                end
         | 
| 120 | 
            -
             | 
| 121 | 
            -
                # Return JSON Schema of for all path parameters
         | 
| 122 | 
            -
                def path_parameters_schema
         | 
| 123 | 
            -
                  @path_parameters_schema ||= build_json_schema(path_parameters)
         | 
| 124 | 
            -
                end
         | 
| 125 | 
            -
             | 
| 126 | 
            -
                def header_parameters_schema
         | 
| 127 | 
            -
                  @header_parameters_schema ||= build_json_schema(header_parameters)
         | 
| 128 | 
            -
                end
         | 
| 129 | 
            -
             | 
| 130 | 
            -
                def cookie_parameters_schema
         | 
| 131 | 
            -
                  @cookie_parameters_schema ||= build_json_schema(cookie_parameters)
         | 
| 132 | 
            -
                end
         | 
| 133 | 
            -
             | 
| 134 | 
            -
                private
         | 
| 135 | 
            -
             | 
| 136 | 
            -
                # Build JSON Schema for given parameter definitions
         | 
| 137 | 
            -
                # @parameter_defs [Array<Hash>] Parameter definitions
         | 
| 138 | 
            -
                def build_json_schema(parameter_defs)
         | 
| 139 | 
            -
                  init_schema = {
         | 
| 140 | 
            -
                    'type' => 'object',
         | 
| 141 | 
            -
                    'properties' => {},
         | 
| 142 | 
            -
                    'required' => []
         | 
| 143 | 
            -
                  }
         | 
| 144 | 
            -
                  schema = parameter_defs.each_with_object(init_schema) do |parameter_def, result|
         | 
| 145 | 
            -
                    parameter = OpenapiParameters::Parameter.new(parameter_def)
         | 
| 146 | 
            -
                    result['properties'][parameter.name] = parameter.schema if parameter.schema
         | 
| 147 | 
            -
                    result['required'] << parameter.name if parameter.required?
         | 
| 148 | 
            -
                  end
         | 
| 149 | 
            -
                  JsonSchema.new(schema, openapi_version:)
         | 
| 150 | 
            -
                end
         | 
| 151 | 
            -
             | 
| 152 | 
            -
                def response_by_code(status)
         | 
| 153 | 
            -
                  operation_object.dig('responses', status.to_s) ||
         | 
| 154 | 
            -
                    operation_object.dig('responses', "#{status / 100}XX") ||
         | 
| 155 | 
            -
                    operation_object.dig('responses', "#{status / 100}xx") ||
         | 
| 156 | 
            -
                    operation_object.dig('responses', 'default')
         | 
| 157 | 
            -
                end
         | 
| 158 | 
            -
             | 
| 159 | 
            -
                def operation_object
         | 
| 160 | 
            -
                  @path_item_object[method]
         | 
| 161 | 
            -
                end
         | 
| 162 | 
            -
             | 
| 163 | 
            -
                def find_content_for_content_type(content, request_content_type)
         | 
| 164 | 
            -
                  content.fetch(request_content_type) do |_|
         | 
| 165 | 
            -
                    type = request_content_type.split(';')[0]
         | 
| 166 | 
            -
                    content[type] || content["#{type.split('/')[0]}/*"] || content['*/*']
         | 
| 167 | 
            -
                  end
         | 
| 168 | 
            -
                end
         | 
| 169 | 
            -
              end
         | 
| 170 | 
            -
            end
         | 
| @@ -1,20 +0,0 @@ | |
| 1 | 
            -
            # frozen_string_literal: true
         | 
| 2 | 
            -
             | 
| 3 | 
            -
            module OpenapiFirst
         | 
| 4 | 
            -
              class StringKeyedHash
         | 
| 5 | 
            -
                extend Forwardable
         | 
| 6 | 
            -
                def_delegators :@orig, :empty?
         | 
| 7 | 
            -
             | 
| 8 | 
            -
                def initialize(original)
         | 
| 9 | 
            -
                  @orig = original
         | 
| 10 | 
            -
                end
         | 
| 11 | 
            -
             | 
| 12 | 
            -
                def key?(key)
         | 
| 13 | 
            -
                  @orig.key?(key.to_sym)
         | 
| 14 | 
            -
                end
         | 
| 15 | 
            -
             | 
| 16 | 
            -
                def [](key)
         | 
| 17 | 
            -
                  @orig[key.to_sym]
         | 
| 18 | 
            -
                end
         | 
| 19 | 
            -
              end
         | 
| 20 | 
            -
            end
         |