Skip to content

Commit

Permalink
Parameter enum (#58)
Browse files Browse the repository at this point in the history
* Consider all parameters for routing

* Rename Method to Operation

* Cleanup routing

* DRY schema validation

* Rename validators

* improve message without pointer

* Correctly name media_type

* Fail on non-json payloads

* Add PayloadParser
  • Loading branch information
mkon authored Jul 19, 2023
1 parent d145dd9 commit 950915a
Show file tree
Hide file tree
Showing 37 changed files with 509 additions and 389 deletions.
14 changes: 8 additions & 6 deletions lib/openapi_contracts.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@
require 'active_support/core_ext/string'

require 'json_schemer'
require 'rack'
require 'yaml'

module OpenapiContracts
autoload :Doc, 'openapi_contracts/doc'
autoload :Helper, 'openapi_contracts/helper'
autoload :Match, 'openapi_contracts/match'
autoload :Validators, 'openapi_contracts/validators'
autoload :Doc, 'openapi_contracts/doc'
autoload :Helper, 'openapi_contracts/helper'
autoload :Match, 'openapi_contracts/match'
autoload :OperationRouter, 'openapi_contracts/operation_router'
autoload :PayloadParser, 'openapi_contracts/payload_parser'
autoload :Validators, 'openapi_contracts/validators'

Env = Struct.new(:spec, :response, :request, :expected_status, :match_request_body?, :request_body,
keyword_init: true)
Env = Struct.new(:operation, :options, :request, :response, keyword_init: true)

module_function

Expand Down
37 changes: 15 additions & 22 deletions lib/openapi_contracts/doc.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
module OpenapiContracts
class Doc
autoload :Header, 'openapi_contracts/doc/header'
autoload :FileParser, 'openapi_contracts/doc/file_parser'
autoload :Method, 'openapi_contracts/doc/method'
autoload :Parser, 'openapi_contracts/doc/parser'
autoload :Parameter, 'openapi_contracts/doc/parameter'
autoload :Path, 'openapi_contracts/doc/path'
autoload :Request, 'openapi_contracts/doc/request'
autoload :Response, 'openapi_contracts/doc/response'
autoload :Schema, 'openapi_contracts/doc/schema'
autoload :Header, 'openapi_contracts/doc/header'
autoload :FileParser, 'openapi_contracts/doc/file_parser'
autoload :Operation, 'openapi_contracts/doc/operation'
autoload :Parser, 'openapi_contracts/doc/parser'
autoload :Parameter, 'openapi_contracts/doc/parameter'
autoload :Path, 'openapi_contracts/doc/path'
autoload :Request, 'openapi_contracts/doc/request'
autoload :Response, 'openapi_contracts/doc/response'
autoload :Schema, 'openapi_contracts/doc/schema'
autoload :WithParameters, 'openapi_contracts/doc/with_parameters'

def self.parse(dir, filename = 'openapi.yaml')
new Parser.call(dir, filename)
Expand All @@ -29,31 +30,23 @@ def paths
@paths.each_value
end

def response_for(path, method, status)
with_path(path)&.with_method(method)&.with_status(status)
end

def request_for(path, method)
with_path(path)&.with_method(method)&.request_body
def operation_for(path, method)
OperationRouter.new(self).route(path, method.downcase)
end

# Returns an Enumerator over all Responses
def responses(&block)
return enum_for(:responses) unless block_given?

paths.each do |path|
path.methods.each do |method|
method.responses.each(&block)
path.operations.each do |operation|
operation.responses.each(&block)
end
end
end

def with_path(path)
if @paths.key?(path)
@paths[path]
else
@dynamic_paths.find { |p| p.matches?(path) }
end
@paths[path]
end
end
end
21 changes: 0 additions & 21 deletions lib/openapi_contracts/doc/method.rb

This file was deleted.

27 changes: 27 additions & 0 deletions lib/openapi_contracts/doc/operation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module OpenapiContracts
class Doc::Operation
include Doc::WithParameters

def initialize(path, spec)
@path = path
@spec = spec
@responses = spec.navigate('responses').each.to_h do |status, subspec| # rubocop:disable Style/HashTransformValues
[status, Doc::Response.new(subspec)]
end
end

def request_body
return @request_body if instance_variable_defined?(:@request_body)

@request_body = @spec.navigate('requestBody')&.then { |s| Doc::Request.new(s) }
end

def responses
@responses.each_value
end

def response_for_status(status)
@responses[status.to_s]
end
end
end
12 changes: 8 additions & 4 deletions lib/openapi_contracts/doc/parameter.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
module OpenapiContracts
class Doc::Parameter
attr_reader :schema
attr_reader :name, :in, :schema

def initialize(spec)
@spec = spec
options = spec.to_h
@name = options[:name]
@in = options[:in]
@required = options[:required]
@name = options['name']
@in = options['in']
@required = options['required']
end

def in_path?
@in == 'path'
end

def matches?(value)
Expand Down
59 changes: 21 additions & 38 deletions lib/openapi_contracts/doc/path.rb
Original file line number Diff line number Diff line change
@@ -1,61 +1,44 @@
module OpenapiContracts
class Doc::Path
def initialize(path, schema)
@path = path
@schema = schema
include Doc::WithParameters

@methods = (known_http_methods & @schema.keys).to_h do |method|
[method, Doc::Method.new(@schema.navigate(method))]
end
HTTP_METHODS = %w(get head post put delete connect options trace patch).freeze

def initialize(path, spec)
@path = path
@spec = spec
@supported_methods = HTTP_METHODS & @spec.keys
end

def dynamic?
@path.include?('{')
end

def matches?(path)
@path == path || regexp_path.match(path) do |m|
m.named_captures.each do |k, v|
return false unless parameter_matches?(k, v)
end
true
end
def operations
@supported_methods.each.lazy.map { |m| Doc::Operation.new(self, @spec.navigate(m)) }
end

def methods
@methods.each_value
def path_regexp
@path_regexp ||= begin
re = /\{(\S+)\}/
@path.gsub(re) { |placeholder|
placeholder.match(re) { |m| "(?<#{m[1]}>[^/]*)" }
}.then { |str| Regexp.new(str) }
end
end

def static?
!dynamic?
end

def with_method(method)
@methods[method]
def supports_method?(method)
@supported_methods.include?(method)
end

private

def parameter_matches?(name, value)
parameter = Array.wrap(@schema['parameters'])
.map.with_index { |_spec, index| @schema.navigate('parameters', index.to_s).follow_refs }
.find { |s| s['name'] == name && s['in'] == 'path' }
&.then { |s| Doc::Parameter.new(s) }
return false unless parameter

parameter.matches?(value)
end

def regexp_path
re = /\{(\S+)\}/
@path.gsub(re) { |placeholder|
placeholder.match(re) { |m| "(?<#{m[1]}>[^/]*)" }
}.then { |str| Regexp.new(str) }
end
def with_method(method)
return unless supports_method?(method)

def known_http_methods
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
%w(get head post put delete connect options trace patch).freeze
Doc::Operation.new(self, @spec.navigate(method))
end
end
end
10 changes: 5 additions & 5 deletions lib/openapi_contracts/doc/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ def initialize(schema)
@schema = schema.follow_refs
end

def schema_for(content_type)
return unless supports_content_type?(content_type)
def schema_for(media_type)
return unless supports_media_type?(media_type)

@schema.navigate('content', content_type, 'schema')
@schema.navigate('content', media_type, 'schema')
end

def supports_content_type?(content_type)
@schema.dig('content', content_type).present?
def supports_media_type?(media_type)
@schema.dig('content', media_type).present?
end
end
end
10 changes: 5 additions & 5 deletions lib/openapi_contracts/doc/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@ def headers
end
end

def schema_for(content_type)
return unless supports_content_type?(content_type)
def schema_for(media_type)
return unless supports_media_type?(media_type)

@schema.navigate('content', content_type, 'schema')
@schema.navigate('content', media_type, 'schema')
end

def no_content?
!@schema.key? 'content'
end

def supports_content_type?(content_type)
@schema.dig('content', content_type).present?
def supports_media_type?(media_type)
@schema.dig('content', media_type).present?
end
end
end
38 changes: 32 additions & 6 deletions lib/openapi_contracts/doc/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,39 @@ def initialize(raw, pointer = [])
@pointer = pointer.freeze
end

def each # rubocop:disable Metrics/MethodLength
data = resolve
case data
when Array
enum = data.each_with_index
Enumerator.new(enum.size) do |yielder|
loop do
_item, index = enum.next
yielder << navigate(index.to_s)
end
end
when Hash
enum = data.each_key
Enumerator.new(enum.size) do |yielder|
loop do
key = enum.next
yielder << [key, navigate(key)]
end
end
end
end

# :nocov:
def inspect
"<#{self.class.name} @pointer=#{@pointer.inspect}>"
end
# :nocov:

# Resolves Schema ref pointers links like "$ref: #/some/path" and returns new sub-schema
# at the target if the current schema is only a ref link.
def follow_refs
if (ref = as_h['$ref'])
data = resolve
if data.is_a?(Hash) && (ref = data['$ref'])
at_pointer(ref.split('/')[1..])
else
self
Expand All @@ -32,10 +61,7 @@ def at_pointer(pointer)
self.class.new(raw, pointer)
end

def as_h
resolve
end

# Returns the actual sub-specification contents at the pointer of this Specification
def resolve
return @raw if pointer.nil? || pointer.empty?

Expand All @@ -53,7 +79,7 @@ def resolve
end

def navigate(*spointer)
self.class.new(@raw, (pointer + Array.wrap(spointer)))
self.class.new(@raw, (pointer + Array.wrap(spointer))).follow_refs
end
end
end
9 changes: 9 additions & 0 deletions lib/openapi_contracts/doc/with_parameters.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module OpenapiContracts
class Doc
module WithParameters
def parameters
@parameters ||= Array.wrap(@spec.navigate('parameters')&.each&.map { |s| Doc::Parameter.new(s) })
end
end
end
end
Loading

0 comments on commit 950915a

Please sign in to comment.