openapi_contracts 0.7.1 → 0.9.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/README.md +43 -19
- data/lib/openapi_contracts/doc/operation.rb +27 -0
- data/lib/openapi_contracts/doc/parameter.rb +49 -0
- data/lib/openapi_contracts/doc/path.rb +32 -12
- data/lib/openapi_contracts/doc/pointer.rb +81 -0
- data/lib/openapi_contracts/doc/request.rb +17 -0
- data/lib/openapi_contracts/doc/response.rb +5 -5
- data/lib/openapi_contracts/doc/schema.rb +44 -10
- data/lib/openapi_contracts/doc/with_parameters.rb +9 -0
- data/lib/openapi_contracts/doc.rb +17 -14
- data/lib/openapi_contracts/match.rb +34 -10
- data/lib/openapi_contracts/operation_router.rb +33 -0
- data/lib/openapi_contracts/parser/transformers/base.rb +15 -0
- data/lib/openapi_contracts/parser/transformers/nullable.rb +10 -0
- data/lib/openapi_contracts/parser/transformers/pointer.rb +34 -0
- data/lib/openapi_contracts/parser/transformers.rb +5 -0
- data/lib/openapi_contracts/parser.rb +61 -0
- data/lib/openapi_contracts/payload_parser.rb +39 -0
- data/lib/openapi_contracts/rspec.rb +2 -2
- data/lib/openapi_contracts/validators/base.rb +5 -1
- data/lib/openapi_contracts/validators/documented.rb +12 -5
- data/lib/openapi_contracts/validators/headers.rb +4 -0
- data/lib/openapi_contracts/validators/http_status.rb +2 -6
- data/lib/openapi_contracts/validators/request_body.rb +26 -0
- data/lib/openapi_contracts/validators/response_body.rb +28 -0
- data/lib/openapi_contracts/validators/schema_validation.rb +40 -0
- data/lib/openapi_contracts/validators.rb +9 -6
- data/lib/openapi_contracts.rb +11 -5
- metadata +31 -20
- data/lib/openapi_contracts/doc/file_parser.rb +0 -85
- data/lib/openapi_contracts/doc/method.rb +0 -18
- data/lib/openapi_contracts/doc/parser.rb +0 -44
- data/lib/openapi_contracts/validators/body.rb +0 -38
| @@ -0,0 +1,34 @@ | |
| 1 | 
            +
            module OpenapiContracts::Parser::Transformers
         | 
| 2 | 
            +
              class Pointer < Base
         | 
| 3 | 
            +
                def call(object)
         | 
| 4 | 
            +
                  return unless object['$ref'].present?
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  object['$ref'] = transform_pointer(object['$ref'])
         | 
| 7 | 
            +
                end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                private
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def transform_pointer(target)
         | 
| 12 | 
            +
                  if %r{^#/(?<pointer>.*)} =~ target
         | 
| 13 | 
            +
                    # A JSON Pointer
         | 
| 14 | 
            +
                    generate_absolute_pointer(pointer)
         | 
| 15 | 
            +
                  elsif %r{^(?<relpath>[^#]+)(?:#/(?<pointer>.*))?} =~ target
         | 
| 16 | 
            +
                    ptr = @parser.filenesting[@cwd.join(relpath)]
         | 
| 17 | 
            +
                    tgt = ptr.to_json_pointer
         | 
| 18 | 
            +
                    tgt += "/#{pointer}" if pointer
         | 
| 19 | 
            +
                    tgt
         | 
| 20 | 
            +
                  else
         | 
| 21 | 
            +
                    target
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                # A JSON pointer to the currently parsed file as seen from the root openapi file
         | 
| 26 | 
            +
                def generate_absolute_pointer(json_pointer)
         | 
| 27 | 
            +
                  if @pointer.empty?
         | 
| 28 | 
            +
                    "#/#{json_pointer}"
         | 
| 29 | 
            +
                  else
         | 
| 30 | 
            +
                    "#{@pointer.to_json_pointer}/#{json_pointer}"
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
              end
         | 
| 34 | 
            +
            end
         | 
| @@ -0,0 +1,61 @@ | |
| 1 | 
            +
            module OpenapiContracts
         | 
| 2 | 
            +
              class Parser
         | 
| 3 | 
            +
                autoload :Transformers, 'openapi_contracts/parser/transformers'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                TRANSFORMERS = [Transformers::Nullable, Transformers::Pointer].freeze
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def self.call(dir, filename)
         | 
| 8 | 
            +
                  new(dir.join(filename)).parse
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                attr_reader :filenesting, :rootfile
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def initialize(rootfile)
         | 
| 14 | 
            +
                  @cwd = rootfile.parent
         | 
| 15 | 
            +
                  @rootfile = rootfile
         | 
| 16 | 
            +
                  @filenesting = {}
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                def parse
         | 
| 20 | 
            +
                  @filenesting = build_file_list
         | 
| 21 | 
            +
                  @filenesting.each_with_object({}) do |(path, pointer), schema|
         | 
| 22 | 
            +
                    target = pointer.to_a.reduce(schema) { |d, k| d[k] ||= {} }
         | 
| 23 | 
            +
                    target.delete('$ref') # ref file pointers must be replaced
         | 
| 24 | 
            +
                    target.merge! file_to_data(path, pointer)
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                private
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                def build_file_list
         | 
| 31 | 
            +
                  list = {@rootfile.relative_path_from(@cwd) => Doc::Pointer[]}
         | 
| 32 | 
            +
                  Dir[File.expand_path('components/**/*.yaml', @cwd)].each do |file|
         | 
| 33 | 
            +
                    pathname = Pathname(file).relative_path_from(@cwd)
         | 
| 34 | 
            +
                    pointer = Doc::Pointer.from_path pathname.sub_ext('')
         | 
| 35 | 
            +
                    list.merge! pathname => pointer
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
                  YAML.safe_load_file(@rootfile).fetch('paths') { {} }.each_pair do |k, v|
         | 
| 38 | 
            +
                    next unless v['$ref'] && !v['$ref'].start_with?('#')
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                    list.merge! Pathname(v['$ref']) => Doc::Pointer['paths', k]
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
                  list
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                def file_to_data(pathname, pointer)
         | 
| 46 | 
            +
                  YAML.safe_load_file(@cwd.join(pathname)).tap do |data|
         | 
| 47 | 
            +
                    transform_objects!(data, pathname.parent, pointer)
         | 
| 48 | 
            +
                  end
         | 
| 49 | 
            +
                end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                def transform_objects!(object, cwd, pointer)
         | 
| 52 | 
            +
                  case object
         | 
| 53 | 
            +
                  when Hash
         | 
| 54 | 
            +
                    object.each_value { |v| transform_objects!(v, cwd, pointer) }
         | 
| 55 | 
            +
                    TRANSFORMERS.map { |t| t.new(self, cwd, pointer) }.each { |t| t.call(object) }
         | 
| 56 | 
            +
                  when Array
         | 
| 57 | 
            +
                    object.each { |o| transform_objects!(o, cwd, pointer) }
         | 
| 58 | 
            +
                  end
         | 
| 59 | 
            +
                end
         | 
| 60 | 
            +
              end
         | 
| 61 | 
            +
            end
         | 
| @@ -0,0 +1,39 @@ | |
| 1 | 
            +
            require 'singleton'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module OpenapiContracts
         | 
| 4 | 
            +
              class PayloadParser
         | 
| 5 | 
            +
                include Singleton
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                class << self
         | 
| 8 | 
            +
                  delegate :parse, :register, to: :instance
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                Entry = Struct.new(:matcher, :parser) do
         | 
| 12 | 
            +
                  def call(raw)
         | 
| 13 | 
            +
                    parser.call(raw)
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  def match?(media_type)
         | 
| 17 | 
            +
                    matcher == media_type || matcher.match?(media_type)
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def initialize
         | 
| 22 | 
            +
                  @parsers = []
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                def parse(media_type, payload)
         | 
| 26 | 
            +
                  parser = @parsers.find { |e| e.match?(media_type) }
         | 
| 27 | 
            +
                  raise ArgumentError, "#{media_type.inspect} is not supported yet" unless parser
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  parser.call(payload)
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                def register(matcher, parser)
         | 
| 33 | 
            +
                  @parsers << Entry.new(matcher, parser)
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
              end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
              PayloadParser.register(%r{(/|\+)json$}, ->(raw) { JSON(raw) })
         | 
| 38 | 
            +
              PayloadParser.register('application/x-www-form-urlencoded', ->(raw) { Rack::Utils.parse_nested_query(raw) })
         | 
| 39 | 
            +
            end
         | 
| @@ -14,12 +14,12 @@ RSpec::Matchers.define :match_openapi_doc do |doc, options = {}| # rubocop:disab | |
| 14 14 | 
             
              end
         | 
| 15 15 |  | 
| 16 16 | 
             
              description do
         | 
| 17 | 
            -
                desc = ' | 
| 17 | 
            +
                desc = 'match the openapi schema'
         | 
| 18 18 | 
             
                desc << " with #{http_status_desc(@status)}" if @status
         | 
| 19 19 | 
             
                desc
         | 
| 20 20 | 
             
              end
         | 
| 21 21 |  | 
| 22 | 
            -
              failure_message do | | 
| 22 | 
            +
              failure_message do |_response|
         | 
| 23 23 | 
             
                @errors.map { |e| "* #{e}" }.join("\n")
         | 
| 24 24 | 
             
              end
         | 
| 25 25 |  | 
| @@ -22,7 +22,11 @@ module OpenapiContracts::Validators | |
| 22 22 |  | 
| 23 23 | 
             
                private
         | 
| 24 24 |  | 
| 25 | 
            -
                delegate : | 
| 25 | 
            +
                delegate :operation, :options, :request, :response, to: :@env
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                def response_desc
         | 
| 28 | 
            +
                  "#{request.request_method} #{request.path}"
         | 
| 29 | 
            +
                end
         | 
| 26 30 |  | 
| 27 31 | 
             
                # :nocov:
         | 
| 28 32 | 
             
                def validate
         | 
| @@ -1,18 +1,25 @@ | |
| 1 1 | 
             
            module OpenapiContracts::Validators
         | 
| 2 | 
            +
              # Purpose of this validator
         | 
| 3 | 
            +
              # * ensure the operation is documented (combination http-method + path)
         | 
| 4 | 
            +
              # * ensure the response-status is documented on the operation
         | 
| 2 5 | 
             
              class Documented < Base
         | 
| 3 6 | 
             
                self.final = true
         | 
| 4 7 |  | 
| 5 8 | 
             
                private
         | 
| 6 9 |  | 
| 7 10 | 
             
                def validate
         | 
| 8 | 
            -
                  return  | 
| 11 | 
            +
                  return operation_missing unless operation
         | 
| 9 12 |  | 
| 10 | 
            -
                   | 
| 11 | 
            -
             | 
| 13 | 
            +
                  response_missing unless operation.response_for_status(response.status)
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                def operation_missing
         | 
| 17 | 
            +
                  @errors << "Undocumented operation for #{response_desc.inspect}"
         | 
| 12 18 | 
             
                end
         | 
| 13 19 |  | 
| 14 | 
            -
                def  | 
| 15 | 
            -
                   | 
| 20 | 
            +
                def response_missing
         | 
| 21 | 
            +
                  status_desc = http_status_desc(response.status)
         | 
| 22 | 
            +
                  @errors << "Undocumented response for #{response_desc.inspect} with #{status_desc}"
         | 
| 16 23 | 
             
                end
         | 
| 17 24 | 
             
              end
         | 
| 18 25 | 
             
            end
         | 
| @@ -5,13 +5,9 @@ module OpenapiContracts::Validators | |
| 5 5 | 
             
                private
         | 
| 6 6 |  | 
| 7 7 | 
             
                def validate
         | 
| 8 | 
            -
                  return if  | 
| 8 | 
            +
                  return if options[:status] == response.status
         | 
| 9 9 |  | 
| 10 | 
            -
                  @errors << "Response has #{http_status_desc}"
         | 
| 11 | 
            -
                end
         | 
| 12 | 
            -
             | 
| 13 | 
            -
                def http_status_desc
         | 
| 14 | 
            -
                  "http status #{Rack::Utils::HTTP_STATUS_CODES[response.status]} (#{response.status})"
         | 
| 10 | 
            +
                  @errors << "Response has #{http_status_desc(response.status)}"
         | 
| 15 11 | 
             
                end
         | 
| 16 12 | 
             
              end
         | 
| 17 13 | 
             
            end
         | 
| @@ -0,0 +1,26 @@ | |
| 1 | 
            +
            module OpenapiContracts::Validators
         | 
| 2 | 
            +
              class RequestBody < Base
         | 
| 3 | 
            +
                include SchemaValidation
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                private
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                delegate :media_type, to: :request
         | 
| 8 | 
            +
                delegate :request_body, to: :operation
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                def data_for_validation
         | 
| 11 | 
            +
                  request.body.rewind
         | 
| 12 | 
            +
                  raw = request.body.read
         | 
| 13 | 
            +
                  OpenapiContracts::PayloadParser.parse(media_type, raw)
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                def validate
         | 
| 17 | 
            +
                  if !request_body
         | 
| 18 | 
            +
                    @errors << "Undocumented request body for #{response_desc.inspect}"
         | 
| 19 | 
            +
                  elsif !request_body.supports_media_type?(media_type)
         | 
| 20 | 
            +
                    @errors << "Undocumented request with media-type #{media_type.inspect}"
         | 
| 21 | 
            +
                  else
         | 
| 22 | 
            +
                    @errors += validate_schema(request_body.schema_for(media_type), data_for_validation)
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
              end
         | 
| 26 | 
            +
            end
         | 
| @@ -0,0 +1,28 @@ | |
| 1 | 
            +
            module OpenapiContracts::Validators
         | 
| 2 | 
            +
              class ResponseBody < Base
         | 
| 3 | 
            +
                include SchemaValidation
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                private
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                delegate :media_type, to: :response
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                def data_for_validation
         | 
| 10 | 
            +
                  # ActionDispatch::Response body is a plain string, while Rack::Response returns an array
         | 
| 11 | 
            +
                  OpenapiContracts::PayloadParser.parse(media_type, Array.wrap(response.body).join)
         | 
| 12 | 
            +
                end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                def spec
         | 
| 15 | 
            +
                  @spec ||= operation.response_for_status(response.status)
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                def validate
         | 
| 19 | 
            +
                  if spec.no_content?
         | 
| 20 | 
            +
                    @errors << 'Expected empty response body' if Array.wrap(response.body).any?(&:present?)
         | 
| 21 | 
            +
                  elsif !spec.supports_media_type?(media_type)
         | 
| 22 | 
            +
                    @errors << "Undocumented response with content-type #{media_type.inspect}"
         | 
| 23 | 
            +
                  else
         | 
| 24 | 
            +
                    @errors += validate_schema(spec.schema_for(media_type), data_for_validation)
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
              end
         | 
| 28 | 
            +
            end
         | 
| @@ -0,0 +1,40 @@ | |
| 1 | 
            +
            module OpenapiContracts::Validators
         | 
| 2 | 
            +
              module SchemaValidation
         | 
| 3 | 
            +
                module_function
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                def build_validation_schema(schema)
         | 
| 6 | 
            +
                  schema.raw.merge(
         | 
| 7 | 
            +
                    '$ref'    => schema.fragment,
         | 
| 8 | 
            +
                    '$schema' => schema_draft_version(schema)
         | 
| 9 | 
            +
                  )
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                def error_to_message(error)
         | 
| 13 | 
            +
                  pointer = " at #{error['data_pointer']}" if error['data_pointer'].present?
         | 
| 14 | 
            +
                  if error.key?('details')
         | 
| 15 | 
            +
                    error['details'].to_a.map { |(key, val)|
         | 
| 16 | 
            +
                      "#{key.humanize}: #{val}#{pointer}"
         | 
| 17 | 
            +
                    }.to_sentence
         | 
| 18 | 
            +
                  else
         | 
| 19 | 
            +
                    "#{error['data'].inspect}#{pointer} does not match the schema"
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                def schema_draft_version(schema)
         | 
| 24 | 
            +
                  if schema.openapi_version.blank? || schema.openapi_version < Gem::Version.new('3.1')
         | 
| 25 | 
            +
                    # Closest compatible version is actually draft 5 but not supported by json-schemer
         | 
| 26 | 
            +
                    'http://json-schema.org/draft-04/schema#'
         | 
| 27 | 
            +
                  else
         | 
| 28 | 
            +
                    # >= 3.1 is actually comptable with 2020-12 but not yet supported by json-schemer
         | 
| 29 | 
            +
                    'http://json-schema.org/draft-07/schema#'
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                def validate_schema(schema, data)
         | 
| 34 | 
            +
                  schemer = JSONSchemer.schema(build_validation_schema(schema))
         | 
| 35 | 
            +
                  schemer.validate(data).map do |err|
         | 
| 36 | 
            +
                    error_to_message(err)
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
              end
         | 
| 40 | 
            +
            end
         | 
| @@ -1,16 +1,19 @@ | |
| 1 1 | 
             
            module OpenapiContracts
         | 
| 2 2 | 
             
              module Validators
         | 
| 3 | 
            -
                autoload :Base, | 
| 4 | 
            -
                autoload : | 
| 5 | 
            -
                autoload : | 
| 6 | 
            -
                autoload : | 
| 7 | 
            -
                autoload : | 
| 3 | 
            +
                autoload :Base,             'openapi_contracts/validators/base'
         | 
| 4 | 
            +
                autoload :Documented,       'openapi_contracts/validators/documented'
         | 
| 5 | 
            +
                autoload :Headers,          'openapi_contracts/validators/headers'
         | 
| 6 | 
            +
                autoload :HttpStatus,       'openapi_contracts/validators/http_status'
         | 
| 7 | 
            +
                autoload :RequestBody,      'openapi_contracts/validators/request_body'
         | 
| 8 | 
            +
                autoload :ResponseBody,     'openapi_contracts/validators/response_body'
         | 
| 9 | 
            +
                autoload :SchemaValidation, 'openapi_contracts/validators/schema_validation'
         | 
| 8 10 |  | 
| 9 11 | 
             
                # Defines order of matching
         | 
| 10 12 | 
             
                ALL = [
         | 
| 11 13 | 
             
                  Documented,
         | 
| 12 14 | 
             
                  HttpStatus,
         | 
| 13 | 
            -
                   | 
| 15 | 
            +
                  RequestBody,
         | 
| 16 | 
            +
                  ResponseBody,
         | 
| 14 17 | 
             
                  Headers
         | 
| 15 18 | 
             
                ].freeze
         | 
| 16 19 | 
             
              end
         | 
    
        data/lib/openapi_contracts.rb
    CHANGED
    
    | @@ -1,19 +1,25 @@ | |
| 1 1 | 
             
            require 'active_support'
         | 
| 2 2 | 
             
            require 'active_support/core_ext/array'
         | 
| 3 | 
            +
            require 'active_support/core_ext/hash'
         | 
| 3 4 | 
             
            require 'active_support/core_ext/class'
         | 
| 4 5 | 
             
            require 'active_support/core_ext/module'
         | 
| 5 6 | 
             
            require 'active_support/core_ext/string'
         | 
| 7 | 
            +
            require 'rubygems/version'
         | 
| 6 8 |  | 
| 7 9 | 
             
            require 'json_schemer'
         | 
| 10 | 
            +
            require 'rack'
         | 
| 8 11 | 
             
            require 'yaml'
         | 
| 9 12 |  | 
| 10 13 | 
             
            module OpenapiContracts
         | 
| 11 | 
            -
              autoload :Doc, | 
| 12 | 
            -
              autoload :Helper, | 
| 13 | 
            -
              autoload :Match, | 
| 14 | 
            -
              autoload : | 
| 14 | 
            +
              autoload :Doc,             'openapi_contracts/doc'
         | 
| 15 | 
            +
              autoload :Helper,          'openapi_contracts/helper'
         | 
| 16 | 
            +
              autoload :Match,           'openapi_contracts/match'
         | 
| 17 | 
            +
              autoload :OperationRouter, 'openapi_contracts/operation_router'
         | 
| 18 | 
            +
              autoload :Parser,          'openapi_contracts/parser'
         | 
| 19 | 
            +
              autoload :PayloadParser,   'openapi_contracts/payload_parser'
         | 
| 20 | 
            +
              autoload :Validators,      'openapi_contracts/validators'
         | 
| 15 21 |  | 
| 16 | 
            -
              Env = Struct.new(: | 
| 22 | 
            +
              Env = Struct.new(:operation, :options, :request, :response, keyword_init: true)
         | 
| 17 23 |  | 
| 18 24 | 
             
              module_function
         | 
| 19 25 |  | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: openapi_contracts
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0. | 
| 4 | 
            +
              version: 0.9.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - mkon
         | 
| 8 8 | 
             
            autorequire:
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2023- | 
| 11 | 
            +
            date: 2023-08-03 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: activesupport
         | 
| @@ -36,42 +36,42 @@ dependencies: | |
| 36 36 | 
             
                requirements:
         | 
| 37 37 | 
             
                - - "~>"
         | 
| 38 38 | 
             
                  - !ruby/object:Gem::Version
         | 
| 39 | 
            -
                    version: 0. | 
| 39 | 
            +
                    version: 1.0.3
         | 
| 40 40 | 
             
              type: :runtime
         | 
| 41 41 | 
             
              prerelease: false
         | 
| 42 42 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 43 43 | 
             
                requirements:
         | 
| 44 44 | 
             
                - - "~>"
         | 
| 45 45 | 
             
                  - !ruby/object:Gem::Version
         | 
| 46 | 
            -
                    version: 0. | 
| 46 | 
            +
                    version: 1.0.3
         | 
| 47 47 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 48 | 
            -
              name:  | 
| 48 | 
            +
              name: rack
         | 
| 49 49 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| 50 50 | 
             
                requirements:
         | 
| 51 | 
            -
                - - " | 
| 51 | 
            +
                - - ">="
         | 
| 52 52 | 
             
                  - !ruby/object:Gem::Version
         | 
| 53 | 
            -
                    version:  | 
| 54 | 
            -
              type: : | 
| 53 | 
            +
                    version: 2.0.0
         | 
| 54 | 
            +
              type: :runtime
         | 
| 55 55 | 
             
              prerelease: false
         | 
| 56 56 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 57 57 | 
             
                requirements:
         | 
| 58 | 
            -
                - - " | 
| 58 | 
            +
                - - ">="
         | 
| 59 59 | 
             
                  - !ruby/object:Gem::Version
         | 
| 60 | 
            -
                    version:  | 
| 60 | 
            +
                    version: 2.0.0
         | 
| 61 61 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 62 | 
            -
              name:  | 
| 62 | 
            +
              name: json_spec
         | 
| 63 63 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| 64 64 | 
             
                requirements:
         | 
| 65 65 | 
             
                - - "~>"
         | 
| 66 66 | 
             
                  - !ruby/object:Gem::Version
         | 
| 67 | 
            -
                    version:  | 
| 67 | 
            +
                    version: 1.1.5
         | 
| 68 68 | 
             
              type: :development
         | 
| 69 69 | 
             
              prerelease: false
         | 
| 70 70 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 71 71 | 
             
                requirements:
         | 
| 72 72 | 
             
                - - "~>"
         | 
| 73 73 | 
             
                  - !ruby/object:Gem::Version
         | 
| 74 | 
            -
                    version:  | 
| 74 | 
            +
                    version: 1.1.5
         | 
| 75 75 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 76 76 | 
             
              name: rspec
         | 
| 77 77 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| @@ -92,14 +92,14 @@ dependencies: | |
| 92 92 | 
             
                requirements:
         | 
| 93 93 | 
             
                - - '='
         | 
| 94 94 | 
             
                  - !ruby/object:Gem::Version
         | 
| 95 | 
            -
                    version: 1. | 
| 95 | 
            +
                    version: 1.54.1
         | 
| 96 96 | 
             
              type: :development
         | 
| 97 97 | 
             
              prerelease: false
         | 
| 98 98 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 99 99 | 
             
                requirements:
         | 
| 100 100 | 
             
                - - '='
         | 
| 101 101 | 
             
                  - !ruby/object:Gem::Version
         | 
| 102 | 
            -
                    version: 1. | 
| 102 | 
            +
                    version: 1.54.1
         | 
| 103 103 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 104 104 | 
             
              name: rubocop-rspec
         | 
| 105 105 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| @@ -138,22 +138,33 @@ files: | |
| 138 138 | 
             
            - README.md
         | 
| 139 139 | 
             
            - lib/openapi_contracts.rb
         | 
| 140 140 | 
             
            - lib/openapi_contracts/doc.rb
         | 
| 141 | 
            -
            - lib/openapi_contracts/doc/file_parser.rb
         | 
| 142 141 | 
             
            - lib/openapi_contracts/doc/header.rb
         | 
| 143 | 
            -
            - lib/openapi_contracts/doc/ | 
| 144 | 
            -
            - lib/openapi_contracts/doc/ | 
| 142 | 
            +
            - lib/openapi_contracts/doc/operation.rb
         | 
| 143 | 
            +
            - lib/openapi_contracts/doc/parameter.rb
         | 
| 145 144 | 
             
            - lib/openapi_contracts/doc/path.rb
         | 
| 145 | 
            +
            - lib/openapi_contracts/doc/pointer.rb
         | 
| 146 | 
            +
            - lib/openapi_contracts/doc/request.rb
         | 
| 146 147 | 
             
            - lib/openapi_contracts/doc/response.rb
         | 
| 147 148 | 
             
            - lib/openapi_contracts/doc/schema.rb
         | 
| 149 | 
            +
            - lib/openapi_contracts/doc/with_parameters.rb
         | 
| 148 150 | 
             
            - lib/openapi_contracts/helper.rb
         | 
| 149 151 | 
             
            - lib/openapi_contracts/match.rb
         | 
| 152 | 
            +
            - lib/openapi_contracts/operation_router.rb
         | 
| 153 | 
            +
            - lib/openapi_contracts/parser.rb
         | 
| 154 | 
            +
            - lib/openapi_contracts/parser/transformers.rb
         | 
| 155 | 
            +
            - lib/openapi_contracts/parser/transformers/base.rb
         | 
| 156 | 
            +
            - lib/openapi_contracts/parser/transformers/nullable.rb
         | 
| 157 | 
            +
            - lib/openapi_contracts/parser/transformers/pointer.rb
         | 
| 158 | 
            +
            - lib/openapi_contracts/payload_parser.rb
         | 
| 150 159 | 
             
            - lib/openapi_contracts/rspec.rb
         | 
| 151 160 | 
             
            - lib/openapi_contracts/validators.rb
         | 
| 152 161 | 
             
            - lib/openapi_contracts/validators/base.rb
         | 
| 153 | 
            -
            - lib/openapi_contracts/validators/body.rb
         | 
| 154 162 | 
             
            - lib/openapi_contracts/validators/documented.rb
         | 
| 155 163 | 
             
            - lib/openapi_contracts/validators/headers.rb
         | 
| 156 164 | 
             
            - lib/openapi_contracts/validators/http_status.rb
         | 
| 165 | 
            +
            - lib/openapi_contracts/validators/request_body.rb
         | 
| 166 | 
            +
            - lib/openapi_contracts/validators/response_body.rb
         | 
| 167 | 
            +
            - lib/openapi_contracts/validators/schema_validation.rb
         | 
| 157 168 | 
             
            homepage: https://github.com/mkon/openapi_contracts
         | 
| 158 169 | 
             
            licenses:
         | 
| 159 170 | 
             
            - MIT
         | 
| @@ -167,7 +178,7 @@ required_ruby_version: !ruby/object:Gem::Requirement | |
| 167 178 | 
             
              requirements:
         | 
| 168 179 | 
             
              - - ">="
         | 
| 169 180 | 
             
                - !ruby/object:Gem::Version
         | 
| 170 | 
            -
                  version: ' | 
| 181 | 
            +
                  version: '3.0'
         | 
| 171 182 | 
             
              - - "<"
         | 
| 172 183 | 
             
                - !ruby/object:Gem::Version
         | 
| 173 184 | 
             
                  version: '3.3'
         | 
| @@ -1,85 +0,0 @@ | |
| 1 | 
            -
            module OpenapiContracts
         | 
| 2 | 
            -
              class Doc::FileParser
         | 
| 3 | 
            -
                Result = Struct.new(:data, :path) do
         | 
| 4 | 
            -
                  def to_mergable_hash
         | 
| 5 | 
            -
                    d = data
         | 
| 6 | 
            -
                    path.ascend do |p|
         | 
| 7 | 
            -
                      d = {p.basename.to_s => d}
         | 
| 8 | 
            -
                    end
         | 
| 9 | 
            -
                    d
         | 
| 10 | 
            -
                  end
         | 
| 11 | 
            -
                end
         | 
| 12 | 
            -
             | 
| 13 | 
            -
                def self.parse(rootfile, pathname)
         | 
| 14 | 
            -
                  new(rootfile, pathname).call
         | 
| 15 | 
            -
                end
         | 
| 16 | 
            -
             | 
| 17 | 
            -
                def initialize(rootfile, pathname)
         | 
| 18 | 
            -
                  @root = rootfile.parent
         | 
| 19 | 
            -
                  @rootfile = rootfile
         | 
| 20 | 
            -
                  @pathname = pathname.relative? ? @root.join(pathname) : pathname
         | 
| 21 | 
            -
                end
         | 
| 22 | 
            -
             | 
| 23 | 
            -
                def call
         | 
| 24 | 
            -
                  schema = YAML.safe_load(File.read(@pathname))
         | 
| 25 | 
            -
                  schema = transform_hash(schema)
         | 
| 26 | 
            -
                  Result.new(schema, @pathname.relative_path_from(@root).sub_ext(''))
         | 
| 27 | 
            -
                end
         | 
| 28 | 
            -
             | 
| 29 | 
            -
                private
         | 
| 30 | 
            -
             | 
| 31 | 
            -
                def transform_hash(data)
         | 
| 32 | 
            -
                  data.each_with_object({}) do |(key, val), m|
         | 
| 33 | 
            -
                    if val.is_a?(Array)
         | 
| 34 | 
            -
                      m[key] = transform_array(val)
         | 
| 35 | 
            -
                    elsif val.is_a?(Hash)
         | 
| 36 | 
            -
                      m[key] = transform_hash(val)
         | 
| 37 | 
            -
                    elsif key == '$ref'
         | 
| 38 | 
            -
                      m.merge! transform_pointer(key, val)
         | 
| 39 | 
            -
                    else
         | 
| 40 | 
            -
                      m[key] = val
         | 
| 41 | 
            -
                    end
         | 
| 42 | 
            -
                  end
         | 
| 43 | 
            -
                end
         | 
| 44 | 
            -
             | 
| 45 | 
            -
                def transform_array(data)
         | 
| 46 | 
            -
                  data.each_with_object([]) do |item, m|
         | 
| 47 | 
            -
                    case item
         | 
| 48 | 
            -
                    when Hash
         | 
| 49 | 
            -
                      m.push transform_hash(item)
         | 
| 50 | 
            -
                    when Array
         | 
| 51 | 
            -
                      m.push transform_array(item)
         | 
| 52 | 
            -
                    else
         | 
| 53 | 
            -
                      m.push item
         | 
| 54 | 
            -
                    end
         | 
| 55 | 
            -
                  end
         | 
| 56 | 
            -
                end
         | 
| 57 | 
            -
             | 
| 58 | 
            -
                def transform_pointer(key, target)
         | 
| 59 | 
            -
                  if %r{^#/(?<pointer>.*)} =~ target
         | 
| 60 | 
            -
                    # A JSON Pointer
         | 
| 61 | 
            -
                    {key => generate_absolute_pointer(pointer)}
         | 
| 62 | 
            -
                  elsif %r{^(?<relpath>[^#]+)(?:#/(?<pointer>.*))?} =~ target
         | 
| 63 | 
            -
                    if relpath.start_with?('paths') # path description file pointer
         | 
| 64 | 
            -
                      # Inline the file contents
         | 
| 65 | 
            -
                      self.class.parse(@rootfile, Pathname(relpath)).data
         | 
| 66 | 
            -
                    else # A file pointer with potential JSON sub-pointer
         | 
| 67 | 
            -
                      tgt = @pathname.parent.relative_path_from(@root).join(relpath).sub_ext('')
         | 
| 68 | 
            -
                      tgt = tgt.join(pointer) if pointer
         | 
| 69 | 
            -
                      {key => "#/#{tgt}"}
         | 
| 70 | 
            -
                    end
         | 
| 71 | 
            -
                  else
         | 
| 72 | 
            -
                    {key => target}
         | 
| 73 | 
            -
                  end
         | 
| 74 | 
            -
                end
         | 
| 75 | 
            -
             | 
| 76 | 
            -
                # A JSON pointer to the currently parsed file as seen from the root openapi file
         | 
| 77 | 
            -
                def generate_absolute_pointer(json_pointer)
         | 
| 78 | 
            -
                  if @rootfile == @pathname
         | 
| 79 | 
            -
                    "#/#{json_pointer}"
         | 
| 80 | 
            -
                  else
         | 
| 81 | 
            -
                    "#/#{@pathname.relative_path_from(@root).sub_ext('').join(json_pointer)}"
         | 
| 82 | 
            -
                  end
         | 
| 83 | 
            -
                end
         | 
| 84 | 
            -
              end
         | 
| 85 | 
            -
            end
         | 
| @@ -1,18 +0,0 @@ | |
| 1 | 
            -
            module OpenapiContracts
         | 
| 2 | 
            -
              class Doc::Method
         | 
| 3 | 
            -
                def initialize(schema)
         | 
| 4 | 
            -
                  @schema = schema
         | 
| 5 | 
            -
                  @responses = schema['responses'].to_h do |status, _|
         | 
| 6 | 
            -
                    [status, Doc::Response.new(schema.navigate('responses', status))]
         | 
| 7 | 
            -
                  end
         | 
| 8 | 
            -
                end
         | 
| 9 | 
            -
             | 
| 10 | 
            -
                def responses
         | 
| 11 | 
            -
                  @responses.each_value
         | 
| 12 | 
            -
                end
         | 
| 13 | 
            -
             | 
| 14 | 
            -
                def with_status(status)
         | 
| 15 | 
            -
                  @responses[status]
         | 
| 16 | 
            -
                end
         | 
| 17 | 
            -
              end
         | 
| 18 | 
            -
            end
         | 
| @@ -1,44 +0,0 @@ | |
| 1 | 
            -
            module OpenapiContracts
         | 
| 2 | 
            -
              class Doc::Parser
         | 
| 3 | 
            -
                def self.call(dir, filename)
         | 
| 4 | 
            -
                  new(dir.join(filename)).parse
         | 
| 5 | 
            -
                end
         | 
| 6 | 
            -
             | 
| 7 | 
            -
                def initialize(rootfile)
         | 
| 8 | 
            -
                  @rootfile = rootfile
         | 
| 9 | 
            -
                end
         | 
| 10 | 
            -
             | 
| 11 | 
            -
                def parse
         | 
| 12 | 
            -
                  file = Doc::FileParser.parse(@rootfile, @rootfile)
         | 
| 13 | 
            -
                  data = file.data
         | 
| 14 | 
            -
                  data.deep_merge! merge_components
         | 
| 15 | 
            -
                  nullable_to_type!(data)
         | 
| 16 | 
            -
                  # debugger
         | 
| 17 | 
            -
                end
         | 
| 18 | 
            -
             | 
| 19 | 
            -
                private
         | 
| 20 | 
            -
             | 
| 21 | 
            -
                def merge_components
         | 
| 22 | 
            -
                  data = {}
         | 
| 23 | 
            -
                  Dir[File.expand_path('components/**/*.yaml', @rootfile.parent)].each do |file|
         | 
| 24 | 
            -
                    result = Doc::FileParser.parse(@rootfile, Pathname(file))
         | 
| 25 | 
            -
                    data.deep_merge!(result.to_mergable_hash)
         | 
| 26 | 
            -
                  end
         | 
| 27 | 
            -
                  data
         | 
| 28 | 
            -
                end
         | 
| 29 | 
            -
             | 
| 30 | 
            -
                def nullable_to_type!(object)
         | 
| 31 | 
            -
                  case object
         | 
| 32 | 
            -
                  when Hash
         | 
| 33 | 
            -
                    if object['type'] && object['nullable']
         | 
| 34 | 
            -
                      object['type'] = [object['type'], 'null']
         | 
| 35 | 
            -
                      object.delete 'nullable'
         | 
| 36 | 
            -
                    else
         | 
| 37 | 
            -
                      object.each_value { |o| nullable_to_type! o }
         | 
| 38 | 
            -
                    end
         | 
| 39 | 
            -
                  when Array
         | 
| 40 | 
            -
                    object.each { |o| nullable_to_type! o }
         | 
| 41 | 
            -
                  end
         | 
| 42 | 
            -
                end
         | 
| 43 | 
            -
              end
         | 
| 44 | 
            -
            end
         |