Skip to content

Commit

Permalink
Add PayloadParser
Browse files Browse the repository at this point in the history
  • Loading branch information
mkon committed Jul 19, 2023
1 parent 718c47c commit 1e6b960
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 13 deletions.
10 changes: 6 additions & 4 deletions lib/openapi_contracts.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@
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 :Doc, 'openapi_contracts/doc'
autoload :Helper, 'openapi_contracts/helper'
autoload :Match, 'openapi_contracts/match'
autoload :OperationRouter, 'openapi_contracts/operation_router'
autoload :Validators, 'openapi_contracts/validators'
autoload :PayloadParser, 'openapi_contracts/payload_parser'
autoload :Validators, 'openapi_contracts/validators'

Env = Struct.new(:operation, :options, :request, :response, keyword_init: true)

Expand Down
39 changes: 39 additions & 0 deletions lib/openapi_contracts/payload_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
require 'singleton'

module OpenapiContracts
class PayloadParser
include Singleton

class << self
delegate :parse, :register, to: :instance
end

Entry = Struct.new(:matcher, :parser) do
def call(raw)
parser.call(raw)
end

def match?(media_type)
matcher == media_type || matcher.match?(media_type)
end
end

def initialize
@parsers = []
end

def parse(media_type, payload)
parser = @parsers.find { |e| e.match?(media_type) }
raise ArgumentError, "#{media_type.inspect} is not supported yet" unless parser

parser.call(payload)
end

def register(matcher, parser)
@parsers << Entry.new(matcher, parser)
end
end

PayloadParser.register(%r{(/|\+)json$}, ->(raw) { JSON(raw) })
PayloadParser.register('application/x-www-form-urlencoded', ->(raw) { Rack::Utils.parse_nested_query(raw) })
end
6 changes: 2 additions & 4 deletions lib/openapi_contracts/validators/request_body.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@ class RequestBody < Base
delegate :request_body, to: :operation

def data_for_validation
# Support "*/json" or "*/*+json"
raise ArgumentError, "#{media_type.inspect} is not supported yet" unless %r{/|\+json$} =~ media_type

request.body.rewind
JSON(request.body.read)
raw = request.body.read
OpenapiContracts::PayloadParser.parse(media_type, raw)
end

def schema_for_validation
Expand Down
5 changes: 1 addition & 4 deletions lib/openapi_contracts/validators/response_body.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,8 @@ class ResponseBody < Base
delegate :media_type, to: :response

def data_for_validation
# Support "*/json" or "*/*+json"
raise ArgumentError, "#{media_type.inspect} is not supported yet" unless %r{/|\+json$} =~ media_type

# ActionDispatch::Response body is a plain string, while Rack::Response returns an array
JSON(Array.wrap(response.body).join)
OpenapiContracts::PayloadParser.parse(media_type, Array.wrap(response.body).join)
end

def schema_for_validation
Expand Down
2 changes: 1 addition & 1 deletion openapi_contracts.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ Gem::Specification.new do |s|

s.add_dependency 'activesupport', '>= 6.1', '< 8'
s.add_dependency 'json_schemer', '~> 1.0.3'
s.add_dependency 'rack', '>= 2.0.0'

s.add_development_dependency 'json_spec', '~> 1.1.5'
s.add_development_dependency 'rack', '~> 3.0.0'
s.add_development_dependency 'rspec', '~> 3.12.0'
s.add_development_dependency 'rubocop', '1.54.1'
s.add_development_dependency 'rubocop-rspec', '2.22.0'
Expand Down
45 changes: 45 additions & 0 deletions spec/openapi_contracts/payload_parser_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
RSpec.describe OpenapiContracts::PayloadParser do
describe '.parse(media_type, raw_body)' do
subject { described_class.parse(media_type, raw_body) }

context 'when the media-type is application/json' do
let(:media_type) { 'application/json' }
let(:raw_body) { '{"hello":"world"}' }

it 'parses correctly' do
expect(subject).to be_a(Hash)
expect(subject).to eq('hello' => 'world')
end
end

context 'when the media-type is application/vnd.api+json' do
let(:media_type) { 'application/vnd.api+json' }
let(:raw_body) { '{"hello":"world"}' }

it 'parses correctly' do
expect(subject).to be_a(Hash)
expect(subject).to eq('hello' => 'world')
end
end

context 'when the media-type is application/x-www-form-urlencoded' do
let(:media_type) { 'application/x-www-form-urlencoded' }
let(:raw_body) { 'hello=world' }

it 'parses correctly' do
expect(subject).to be_a(Hash)
expect(subject).to eq('hello' => 'world')
end
end

context 'when the media-type is application/x-www-form-urlencoded with Array' do
let(:media_type) { 'application/x-www-form-urlencoded' }
let(:raw_body) { 'hello[]=world&hello[]=foo' }

it 'parses correctly' do
expect(subject).to be_a(Hash)
expect(subject).to eq('hello' => %w(world foo))
end
end
end
end

0 comments on commit 1e6b960

Please sign in to comment.