Skip to content

Commit

Permalink
Body parsing refactoring. Allow custom parsers.
Browse files Browse the repository at this point in the history
  • Loading branch information
jodosha committed Oct 1, 2014
1 parent 17077da commit 01d444e
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 44 deletions.
67 changes: 31 additions & 36 deletions lib/lotus/routing/parsers.rb
@@ -1,64 +1,59 @@
require 'lotus/routing/parsing/parser'

module Lotus
module Routing
class Parsers
class UnknownParserError < ::StandardError
def initialize(parser)
super("Unknown Parser: `#{ parser }'")
end
end

CONTENT_TYPE = 'CONTENT_TYPE'.freeze
MEDIA_TYPE_MATCHER = /\s*[;,]\s*/.freeze

RACK_INPUT = 'rack.input'.freeze
ROUTER_PARAMS = 'router.params'.freeze

# Supported Content-Types
#
APPLICATION_JSON = 'application/json'.freeze
IMPLEMENTATIONS = {
json: 'when APPLICATION_JSON then Rack::Utils::OkJson.decode(body)'
}.freeze

def initialize(parsers)
_compile!(parsers)
@parsers = prepare(parsers)
_redefine_call
end

def call(env)
env
end


private
def _compile!(parsers)
parsers = Array(parsers)
return if parsers.empty?
def prepare(args)
result = Hash.new
args = Array(args)
return result if args.empty?

implementations = parsers.map do |parser|
IMPLEMENTATIONS.fetch(parser.to_sym) do
raise UnknownParserError.new(parser)
args.each do |arg|
parser = Parsing::Parser.for(arg)

parser.mime_types.each do |mime|
result[mime] = parser
end
end

instance_eval %{
def call(env)
body = env[RACK_INPUT].read
return env if body.length == 0
result.default = Parsing::Parser.new
result
end

env[RACK_INPUT].rewind # somebody might try to read this stream
env[ROUTER_PARAMS] ||= {} # prepare params
def _redefine_call
return if @parsers.empty?

body = case media_type(env)
#{ implementations.join("\n") }
else
{}
end
define_singleton_method :call do |env|
body = env[RACK_INPUT].read
return env if body.empty?

env[ROUTER_PARAMS].merge!(body)
env[RACK_INPUT].rewind # somebody might try to read this stream
env[ROUTER_PARAMS] ||= {} # prepare params

env
end
}
env[ROUTER_PARAMS].merge!(
@parsers[
media_type(env)
].parse(body)
)

env
end
end

def media_type(env)
Expand Down
17 changes: 17 additions & 0 deletions lib/lotus/routing/parsing/json_parser.rb
@@ -0,0 +1,17 @@
require 'rack/utils/okjson'

module Lotus
module Routing
module Parsing
class JsonParser < Parser
def mime_types
['application/json']
end

def parse(body)
Rack::Utils::OkJson.decode(body)
end
end
end
end
end
43 changes: 43 additions & 0 deletions lib/lotus/routing/parsing/parser.rb
@@ -0,0 +1,43 @@
require 'lotus/utils/class'
require 'lotus/utils/string'

module Lotus
module Routing
module Parsing
class UnknownParserError < ::StandardError
def initialize(parser)
super("Unknown Parser: `#{ parser }'")
end
end

class Parser
def self.for(parser)
case parser
when String, Symbol
require_parser(parser)
else
parser
end
end

def mime_types
raise NotImplementedError
end

def parse(body)
Hash.new
end

private
def self.require_parser(parser)
require "lotus/routing/parsing/#{ parser }_parser"

parser = Utils::String.new(parser).classify
Utils::Class.load!("Lotus::Routing::Parsing::#{ parser }Parser").new
rescue LoadError, NameError
raise UnknownParserError.new(parser)
end
end
end
end
end
30 changes: 23 additions & 7 deletions test/integration/body_parsing_test.rb
@@ -1,24 +1,40 @@
require 'test_helper'
require 'json'

describe 'Body parsing' do
before do
json_endpoint = ->(env) {
[200, {}, [JSON.generate(env['router.params'])]]
endpoint = ->(env) {
[200, {}, [env['router.params']]]
}

@routes = Lotus::Router.new(parsers: [:json]) {
patch '/books/:id', to: json_endpoint
@routes = Lotus::Router.new(parsers: [:json, XmlParser.new]) {
patch '/books/:id', to: endpoint
patch '/authors/:id', to: endpoint
}

@app = Rack::MockRequest.new(@routes)
end

it 'is successful (JSON)' do
body = StringIO.new(JSON.generate({published: 'true'}))
body = StringIO.new( %({"published":"true"}) )
response = @app.patch('/books/23', 'CONTENT_TYPE' => 'application/json', 'rack.input' => body)

response.status.must_equal 200
response.body.must_equal %({"published":"true","id":"23"})
response.body.must_equal %({"published"=>"true", :id=>"23"})
end

it 'is successful (XML)' do
body = StringIO.new( %(<name>LG</name>) )
response = @app.patch('/authors/23', 'CONTENT_TYPE' => 'application/xml', 'rack.input' => body)

response.status.must_equal 200
response.body.must_equal %({"name"=>"LG", :id=>"23"})
end

it 'is successful (XML aliased mime)' do
body = StringIO.new( %(<name>MGF</name>) )
response = @app.patch('/authors/15', 'CONTENT_TYPE' => 'text/xml', 'rack.input' => body)

response.status.must_equal 200
response.body.must_equal %({"name"=>"MGF", :id=>"15"})
end
end
2 changes: 1 addition & 1 deletion test/routing/parsers_test.rb
Expand Up @@ -6,7 +6,7 @@
it 'raises error when unknown parser is given' do
begin
Lotus::Routing::Parsers.new(:a_parser)
rescue Lotus::Routing::Parsers::UnknownParserError => e
rescue Lotus::Routing::Parsing::UnknownParserError => e
e.message.must_equal "Unknown Parser: `a_parser'"
end
end
Expand Down
18 changes: 18 additions & 0 deletions test/support/fixtures.rb
@@ -1,3 +1,6 @@
require 'rexml/document'
require 'lotus/routing/parsing/parser'

class TestEndpoint
def call(env)
'Hi from TestEndpoint!'
Expand Down Expand Up @@ -207,3 +210,18 @@ def call(env)
end
end
end # KeyboardsController

class XmlParser < Lotus::Routing::Parsing::Parser
def mime_types
['application/xml', 'text/xml']
end

def parse(body)
result = {}

xml = REXML::Document.new(body)
xml.elements.each('*') {|el| result[el.name] = el.text }

result
end
end

0 comments on commit 01d444e

Please sign in to comment.