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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4e44f52d84088c227271799372c1dc9cdd570b65
4
- data.tar.gz: bb3eebf865d2d329e81ad97e85655b1035faf4fd
3
+ metadata.gz: 2ba051e13b667b77094be62bf479aebf5452fc3e
4
+ data.tar.gz: f28bd7313a6376bd8a56792e1744dbadf63d19b3
5
5
  SHA512:
6
- metadata.gz: 784cc47d4012ce202e0c089d50dd85fde29388a44758c01f7066ed9ef46c651c5eade41d4089d4753075f5a6722c95486685bcf3511e41bd0d146724c70ca16b
7
- data.tar.gz: 133f5ba1ed2ca8efe5252acb652fb6dc15f6f07cf72b38d6ac32ee641d388b0ac7ac7030956b0bfab8d5bc4255a5393eca2646544e6e82191b7c165d51e6cd35
6
+ metadata.gz: 7155185197a009f8a884a622d05cfb3f877f2990bffa0382c9424c3cf004b67f448e79a1b116a63d39979ad20dd5e2c550201fcf01a51d30f622a7ec7dafcfd5
7
+ data.tar.gz: 5d83dae879c57aa6ce723e4df16528b82fd04d6eb8d67bca4a53e4fa87fc5ba8de699cea6b2aaca4c1df12c8701e99a73e87449052a98d14b50f0c4b92f4709c
data/.rubocop.yml ADDED
@@ -0,0 +1,12 @@
1
+ require: rubocop-rspec
2
+
3
+ Metrics/LineLength:
4
+ Max: 99
5
+
6
+ # Indentation of `when`.
7
+ Style/CaseIndentation:
8
+ IndentWhenRelativeTo: case
9
+ SupportedStyles:
10
+ - case
11
+ - end
12
+ IndentOneStep: true
data/Rakefile CHANGED
@@ -1 +1 @@
1
- require "bundler/gem_tasks"
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 = 'grape-batch'
8
- spec.version = Grape::Batch::VERSION
9
- spec.authors = ['Lionel Oto', 'Vincent Falduto', 'Cédric Darné']
10
- spec.email = ['lionel.oto@c4mprod.com', 'vincent.falduto@c4mprod.com', 'cedric.darne@c4mprod.com']
11
- spec.summary = %q{Extends Grape::API to support request batching }
12
- spec.homepage = 'https://github.com/c4mprod/grape-batch'
13
- spec.license = 'MIT'
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 = `git ls-files -z`.split("\x0")
16
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
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 'rack-test', '~> 0.6.2'
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 = Proc.new {}
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
@@ -1,6 +1,89 @@
1
1
  module Grape
2
2
  module Batch
3
- class RequestBodyError < ArgumentError; end
4
- class TooManyRequestsError < StandardError; end
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
@@ -1,10 +1,19 @@
1
- class Grape::Batch::Response
2
- def self.format(status, headers, response)
3
- if response
4
- body = response.respond_to?(:body) ? response.body.join : response.join
5
- result = MultiJson.decode(body)
6
- end
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
- (200..299).include?(status) ? {success: result} : {code: status, error: result['error']}
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
@@ -1,5 +1,6 @@
1
1
  module Grape
2
+ # Gem main module
2
3
  module Batch
3
- VERSION = '2.1.0'
4
+ VERSION = '2.1.1'
4
5
  end
5
6
  end
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/hash_converter'
6
- require 'grape/batch/parser'
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 is_batch_request?(env)
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
- batch_requests = Grape::Batch::Validator::parse(env, Grape::Batch.configuration.limit)
31
- result = dispatch(env, batch_requests)
32
- body = MultiJson.encode(result)
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
- logger.info("--- Grape::Batch #{request_id} END")
38
- [status, headers, [body]]
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 is_batch_request?(env)
44
- env['PATH_INFO'].start_with?(Grape::Batch.configuration.path) &&
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
- env['api.session'] = Grape::Batch.configuration.session_proc.call(env)
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
- def prepare_tmp_env(tmp_env, request)
65
- method = request['method']
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
- if method == 'GET'
72
- query_string = URI.encode_www_form(HashConverter.encode(body).to_a)
73
- else
74
- rack_input = StringIO.new(MultiJson.encode(body))
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: "User id."
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: "User id."
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: "User id."
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) { Grape::Batch::Base.new(@app) }
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({ error: 'Failed as expected' })) }
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(:response) { request.post('/batch', { 'CONTENT_TYPE' => 'application/json', input: request_body }) }
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({ requests: 'request' }) }
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({ requests: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] }) }
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({ requests: [{}] }) }
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({ requests: [{ method: true }] }) }
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({ requests: [{ method: 'TRACE' }] }) }
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({ requests: [{ method: 'GET' }] }) }
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({ requests: [{ method: 'GET', path: 123 }] }) }
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({ requests: [{ method: 'GET', path: '/api/v1/hello' }] }) }
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({ requests: [{ method: 'GET', path: '/api/v1/user/856' }] }) }
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(:request_body) { encode({ requests: [{ method: 'GET', path: '/api/v1/status', body: { id: 856 } }] }) }
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(:complex) do
139
- { a: { b: { c: 1 } } }
140
- end
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: "hash 1" }])) }
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({ requests: [{ method: 'GET', path: '/api/v1/unknown' }] }) }
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([{ code: 404, error: '/api/v1/unknown not found' }])) }
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({ requests: [{ method: 'POST', path: '/api/v1/hello' }] }) }
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(:request_body) { encode({ requests: [{ method: 'POST', path: '/api/v1/status', body: { id: 856 } }] }) }
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(:request_body) { encode({ requests: [{ method: 'POST', path: '/api/v1/hello' }, { method: 'GET', path: '/api/v1/user/856' }] }) }
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 = Proc.new { 3 + 2 }
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
- # Use the documentation formatter for detailed output,
63
- # unless a formatter has already been configured
64
- # (e.g. via a command-line flag).
65
- config.default_formatter = 'doc'
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.0
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-04 00:00:00.000000000 Z
13
+ date: 2015-11-05 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
- name: grape
16
+ name: multi_json
17
17
  requirement: !ruby/object:Gem::Requirement
18
18
  requirements:
19
19
  - - ">="
20
20
  - !ruby/object:Gem::Version
21
- version: 0.7.0
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: 0.7.0
28
+ version: '1.0'
29
29
  - !ruby/object:Gem::Dependency
30
- name: multi_json
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: '1.0'
36
- type: :runtime
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: '1.0'
56
+ version: 0.7.0
43
57
  - !ruby/object:Gem::Dependency
44
- name: bundler
58
+ name: rack-test
45
59
  requirement: !ruby/object:Gem::Requirement
46
60
  requirements:
47
61
  - - "~>"
48
62
  - !ruby/object:Gem::Version
49
- version: '1.6'
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: '1.6'
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: rack-test
100
+ name: rubocop
87
101
  requirement: !ruby/object:Gem::Requirement
88
102
  requirements:
89
103
  - - "~>"
90
104
  - !ruby/object:Gem::Version
91
- version: 0.6.2
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.6.2
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/hash_converter.rb
123
- - lib/grape/batch/parser.rb
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/requests_spec.rb
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/requests_spec.rb
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
@@ -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