grape-batch 2.1.0 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
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