grape-batch 2.1.0 → 2.1.1
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/.rubocop.yml +12 -0
- data/Rakefile +1 -1
- data/grape-batch.gemspec +16 -12
- data/lib/grape/batch/configuration.rb +3 -1
- data/lib/grape/batch/converter.rb +25 -0
- data/lib/grape/batch/errors.rb +85 -2
- data/lib/grape/batch/logger.rb +33 -0
- data/lib/grape/batch/request.rb +41 -0
- data/lib/grape/batch/response.rb +16 -7
- data/lib/grape/batch/validator.rb +51 -0
- data/lib/grape/batch/version.rb +2 -1
- data/lib/grape/batch.rb +29 -62
- data/spec/api.rb +3 -3
- data/spec/{requests_spec.rb → grape/batch/base_spec.rb} +31 -27
- data/spec/spec_helper.rb +13 -15
- metadata +50 -19
- data/lib/grape/batch/hash_converter.rb +0 -26
- data/lib/grape/batch/parser.rb +0 -55
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2ba051e13b667b77094be62bf479aebf5452fc3e
|
4
|
+
data.tar.gz: f28bd7313a6376bd8a56792e1744dbadf63d19b3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7155185197a009f8a884a622d05cfb3f877f2990bffa0382c9424c3cf004b67f448e79a1b116a63d39979ad20dd5e2c550201fcf01a51d30f622a7ec7dafcfd5
|
7
|
+
data.tar.gz: 5d83dae879c57aa6ce723e4df16528b82fd04d6eb8d67bca4a53e4fa87fc5ba8de699cea6b2aaca4c1df12c8701e99a73e87449052a98d14b50f0c4b92f4709c
|
data/.rubocop.yml
ADDED
data/Rakefile
CHANGED
@@ -1 +1 @@
|
|
1
|
-
require
|
1
|
+
require 'bundler/gem_tasks'
|
data/grape-batch.gemspec
CHANGED
@@ -4,24 +4,28 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
4
|
require 'grape/batch/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name
|
8
|
-
spec.version
|
9
|
-
spec.authors
|
10
|
-
spec.email
|
11
|
-
|
12
|
-
|
13
|
-
spec.
|
7
|
+
spec.name = 'grape-batch'
|
8
|
+
spec.version = Grape::Batch::VERSION
|
9
|
+
spec.authors = ['Lionel Oto', 'Vincent Falduto', 'Cédric Darné']
|
10
|
+
spec.email = %w(lionel.oto@c4mprod.com
|
11
|
+
vincent.falduto@c4mprod.com
|
12
|
+
cedric.darne@c4mprod.com)
|
13
|
+
spec.summary = 'Extends Grape::API to support request batching'
|
14
|
+
spec.homepage = 'https://github.com/c4mprod/grape-batch'
|
15
|
+
spec.license = 'MIT'
|
14
16
|
|
15
|
-
spec.files
|
16
|
-
spec.executables
|
17
|
-
spec.test_files
|
17
|
+
spec.files = `git ls-files -z`.split("\x0")
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
20
|
spec.require_paths = ['lib']
|
19
21
|
|
20
|
-
spec.add_runtime_dependency 'grape', '>= 0.7.0'
|
21
22
|
spec.add_runtime_dependency 'multi_json', '>= 1.0'
|
22
23
|
|
23
24
|
spec.add_development_dependency 'bundler', '~> 1.6'
|
25
|
+
spec.add_development_dependency 'grape', '>= 0.7.0'
|
26
|
+
spec.add_development_dependency 'rack-test', '~> 0.6.2'
|
24
27
|
spec.add_development_dependency 'rake', '~> 10.3.2'
|
25
28
|
spec.add_development_dependency 'rspec', '~> 3.1.0'
|
26
|
-
spec.add_development_dependency '
|
29
|
+
spec.add_development_dependency 'rubocop', '~> 0.34.2'
|
30
|
+
spec.add_development_dependency 'rubocop-rspec', '~> 1.3.1'
|
27
31
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
module Grape
|
2
|
+
# Main gem module
|
2
3
|
module Batch
|
4
|
+
# Gem configuration
|
3
5
|
class Configuration
|
4
6
|
attr_accessor :path, :limit, :formatter, :logger, :session_proc
|
5
7
|
|
@@ -8,7 +10,7 @@ module Grape
|
|
8
10
|
@limit = 10
|
9
11
|
@formatter = Grape::Batch::Response
|
10
12
|
@logger = nil
|
11
|
-
@session_proc =
|
13
|
+
@session_proc = proc {}
|
12
14
|
end
|
13
15
|
end
|
14
16
|
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Grape
|
2
|
+
module Batch
|
3
|
+
# Convert hash to www form url params
|
4
|
+
class Converter
|
5
|
+
class << self
|
6
|
+
def encode(value, key = nil, out_hash = {})
|
7
|
+
case value
|
8
|
+
when Hash
|
9
|
+
value.each { |k, v| encode(v, append_key(key, k), out_hash) }
|
10
|
+
when Array
|
11
|
+
value.each { |v| encode(v, "#{key}[]", out_hash) }
|
12
|
+
else
|
13
|
+
out_hash[key] = value
|
14
|
+
end
|
15
|
+
|
16
|
+
value ? out_hash : ''
|
17
|
+
end
|
18
|
+
|
19
|
+
def append_key(root_key, key)
|
20
|
+
root_key ? :"#{root_key}[#{key.to_s}]" : :"#{key}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/grape/batch/errors.rb
CHANGED
@@ -1,6 +1,89 @@
|
|
1
1
|
module Grape
|
2
2
|
module Batch
|
3
|
-
class RequestBodyError < ArgumentError
|
4
|
-
|
3
|
+
class RequestBodyError < ArgumentError
|
4
|
+
# Request body is blank
|
5
|
+
class Blank < RequestBodyError
|
6
|
+
def initialize
|
7
|
+
super('Request body is blank')
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
# Request body is not properly formatted JSON
|
12
|
+
class JsonFormat < RequestBodyError
|
13
|
+
def initialize
|
14
|
+
super('Request body is not valid JSON')
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Batch body is nil
|
19
|
+
class Nil < RequestBodyError
|
20
|
+
def initialize
|
21
|
+
super('Request body is nil')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Batch body isn't properly formatted as a Hash
|
26
|
+
class Format < RequestBodyError
|
27
|
+
def initialize
|
28
|
+
super('Request body is not well formatted')
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Some requests attributes are missing in the batch body
|
33
|
+
class MissingRequests < RequestBodyError
|
34
|
+
def initialize
|
35
|
+
super("'requests' object is missing in request body")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Some requests attributes aren't properly formatted as an Array
|
40
|
+
class RequestFormat < RequestBodyError
|
41
|
+
def initialize
|
42
|
+
super("'requests' is not well formatted")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Batch request method is missing
|
47
|
+
class MissingMethod < RequestBodyError
|
48
|
+
def initialize
|
49
|
+
super("'method' is missing in one of request objects")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Batch request method isn't properly formatted as a String
|
54
|
+
class MethodFormat < RequestBodyError
|
55
|
+
def initialize
|
56
|
+
super("'method' is invalid in one of request objects")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Batch request method aren't allowed
|
61
|
+
class InvalidMethod < RequestBodyError
|
62
|
+
def initialize
|
63
|
+
super("'method' is invalid in one of request objects")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Batch request path is missing
|
68
|
+
class MissingPath < RequestBodyError
|
69
|
+
def initialize
|
70
|
+
super("'path' is missing in one of request objects")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Batch request path isn't properly formatted as a String
|
75
|
+
class InvalidPath < RequestBodyError
|
76
|
+
def initialize
|
77
|
+
super("'path' is invalid in one of request objects")
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Batch exceeds request limit
|
83
|
+
class TooManyRequestsError < StandardError
|
84
|
+
def initialize
|
85
|
+
super('Batch requests limit exceeded')
|
86
|
+
end
|
87
|
+
end
|
5
88
|
end
|
6
89
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Grape
|
2
|
+
module Batch
|
3
|
+
# Main class logger
|
4
|
+
class Logger
|
5
|
+
def prepare(env)
|
6
|
+
rack_timeout_info = env['rack-timeout.info'][:id] if env['rack-timeout.info']
|
7
|
+
@request_id = env['HTTP_X_REQUEST_ID'] || rack_timeout_info || SecureRandom.hex
|
8
|
+
@logger = Grape::Batch.configuration.logger || rails_logger || default_logger
|
9
|
+
self
|
10
|
+
end
|
11
|
+
|
12
|
+
def default_logger
|
13
|
+
logger = Logger.new($stdout)
|
14
|
+
logger.level = Logger::INFO
|
15
|
+
logger
|
16
|
+
end
|
17
|
+
|
18
|
+
def rails_logger
|
19
|
+
defined?(::Rails) && ::Rails.respond_to?(:logger) && ::Rails.logger
|
20
|
+
end
|
21
|
+
|
22
|
+
def batch_begin
|
23
|
+
@logger.info("--- Grape::Batch #{@request_id} BEGIN")
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def batch_end
|
28
|
+
@logger.info("--- Grape::Batch #{@request_id} END")
|
29
|
+
self
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'grape/batch/converter'
|
2
|
+
|
3
|
+
module Grape
|
4
|
+
module Batch
|
5
|
+
# Prepare batch request
|
6
|
+
class Request
|
7
|
+
def initialize(env, batch_request)
|
8
|
+
@env = env
|
9
|
+
@batch_request = batch_request
|
10
|
+
end
|
11
|
+
|
12
|
+
def method
|
13
|
+
@batch_request['method']
|
14
|
+
end
|
15
|
+
|
16
|
+
def path
|
17
|
+
@batch_request['path']
|
18
|
+
end
|
19
|
+
|
20
|
+
def body
|
21
|
+
@body ||= @batch_request['body'].is_a?(Hash) ? @batch_request['body'] : {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def query_string
|
25
|
+
@query_string ||= method == 'GET' ? URI.encode_www_form(Converter.encode(body).to_a) : ''
|
26
|
+
end
|
27
|
+
|
28
|
+
def rack_input
|
29
|
+
@rack_input ||= method == 'GET' ? '{}' : StringIO.new(MultiJson.encode(body))
|
30
|
+
end
|
31
|
+
|
32
|
+
def build
|
33
|
+
@env['REQUEST_METHOD'] = method
|
34
|
+
@env['PATH_INFO'] = path
|
35
|
+
@env['QUERY_STRING'] = query_string
|
36
|
+
@env['rack.input'] = rack_input
|
37
|
+
@env
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/grape/batch/response.rb
CHANGED
@@ -1,10 +1,19 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
1
|
+
module Grape
|
2
|
+
module Batch
|
3
|
+
# Format batch request response
|
4
|
+
class Response
|
5
|
+
def self.format(status, _headers, response)
|
6
|
+
if response
|
7
|
+
body = response.respond_to?(:body) ? response.body.join : response.join
|
8
|
+
result = MultiJson.decode(body)
|
9
|
+
end
|
7
10
|
|
8
|
-
|
11
|
+
if (200..299).include?(status)
|
12
|
+
{ success: result }
|
13
|
+
else
|
14
|
+
{ code: status, error: result['error'] }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
9
18
|
end
|
10
19
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Grape
|
2
|
+
module Batch
|
3
|
+
# Parse and validate request params and ensure it is a valid batch request
|
4
|
+
class Validator
|
5
|
+
ALLOWED_METHODS = %w(GET DELETE PATCH POST PUT)
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def parse(env, limit)
|
9
|
+
batch_body = decode_body(env['rack.input'].read)
|
10
|
+
|
11
|
+
requests = batch_body['requests']
|
12
|
+
validate_batch(requests, limit)
|
13
|
+
requests.each { |request| validate_request(request) }
|
14
|
+
|
15
|
+
requests
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def decode_body(body)
|
21
|
+
fail RequestBodyError::Blank unless body.length > 0
|
22
|
+
|
23
|
+
begin
|
24
|
+
batch_body = MultiJson.decode(body)
|
25
|
+
rescue MultiJson::ParseError
|
26
|
+
raise RequestBodyError::JsonFormat
|
27
|
+
end
|
28
|
+
|
29
|
+
fail RequestBodyError::Nil unless batch_body
|
30
|
+
fail RequestBodyError::Format unless batch_body.is_a?(Hash)
|
31
|
+
|
32
|
+
batch_body
|
33
|
+
end
|
34
|
+
|
35
|
+
def validate_batch(batch_requests, limit)
|
36
|
+
fail RequestBodyError::MissingRequests unless batch_requests
|
37
|
+
fail RequestBodyError::RequestFormat unless batch_requests.is_a?(Array)
|
38
|
+
fail TooManyRequestsError if batch_requests.count > limit
|
39
|
+
end
|
40
|
+
|
41
|
+
def validate_request(request)
|
42
|
+
fail RequestBodyError::MissingMethod unless request['method']
|
43
|
+
fail RequestBodyError::MethodFormat unless request['method'].is_a?(String)
|
44
|
+
fail RequestBodyError::InvalidMethod unless ALLOWED_METHODS.include?(request['method'])
|
45
|
+
fail RequestBodyError::MissingPath unless request['path']
|
46
|
+
fail RequestBodyError::InvalidPath unless request['path'].is_a?(String)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/grape/batch/version.rb
CHANGED
data/lib/grape/batch.rb
CHANGED
@@ -1,99 +1,66 @@
|
|
1
|
-
require 'active_support'
|
2
|
-
require 'grape/batch/version'
|
3
|
-
require 'grape/batch/errors'
|
4
1
|
require 'grape/batch/configuration'
|
5
|
-
require 'grape/batch/
|
6
|
-
require 'grape/batch/
|
2
|
+
require 'grape/batch/errors'
|
3
|
+
require 'grape/batch/logger'
|
4
|
+
require 'grape/batch/request'
|
7
5
|
require 'grape/batch/response'
|
6
|
+
require 'grape/batch/validator'
|
7
|
+
require 'grape/batch/version'
|
8
8
|
require 'multi_json'
|
9
9
|
|
10
10
|
module Grape
|
11
11
|
module Batch
|
12
|
+
# Gem main class
|
12
13
|
class Base
|
13
14
|
def initialize(app)
|
14
15
|
@app = app
|
15
16
|
@response_klass = Grape::Batch.configuration.formatter
|
17
|
+
@batch_size_limit = Grape::Batch.configuration.limit
|
18
|
+
@api_path = Grape::Batch.configuration.path
|
19
|
+
@session_proc = Grape::Batch.configuration.session_proc
|
20
|
+
@logger = Grape::Batch::Logger.new
|
16
21
|
end
|
17
22
|
|
18
23
|
def call(env)
|
19
|
-
return @app.call(env) unless
|
24
|
+
return @app.call(env) unless batch_request?(env)
|
25
|
+
@logger.prepare(env).batch_begin
|
20
26
|
batch_call(env)
|
21
27
|
end
|
22
28
|
|
23
29
|
def batch_call(env)
|
24
|
-
status = 200
|
25
|
-
headers = { 'Content-Type' => 'application/json' }
|
26
|
-
rack_timeout_info = env['rack-timeout.info'][:id] if env['rack-timeout.info']
|
27
|
-
request_id = env['HTTP_X_REQUEST_ID'] || rack_timeout_info || SecureRandom.hex
|
28
|
-
logger.info("--- Grape::Batch #{request_id} BEGIN")
|
29
30
|
begin
|
30
|
-
|
31
|
-
|
32
|
-
body = MultiJson.encode(
|
31
|
+
status = 200
|
32
|
+
batch_requests = Grape::Batch::Validator.parse(env, @batch_size_limit)
|
33
|
+
body = MultiJson.encode(dispatch(env, batch_requests))
|
33
34
|
rescue Grape::Batch::RequestBodyError, Grape::Batch::TooManyRequestsError => e
|
34
35
|
e.class == TooManyRequestsError ? status = 429 : status = 400
|
35
36
|
body = e.message
|
36
37
|
end
|
37
|
-
|
38
|
-
|
38
|
+
|
39
|
+
@logger.batch_end
|
40
|
+
Rack::Response.new(body, status, 'Content-Type' => 'application/json')
|
39
41
|
end
|
40
42
|
|
41
43
|
private
|
42
44
|
|
43
|
-
def
|
44
|
-
env['PATH_INFO'].start_with?(
|
45
|
+
def batch_request?(env)
|
46
|
+
env['PATH_INFO'].start_with?(@api_path) &&
|
45
47
|
env['REQUEST_METHOD'] == 'POST' &&
|
46
48
|
env['CONTENT_TYPE'] == 'application/json'
|
47
49
|
end
|
48
50
|
|
49
51
|
def dispatch(env, batch_requests)
|
50
|
-
|
51
|
-
|
52
|
-
# iterate
|
53
|
-
batch_env = env.dup
|
54
|
-
batch_requests.map do |request|
|
55
|
-
# init env for Grape resource
|
56
|
-
tmp_env = prepare_tmp_env(batch_env, request)
|
57
|
-
status, headers, response = @app.call(tmp_env)
|
58
|
-
|
59
|
-
# format response
|
60
|
-
@response_klass::format(status, headers, response)
|
61
|
-
end
|
62
|
-
end
|
52
|
+
# Call session proc
|
53
|
+
env['api.session'] = @session_proc.call(env)
|
63
54
|
|
64
|
-
|
65
|
-
|
66
|
-
path = request['path']
|
67
|
-
body = request['body'].is_a?(Hash) ? request['body'] : {}
|
68
|
-
query_string = ''
|
69
|
-
rack_input = '{}'
|
55
|
+
# Prepare batch request env
|
56
|
+
request_env = env.dup
|
70
57
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
58
|
+
# Call batch request
|
59
|
+
batch_requests.map do |batch_request|
|
60
|
+
batch_env = Grape::Batch::Request.new(request_env, batch_request).build
|
61
|
+
status, headers, response = @app.call(batch_env)
|
62
|
+
@response_klass.format(status, headers, response)
|
75
63
|
end
|
76
|
-
|
77
|
-
tmp_env['REQUEST_METHOD'] = method
|
78
|
-
tmp_env['PATH_INFO'] = path
|
79
|
-
tmp_env['QUERY_STRING'] = query_string
|
80
|
-
tmp_env['rack.input'] = rack_input
|
81
|
-
tmp_env
|
82
|
-
end
|
83
|
-
|
84
|
-
def logger
|
85
|
-
@logger ||= Grape::Batch.configuration.logger || rails_logger || default_logger
|
86
|
-
end
|
87
|
-
|
88
|
-
def default_logger
|
89
|
-
logger = Logger.new($stdout)
|
90
|
-
logger.level = Logger::INFO
|
91
|
-
logger
|
92
|
-
end
|
93
|
-
|
94
|
-
# Get the Rails logger if it's defined.
|
95
|
-
def rails_logger
|
96
|
-
defined?(::Rails) && ::Rails.respond_to?(:logger) && ::Rails.logger
|
97
64
|
end
|
98
65
|
end
|
99
66
|
end
|
data/spec/api.rb
CHANGED
@@ -23,7 +23,7 @@ module Twitter
|
|
23
23
|
|
24
24
|
resource :user do
|
25
25
|
params do
|
26
|
-
requires :id, type: Integer, desc:
|
26
|
+
requires :id, type: Integer, desc: 'User id.'
|
27
27
|
end
|
28
28
|
route_param :id do
|
29
29
|
get do
|
@@ -43,14 +43,14 @@ module Twitter
|
|
43
43
|
|
44
44
|
resource :status do
|
45
45
|
params do
|
46
|
-
requires :id, type: Integer, desc:
|
46
|
+
requires :id, type: Integer, desc: 'User id.'
|
47
47
|
end
|
48
48
|
get do
|
49
49
|
"status #{params[:id]}"
|
50
50
|
end
|
51
51
|
|
52
52
|
params do
|
53
|
-
requires :id, type: Integer, desc:
|
53
|
+
requires :id, type: Integer, desc: 'User id.'
|
54
54
|
end
|
55
55
|
post do
|
56
56
|
"status #{params[:id]}"
|
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
require 'rack/test'
|
3
|
-
require 'grape/batch'
|
4
3
|
require 'grape'
|
4
|
+
require 'grape/batch'
|
5
5
|
require 'api'
|
6
6
|
|
7
7
|
RSpec.describe Grape::Batch::Base do
|
@@ -13,7 +13,7 @@ RSpec.describe Grape::Batch::Base do
|
|
13
13
|
@app = Twitter::API.new
|
14
14
|
end
|
15
15
|
|
16
|
-
let(:stack) {
|
16
|
+
let(:stack) { described_class.new(@app) }
|
17
17
|
let(:request) { Rack::MockRequest.new(stack) }
|
18
18
|
|
19
19
|
def encode(message)
|
@@ -35,13 +35,14 @@ RSpec.describe Grape::Batch::Base do
|
|
35
35
|
describe 'GET /failure' do
|
36
36
|
let(:response) { request.get('/api/v1/failure') }
|
37
37
|
it { expect(response.status).to eq(503) }
|
38
|
-
it { expect(response.body).to eq(encode(
|
38
|
+
it { expect(response.body).to eq(encode(error: 'Failed as expected')) }
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
42
42
|
describe '/batch' do
|
43
43
|
let(:request_body) { nil }
|
44
|
-
let(:
|
44
|
+
let(:options) { { 'CONTENT_TYPE' => 'application/json', input: request_body } }
|
45
|
+
let(:response) { request.post('/batch', options) }
|
45
46
|
|
46
47
|
context 'with invalid body' do
|
47
48
|
it { expect(response.status).to eq(400) }
|
@@ -76,40 +77,40 @@ RSpec.describe Grape::Batch::Base do
|
|
76
77
|
end
|
77
78
|
|
78
79
|
context "when body['requests'] is not an array" do
|
79
|
-
let(:request_body) { encode(
|
80
|
+
let(:request_body) { encode(requests: 'request') }
|
80
81
|
it { expect(response.body).to eq("'requests' is not well formatted") }
|
81
82
|
end
|
82
83
|
|
83
84
|
context 'when request limit is exceeded' do
|
84
|
-
let(:request_body) { encode(
|
85
|
+
let(:request_body) { encode(requests: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) }
|
85
86
|
it { expect(response.body).to eq('Batch requests limit exceeded') }
|
86
87
|
end
|
87
88
|
|
88
89
|
describe 'method attribute in request object' do
|
89
90
|
context 'method is missing' do
|
90
|
-
let(:request_body) { encode(
|
91
|
+
let(:request_body) { encode(requests: [{}]) }
|
91
92
|
it { expect(response.body).to eq("'method' is missing in one of request objects") }
|
92
93
|
end
|
93
94
|
|
94
95
|
context 'method is not a String' do
|
95
|
-
let(:request_body) { encode(
|
96
|
+
let(:request_body) { encode(requests: [{ method: true }]) }
|
96
97
|
it { expect(response.body).to eq("'method' is invalid in one of request objects") }
|
97
98
|
end
|
98
99
|
|
99
100
|
context 'method is invalid' do
|
100
|
-
let(:request_body) { encode(
|
101
|
+
let(:request_body) { encode(requests: [{ method: 'TRACE' }]) }
|
101
102
|
it { expect(response.body).to eq("'method' is invalid in one of request objects") }
|
102
103
|
end
|
103
104
|
end
|
104
105
|
|
105
106
|
describe 'path attribute in request object' do
|
106
107
|
context 'path is missing' do
|
107
|
-
let(:request_body) { encode(
|
108
|
+
let(:request_body) { encode(requests: [{ method: 'GET' }]) }
|
108
109
|
it { expect(response.body).to eq("'path' is missing in one of request objects") }
|
109
110
|
end
|
110
111
|
|
111
112
|
context 'path is not a String' do
|
112
|
-
let(:request_body) { encode(
|
113
|
+
let(:request_body) { encode(requests: [{ method: 'GET', path: 123 }]) }
|
113
114
|
it { expect(response.body).to eq("'path' is invalid in one of request objects") }
|
114
115
|
end
|
115
116
|
end
|
@@ -117,50 +118,51 @@ RSpec.describe Grape::Batch::Base do
|
|
117
118
|
|
118
119
|
describe 'GET' do
|
119
120
|
context 'with no parameters' do
|
120
|
-
let(:request_body) { encode(
|
121
|
+
let(:request_body) { encode(requests: [{ method: 'GET', path: '/api/v1/hello' }]) }
|
121
122
|
it { expect(response.status).to eq(200) }
|
122
123
|
it { expect(response.body).to eq(encode([{ success: 'world' }])) }
|
123
124
|
end
|
124
125
|
|
125
126
|
context 'with parameters' do
|
126
|
-
let(:request_body) { encode(
|
127
|
+
let(:request_body) { encode(requests: [{ method: 'GET', path: '/api/v1/user/856' }]) }
|
127
128
|
it { expect(response.status).to eq(200) }
|
128
129
|
it { expect(response.body).to eq(encode([{ success: 'user 856' }])) }
|
129
130
|
end
|
130
131
|
|
131
132
|
context 'with a body' do
|
132
|
-
let(:
|
133
|
+
let(:path) { '/api/v1/status' }
|
134
|
+
let(:request_body) { encode(requests: [{ method: 'GET', path: path, body: { id: 856 } }]) }
|
133
135
|
it { expect(response.status).to eq(200) }
|
134
136
|
it { expect(response.body).to eq(encode([{ success: 'status 856' }])) }
|
135
137
|
end
|
136
138
|
|
137
139
|
context 'with a body and nested hash' do
|
138
|
-
let(:
|
139
|
-
|
140
|
-
|
141
|
-
let(:request_body) do
|
142
|
-
encode({ requests: [{ method: 'GET', path: '/api/v1/complex', body: complex}] })
|
143
|
-
end
|
140
|
+
let(:path) { '/api/v1/complex' }
|
141
|
+
let(:complex) { { a: { b: { c: 1 } } } }
|
142
|
+
let(:request_body) { encode(requests: [{ method: 'GET', path: path, body: complex }]) }
|
144
143
|
it { expect(response.status).to eq(200) }
|
145
|
-
it { expect(response.body).to eq(encode([{ success:
|
144
|
+
it { expect(response.body).to eq(encode([{ success: 'hash 1' }])) }
|
146
145
|
end
|
147
146
|
|
148
147
|
describe '404 errors' do
|
149
|
-
let(:request_body) { encode(
|
148
|
+
let(:request_body) { encode(requests: [{ method: 'GET', path: '/api/v1/unknown' }]) }
|
149
|
+
let(:expected_error) { { code: 404, error: '/api/v1/unknown not found' } }
|
150
150
|
it { expect(response.status).to eq(200) }
|
151
|
-
it { expect(response.body).to eq(encode([
|
151
|
+
it { expect(response.body).to eq(encode([expected_error])) }
|
152
152
|
end
|
153
153
|
end
|
154
154
|
|
155
155
|
describe 'POST' do
|
156
156
|
context 'with no parameters' do
|
157
|
-
let(:request_body) { encode(
|
157
|
+
let(:request_body) { encode(requests: [{ method: 'POST', path: '/api/v1/hello' }]) }
|
158
158
|
it { expect(response.status).to eq(200) }
|
159
159
|
it { expect(response.body).to eq(encode([{ success: 'world' }])) }
|
160
160
|
end
|
161
161
|
|
162
162
|
context 'with a body' do
|
163
|
-
let(:
|
163
|
+
let(:path) { '/api/v1/status' }
|
164
|
+
let(:body) { { id: 856 } }
|
165
|
+
let(:request_body) { encode(requests: [{ method: 'POST', path: path, body: body }]) }
|
164
166
|
it { expect(response.status).to eq(200) }
|
165
167
|
it { expect(response.body).to eq(encode([{ success: 'status 856' }])) }
|
166
168
|
end
|
@@ -168,7 +170,9 @@ RSpec.describe Grape::Batch::Base do
|
|
168
170
|
|
169
171
|
describe 'POST' do
|
170
172
|
context 'with multiple requests' do
|
171
|
-
let(:
|
173
|
+
let(:request_1) { { method: 'POST', path: '/api/v1/hello' } }
|
174
|
+
let(:request_2) { { method: 'GET', path: '/api/v1/user/856' } }
|
175
|
+
let(:request_body) { encode(requests: [request_1, request_2]) }
|
172
176
|
it { expect(response.status).to eq(200) }
|
173
177
|
it { expect(decode(response.body).size).to eq(2) }
|
174
178
|
end
|
@@ -190,7 +194,7 @@ RSpec.describe Grape::Batch::Base do
|
|
190
194
|
config = Grape::Batch::Configuration.new
|
191
195
|
config.path = '/custom_path'
|
192
196
|
config.limit = 15
|
193
|
-
config.session_proc =
|
197
|
+
config.session_proc = proc { 3 + 2 }
|
194
198
|
config
|
195
199
|
end
|
196
200
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -38,37 +38,36 @@ RSpec.configure do |config|
|
|
38
38
|
mocks.verify_partial_doubles = true
|
39
39
|
end
|
40
40
|
|
41
|
-
# The settings below are suggested to provide a good initial experience
|
42
|
-
# with RSpec, but feel free to customize to your heart's content.
|
43
|
-
=begin
|
41
|
+
# The settings below are suggested to provide a good initial experience
|
42
|
+
# with RSpec, but feel free to customize to your heart's content.
|
44
43
|
# These two settings work together to allow you to limit a spec run
|
45
44
|
# to individual examples or groups you care about by tagging them with
|
46
45
|
# `:focus` metadata. When nothing is tagged with `:focus`, all examples
|
47
46
|
# get run.
|
48
|
-
config.filter_run :focus
|
49
|
-
config.run_all_when_everything_filtered = true
|
47
|
+
# config.filter_run :focus
|
48
|
+
# config.run_all_when_everything_filtered = true
|
50
49
|
|
51
50
|
# Limits the available syntax to the non-monkey patched syntax that is recommended.
|
52
51
|
# For more details, see:
|
53
52
|
# - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
|
54
53
|
# - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
55
54
|
# - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
|
56
|
-
config.disable_monkey_patching!
|
55
|
+
# config.disable_monkey_patching!
|
57
56
|
|
58
57
|
# Many RSpec users commonly either run the entire suite or an individual
|
59
58
|
# file, and it's useful to allow more verbose output when running an
|
60
59
|
# individual spec file.
|
61
|
-
if config.files_to_run.one?
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
end
|
60
|
+
# if config.files_to_run.one?
|
61
|
+
# Use the documentation formatter for detailed output,
|
62
|
+
# unless a formatter has already been configured
|
63
|
+
# (e.g. via a command-line flag).
|
64
|
+
# config.default_formatter = 'doc'
|
65
|
+
# end
|
67
66
|
|
68
67
|
# Print the 10 slowest examples and example groups at the
|
69
68
|
# end of the spec run, to help surface which specs are running
|
70
69
|
# particularly slow.
|
71
|
-
config.profile_examples = 10
|
70
|
+
# config.profile_examples = 10
|
72
71
|
|
73
72
|
# Run specs in random order to surface order dependencies. If you find an
|
74
73
|
# order dependency and want to debug it, you can fix the order by providing
|
@@ -80,6 +79,5 @@ RSpec.configure do |config|
|
|
80
79
|
# Setting this allows you to use `--seed` to deterministically reproduce
|
81
80
|
# test failures related to randomization by passing the same `--seed` value
|
82
81
|
# as the one that triggered the failure.
|
83
|
-
Kernel.srand config.seed
|
84
|
-
=end
|
82
|
+
# Kernel.srand config.seed
|
85
83
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: grape-batch
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.1.
|
4
|
+
version: 2.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lionel Oto
|
@@ -10,50 +10,64 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2015-11-
|
13
|
+
date: 2015-11-05 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
|
-
name:
|
16
|
+
name: multi_json
|
17
17
|
requirement: !ruby/object:Gem::Requirement
|
18
18
|
requirements:
|
19
19
|
- - ">="
|
20
20
|
- !ruby/object:Gem::Version
|
21
|
-
version:
|
21
|
+
version: '1.0'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
24
|
version_requirements: !ruby/object:Gem::Requirement
|
25
25
|
requirements:
|
26
26
|
- - ">="
|
27
27
|
- !ruby/object:Gem::Version
|
28
|
-
version:
|
28
|
+
version: '1.0'
|
29
29
|
- !ruby/object:Gem::Dependency
|
30
|
-
name:
|
30
|
+
name: bundler
|
31
|
+
requirement: !ruby/object:Gem::Requirement
|
32
|
+
requirements:
|
33
|
+
- - "~>"
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '1.6'
|
36
|
+
type: :development
|
37
|
+
prerelease: false
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - "~>"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '1.6'
|
43
|
+
- !ruby/object:Gem::Dependency
|
44
|
+
name: grape
|
31
45
|
requirement: !ruby/object:Gem::Requirement
|
32
46
|
requirements:
|
33
47
|
- - ">="
|
34
48
|
- !ruby/object:Gem::Version
|
35
|
-
version:
|
36
|
-
type: :
|
49
|
+
version: 0.7.0
|
50
|
+
type: :development
|
37
51
|
prerelease: false
|
38
52
|
version_requirements: !ruby/object:Gem::Requirement
|
39
53
|
requirements:
|
40
54
|
- - ">="
|
41
55
|
- !ruby/object:Gem::Version
|
42
|
-
version:
|
56
|
+
version: 0.7.0
|
43
57
|
- !ruby/object:Gem::Dependency
|
44
|
-
name:
|
58
|
+
name: rack-test
|
45
59
|
requirement: !ruby/object:Gem::Requirement
|
46
60
|
requirements:
|
47
61
|
- - "~>"
|
48
62
|
- !ruby/object:Gem::Version
|
49
|
-
version:
|
63
|
+
version: 0.6.2
|
50
64
|
type: :development
|
51
65
|
prerelease: false
|
52
66
|
version_requirements: !ruby/object:Gem::Requirement
|
53
67
|
requirements:
|
54
68
|
- - "~>"
|
55
69
|
- !ruby/object:Gem::Version
|
56
|
-
version:
|
70
|
+
version: 0.6.2
|
57
71
|
- !ruby/object:Gem::Dependency
|
58
72
|
name: rake
|
59
73
|
requirement: !ruby/object:Gem::Requirement
|
@@ -83,19 +97,33 @@ dependencies:
|
|
83
97
|
- !ruby/object:Gem::Version
|
84
98
|
version: 3.1.0
|
85
99
|
- !ruby/object:Gem::Dependency
|
86
|
-
name:
|
100
|
+
name: rubocop
|
87
101
|
requirement: !ruby/object:Gem::Requirement
|
88
102
|
requirements:
|
89
103
|
- - "~>"
|
90
104
|
- !ruby/object:Gem::Version
|
91
|
-
version: 0.
|
105
|
+
version: 0.34.2
|
92
106
|
type: :development
|
93
107
|
prerelease: false
|
94
108
|
version_requirements: !ruby/object:Gem::Requirement
|
95
109
|
requirements:
|
96
110
|
- - "~>"
|
97
111
|
- !ruby/object:Gem::Version
|
98
|
-
version: 0.
|
112
|
+
version: 0.34.2
|
113
|
+
- !ruby/object:Gem::Dependency
|
114
|
+
name: rubocop-rspec
|
115
|
+
requirement: !ruby/object:Gem::Requirement
|
116
|
+
requirements:
|
117
|
+
- - "~>"
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: 1.3.1
|
120
|
+
type: :development
|
121
|
+
prerelease: false
|
122
|
+
version_requirements: !ruby/object:Gem::Requirement
|
123
|
+
requirements:
|
124
|
+
- - "~>"
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: 1.3.1
|
99
127
|
description:
|
100
128
|
email:
|
101
129
|
- lionel.oto@c4mprod.com
|
@@ -107,6 +135,7 @@ extra_rdoc_files: []
|
|
107
135
|
files:
|
108
136
|
- ".gitignore"
|
109
137
|
- ".rspec"
|
138
|
+
- ".rubocop.yml"
|
110
139
|
- ".ruby-gemset"
|
111
140
|
- ".ruby-version"
|
112
141
|
- ".travis.yml"
|
@@ -118,13 +147,15 @@ files:
|
|
118
147
|
- grape-batch.gemspec
|
119
148
|
- lib/grape/batch.rb
|
120
149
|
- lib/grape/batch/configuration.rb
|
150
|
+
- lib/grape/batch/converter.rb
|
121
151
|
- lib/grape/batch/errors.rb
|
122
|
-
- lib/grape/batch/
|
123
|
-
- lib/grape/batch/
|
152
|
+
- lib/grape/batch/logger.rb
|
153
|
+
- lib/grape/batch/request.rb
|
124
154
|
- lib/grape/batch/response.rb
|
155
|
+
- lib/grape/batch/validator.rb
|
125
156
|
- lib/grape/batch/version.rb
|
126
157
|
- spec/api.rb
|
127
|
-
- spec/
|
158
|
+
- spec/grape/batch/base_spec.rb
|
128
159
|
- spec/spec_helper.rb
|
129
160
|
homepage: https://github.com/c4mprod/grape-batch
|
130
161
|
licenses:
|
@@ -152,5 +183,5 @@ specification_version: 4
|
|
152
183
|
summary: Extends Grape::API to support request batching
|
153
184
|
test_files:
|
154
185
|
- spec/api.rb
|
155
|
-
- spec/
|
186
|
+
- spec/grape/batch/base_spec.rb
|
156
187
|
- spec/spec_helper.rb
|
@@ -1,26 +0,0 @@
|
|
1
|
-
module Grape
|
2
|
-
module Batch
|
3
|
-
class HashConverter
|
4
|
-
def self.encode(value, key = nil, out_hash = {})
|
5
|
-
case value
|
6
|
-
when Hash then
|
7
|
-
value.each { |k,v| encode(v, append_key(key,k), out_hash) }
|
8
|
-
out_hash
|
9
|
-
when Array then
|
10
|
-
value.each { |v| encode(v, "#{key}[]", out_hash) }
|
11
|
-
out_hash
|
12
|
-
when nil then ''
|
13
|
-
else
|
14
|
-
out_hash[key] = value
|
15
|
-
out_hash
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
private
|
20
|
-
|
21
|
-
def self.append_key(root_key, key)
|
22
|
-
root_key.nil? ? :"#{key}" : :"#{root_key}[#{key.to_s}]"
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
data/lib/grape/batch/parser.rb
DELETED
@@ -1,55 +0,0 @@
|
|
1
|
-
module Grape
|
2
|
-
module Batch
|
3
|
-
class Validator
|
4
|
-
class << self
|
5
|
-
def parse(env, limit)
|
6
|
-
batch_body = decode_body(env['rack.input'].read)
|
7
|
-
|
8
|
-
requests = batch_body['requests']
|
9
|
-
validate_requests(requests, limit)
|
10
|
-
|
11
|
-
requests.each do |request|
|
12
|
-
validate_request(request)
|
13
|
-
end
|
14
|
-
|
15
|
-
requests
|
16
|
-
end
|
17
|
-
|
18
|
-
private
|
19
|
-
|
20
|
-
def decode_body(body)
|
21
|
-
raise RequestBodyError.new('Request body is blank') unless body.length > 0
|
22
|
-
|
23
|
-
begin
|
24
|
-
batch_body = MultiJson.decode(body)
|
25
|
-
rescue MultiJson::ParseError
|
26
|
-
raise RequestBodyError.new('Request body is not valid JSON')
|
27
|
-
end
|
28
|
-
|
29
|
-
raise RequestBodyError.new('Request body is nil') unless batch_body
|
30
|
-
raise RequestBodyError.new('Request body is not well formatted') unless batch_body.is_a?(Hash)
|
31
|
-
|
32
|
-
batch_body
|
33
|
-
end
|
34
|
-
|
35
|
-
def validate_requests(batch_requests, limit)
|
36
|
-
raise RequestBodyError.new("'requests' object is missing in request body") unless batch_requests
|
37
|
-
raise RequestBodyError.new("'requests' is not well formatted") unless batch_requests.is_a?(Array)
|
38
|
-
raise TooManyRequestsError.new('Batch requests limit exceeded') if batch_requests.count > limit
|
39
|
-
end
|
40
|
-
|
41
|
-
def validate_request(request)
|
42
|
-
raise RequestBodyError.new("'method' is missing in one of request objects") unless request['method']
|
43
|
-
raise RequestBodyError.new("'method' is invalid in one of request objects") unless request['method'].is_a?(String)
|
44
|
-
|
45
|
-
unless %w(GET POST PUT DELETE).include?(request['method'])
|
46
|
-
raise RequestBodyError.new("'method' is invalid in one of request objects")
|
47
|
-
end
|
48
|
-
|
49
|
-
raise RequestBodyError.new("'path' is missing in one of request objects") unless request['path']
|
50
|
-
raise RequestBodyError.new("'path' is invalid in one of request objects") unless request['path'].is_a?(String)
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|