Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parameter enum #58

Merged
merged 10 commits into from
Jul 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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