batch_api 0.0.1 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
data/changelog.md CHANGED
@@ -1,2 +1,29 @@
1
+ v0.0.8
2
+ * Return the results wrapped in a hash, rather than a raw array
3
+ * Add process_start timestamp option
4
+
5
+ v0.0.7
6
+ * Return more specific error codes to alert clients to param errors
7
+
8
+ v0.0.6
9
+ * Refactor Rack middleware to be Sinatra-compatible
10
+
11
+ v0.0.5
12
+ * Add setting to decode JSON responses before sending batch results
13
+
14
+ v0.0.4
15
+ * Switch from Rails-based process to a Rack middleware
16
+ * Improve tests
17
+
18
+ v0.0.3
19
+ * Encapsulate processing into a Processor module
20
+ * Prepare for parallel processing in the future
21
+ * Add specific errors
22
+ * Allow controlling the routing target
23
+
24
+ v0.0.2
25
+ * Add config module
26
+ * Add options for operation limit, endpoint, and verb
27
+
1
28
  v0.0.1
2
29
  * Initial build
data/lib/batch_api.rb CHANGED
@@ -1,6 +1,11 @@
1
- require 'batch_api/routing_helper'
2
- require 'batch_api/engine'
1
+ require 'batch_api/configuration'
3
2
  require 'batch_api/version'
3
+ require 'batch_api/utils'
4
+ require 'batch_api/processor'
5
+ require 'batch_api/middleware'
4
6
 
5
7
  module BatchApi
8
+ def self.config
9
+ @config ||= Configuration.new
10
+ end
6
11
  end
@@ -0,0 +1,29 @@
1
+ module BatchApi
2
+ # Batch API Configuration
3
+ class Configuration
4
+ # Public: configuration options.
5
+ # Currently, you can set:
6
+ # - endpoint: (URL) through which the Batch API will be exposed (default
7
+ # "/batch)
8
+ # - verb: through which it's accessed (default "POST")
9
+ # - limit: how many requests can be processed in a single request
10
+ # (default 50)
11
+ # decode_json_responses - automatically decode JSON response bodies,
12
+ # so they don't get double-decoded (e.g. when you decode the batch
13
+ # response, the bodies are already objects).
14
+ attr_accessor :verb, :endpoint, :limit
15
+ attr_accessor :decode_json_responses
16
+ attr_accessor :add_timestamp
17
+
18
+ # Default values for configuration variables
19
+ def initialize
20
+ @verb = :post
21
+ @endpoint = "/batch"
22
+ @limit = 50
23
+ @decode_json_responses = true
24
+ @add_timestamp = true
25
+ end
26
+ end
27
+ end
28
+
29
+
@@ -1,32 +1,3 @@
1
- module BatchApi
2
- # Public: an error thrown during a batch operation.
3
- # This has a body class and a cookies accessor and can
4
- # function in place of a regular BatchResponse object.
5
- class Error
6
- # Public: create a new BatchError from a Rails error.
7
- def initialize(error)
8
- @message = error.message
9
- @backtrace = error.backtrace
10
- end
1
+ require 'batch_api/errors/request'
2
+ require 'batch_api/errors/operation'
11
3
 
12
- # Public: here for compatibility with BatchResponse interface.
13
- attr_reader :cookies
14
-
15
- # Public: the error details as a hash, which can be returned
16
- # to clients as JSON.
17
- def body
18
- if expose_backtrace?
19
- {
20
- message: @message,
21
- backtrace: @backtrace
22
- }
23
- else
24
- { message: @message }
25
- end
26
- end
27
-
28
- def expose_backtrace?
29
- Rails.env.production?
30
- end
31
- end
32
- end
@@ -0,0 +1,45 @@
1
+ module BatchApi
2
+ # Public: an error thrown during a batch operation.
3
+ # This has a body class and a cookies accessor and can
4
+ # function in place of a regular BatchResponse object.
5
+ module Errors
6
+ class Base
7
+ # Public: create a new BatchError from a Rails error.
8
+ def initialize(error)
9
+ @error = error
10
+ end
11
+
12
+ # Public: the error details as a hash, which can be returned
13
+ # to clients as JSON.
14
+ def body
15
+ message = if expose_backtrace?
16
+ {
17
+ message: @error.message,
18
+ backtrace: @error.backtrace
19
+ }
20
+ else
21
+ { message: @error.message }
22
+ end
23
+ { error: message }
24
+ end
25
+
26
+ # Public: turn the error body into a Rack-compatible body component.
27
+ #
28
+ # Returns: an Array with the error body represented as JSON.
29
+ def render
30
+ [status_code, Middleware.content_type, [MultiJson.dump(body)]]
31
+ end
32
+
33
+ # Public: the status code to return for the given error.
34
+ def status_code
35
+ 500
36
+ end
37
+
38
+ # Internal: whether the backtrace should be exposed in the response.
39
+ # Currently Rails-specific, needs to be generalized (to ENV["RACK_ENV"])?
40
+ def expose_backtrace?
41
+ !Rails.env.production?
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,7 @@
1
+ require 'batch_api/errors/base'
2
+
3
+ module BatchApi
4
+ module Errors
5
+ class Operation < Base; end
6
+ end
7
+ end
@@ -0,0 +1,26 @@
1
+ require 'batch_api/errors/base'
2
+
3
+ module BatchApi
4
+ module Errors
5
+ # Public: This class encapsulates errors that occur at a request level.
6
+ # For instance, it returns proper error codes for BadOptionErrors or other
7
+ # identifiable problems. (For actual code errors, it returns a 500
8
+ # response.)
9
+ class Request < BatchApi::Errors::Base
10
+ # Public: return the appropriate status code for the error. For
11
+ # errors from bad Batch API input, raise a 422, otherwise, a 500.
12
+ def status_code
13
+ case @error
14
+ when BatchApi::Processor::BadOptionError,
15
+ BatchApi::Processor::OperationLimitExceeded,
16
+ BatchApi::Processor::NoOperationsError,
17
+ BatchApi::Operation::MalformedOperationError
18
+ 422
19
+ else
20
+ 500
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+
@@ -0,0 +1,37 @@
1
+ module BatchApi
2
+ class Middleware
3
+ def initialize(app, &block)
4
+ @app = app
5
+ yield BatchApi.config if block
6
+ end
7
+
8
+ def call(env)
9
+ if batch_request?(env)
10
+ begin
11
+ request = request_klass.new(env)
12
+ result = BatchApi::Processor.new(request, @app).execute!
13
+ [200, self.class.content_type, [MultiJson.dump(result)]]
14
+ rescue => err
15
+ BatchApi::Errors::Request.new(err).render
16
+ end
17
+ else
18
+ @app.call(env)
19
+ end
20
+ end
21
+
22
+ def self.content_type
23
+ {"Content-Type" => "application/json"}
24
+ end
25
+
26
+ private
27
+
28
+ def batch_request?(env)
29
+ env["PATH_INFO"] == BatchApi.config.endpoint &&
30
+ env["REQUEST_METHOD"] == BatchApi.config.verb.to_s.upcase
31
+ end
32
+
33
+ def request_klass
34
+ defined?(ActionDispatch) ? ActionDispatch::Request : Rack::Request
35
+ end
36
+ end
37
+ end
@@ -3,46 +3,41 @@ require 'batch_api/response'
3
3
  module BatchApi
4
4
  # Public: an individual batch operation.
5
5
  class Operation
6
+ class MalformedOperationError < ArgumentError; end
7
+
6
8
  attr_accessor :method, :url, :params, :headers
7
- attr_accessor :env, :result
9
+ attr_accessor :env, :app, :result
8
10
 
9
11
  # Public: create a new Batch Operation given the specifications for a batch
10
12
  # operation (as defined above) and the request environment for the main
11
13
  # batch request.
12
- def initialize(op, base_env)
14
+ def initialize(op, base_env, app)
13
15
  @op = op
14
16
 
15
- @method = op[:method]
16
- @url = op[:url]
17
- @params = op[:params]
18
- @headers = op[:headers]
17
+ @method = op["method"]
18
+ @url = op["url"]
19
+ @params = op["params"] || {}
20
+ @headers = op["headers"] || {}
21
+
22
+ raise MalformedOperationError,
23
+ "BatchAPI operation must include method (received #{@method.inspect}) " +
24
+ "and url (received #{@url.inspect})" unless @method && @url
19
25
 
26
+ @app = app
20
27
  # deep_dup to avoid unwanted changes across requests
21
- @env = base_env.deep_dup
28
+ @env = BatchApi::Utils.deep_dup(base_env)
22
29
  end
23
30
 
24
31
  # Execute a batch request, returning a BatchResponse object. If an error
25
32
  # occurs, it returns the same results as Rails would.
26
33
  def execute
34
+ process_env
27
35
  begin
28
- action = identify_routing
29
- process_env
30
- BatchApi::Response.new(action.call(@env))
36
+ response = @app.call(@env)
31
37
  rescue => err
32
- error_response(err)
38
+ response = BatchApi::Errors::Operation.new(err).render
33
39
  end
34
- end
35
-
36
- # Internal: given a URL and other operation details as specified above,
37
- # identify the appropriate controller and action to execute the action.
38
- #
39
- # Raises a routing error if the route doesn't exist.
40
- #
41
- # Returns the action object, which can be called with the environment.
42
- def identify_routing
43
- @path_params = Rails.application.routes.recognize_path(@url, @op)
44
- @controller = ActionDispatch::Routing::RouteSet::Dispatcher.new.controller(@path_params)
45
- @controller.action(@path_params[:action])
40
+ BatchApi::Response.new(response)
46
41
  end
47
42
 
48
43
  # Internal: customize the request environment. This is currently done
@@ -51,12 +46,6 @@ module BatchApi
51
46
  def process_env
52
47
  path, qs = @url.split("?")
53
48
 
54
- # rails routing
55
- @env["action_dispatch.request.path_parameters"] = @path_params
56
- # this isn't quite right, but hopefully it'll work
57
- # since we're not executing any middleware
58
- @env["action_controller.instance"] = @controller.new
59
-
60
49
  # Headers
61
50
  headrs = (@headers || {}).inject({}) do |heads, (k, v)|
62
51
  heads.tap {|h| h["HTTP_" + k.gsub(/\-/, "_").upcase] = v}
@@ -72,23 +61,14 @@ module BatchApi
72
61
  @env["REQUEST_PATH"] = path
73
62
  @env["ORIGINAL_FULLPATH"] = @env["PATH_INFO"] = @url
74
63
 
75
- @env["rack.request.query_string"] = @env["QUERY_STRING"] = qs
64
+ @env["rack.request.query_string"] = qs
65
+ @env["QUERY_STRING"] = qs
76
66
 
77
67
  # parameters
68
+ @env["rack.request.form_hash"] = @params
78
69
  @env["action_dispatch.request.parameters"] = @params
79
70
  @env["action_dispatch.request.request_parameters"] = @params
80
71
  @env["rack.request.query_hash"] = @method == "get" ? @params : nil
81
72
  end
82
-
83
- # Internal: create a BatchResponse for an exception thrown during batch
84
- # processing.
85
- def error_response(err)
86
- wrapper = ActionDispatch::ExceptionWrapper.new(@env, err)
87
- BatchApi::Response.new([
88
- wrapper.status_code,
89
- {},
90
- BatchApi::Error.new(err)
91
- ])
92
- end
93
73
  end
94
74
  end
@@ -0,0 +1,104 @@
1
+ require 'batch_api/processor/strategies/sequential'
2
+ require 'batch_api/operation'
3
+
4
+ module BatchApi
5
+ class Processor
6
+ # Public: Raised when a user provides more Batch API requests than a service
7
+ # allows.
8
+ class OperationLimitExceeded < StandardError; end
9
+ # Public: Raised if a provided option is invalid.
10
+ class BadOptionError < StandardError; end
11
+ # Public: Raised if no operations are provided.
12
+ class NoOperationsError < ArgumentError; end
13
+
14
+ attr_reader :ops, :options, :app
15
+
16
+ # Public: create a new Processor.
17
+ #
18
+ # env - a Rack environment hash
19
+ # app - a Rack application
20
+ #
21
+ # Raises OperationLimitExceeded if more operations are requested than
22
+ # allowed by the BatchApi configuration.
23
+ # Raises BadOptionError if other provided options are invalid.
24
+ # Raises ArgumentError if no operations are provided (nil or []).
25
+ #
26
+ # Returns the new Processor instance.
27
+ def initialize(request, app)
28
+ @app = app
29
+ @request = request
30
+ @env = request.env
31
+ @ops = self.process_ops
32
+ @options = self.process_options
33
+
34
+ @start_time = Time.now.to_i
35
+ end
36
+
37
+ # Public: the processing strategy to use, based on the options
38
+ # provided in BatchApi setup and the request.
39
+ # Currently only Sequential is supported.
40
+ def strategy
41
+ BatchApi::Processor::Strategies::Sequential
42
+ end
43
+
44
+ # Public: run the batch operations according to the appropriate strategy.
45
+ #
46
+ # Returns a set of BatchResponses
47
+ def execute!
48
+ format_response(strategy.execute!(@ops, @options))
49
+ end
50
+
51
+ protected
52
+
53
+ # Internal: format the result of the operations, and include
54
+ # any other appropriate information (such as timestamp).
55
+ #
56
+ # result - the array of batch operations
57
+ #
58
+ # Returns a hash ready to go to the user
59
+ def format_response(operation_results)
60
+ {
61
+ "results" => operation_results,
62
+ "timestamp" => @start_time.to_s
63
+ }
64
+ end
65
+
66
+ # Internal: Validate that an allowable number of operations have been
67
+ # provided, and turn them into BatchApi::Operation objects.
68
+ #
69
+ # ops - a series of operations
70
+ #
71
+ # Raises OperationLimitExceeded if more operations are requested than
72
+ # allowed by the BatchApi configuration.
73
+ #
74
+ # Returns an array of BatchApi::Operation objects
75
+ def process_ops
76
+ ops = @request.params.delete("ops")
77
+ if !ops || ops.empty?
78
+ raise NoOperationsError, "No operations provided"
79
+ elsif ops.length > BatchApi.config.limit
80
+ raise OperationLimitExceeded,
81
+ "Only #{BatchApi.config.limit} operations can be submitted at once, " +
82
+ "#{ops.length} were provided"
83
+ else
84
+ ops.map do |op|
85
+ BatchApi::Operation.new(op, @env, @app)
86
+ end
87
+ end
88
+ end
89
+
90
+ # Internal: Processes any other provided options for validity.
91
+ # Currently, the :sequential option is REQUIRED (until parallel
92
+ # implementation is created).
93
+ #
94
+ # options - an options hash
95
+ #
96
+ # Returns the valid options hash.
97
+ def process_options
98
+ unless @request.params["sequential"]
99
+ raise BadOptionError, "Sequential flag is currently required"
100
+ end
101
+ @request.params
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,18 @@
1
+ module BatchApi
2
+ class Processor
3
+ module Strategies
4
+ module Sequential
5
+ # Public: execute all operations sequentially.
6
+ #
7
+ # ops - a set of BatchApi::Operations
8
+ # options - a set of options
9
+ #
10
+ # Returns an array of BatchApi::Response objects.
11
+ def self.execute!(ops, options = {})
12
+ ops.map(&:execute)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+
@@ -6,17 +6,30 @@ module BatchApi
6
6
  # outcome.
7
7
  class Response
8
8
  # Public: the attributes of the HTTP response.
9
- attr_accessor :status, :body, :headers, :cookies
9
+ attr_accessor :status, :body, :headers
10
10
 
11
11
  # Public: create a new response representation from a Rack-compatible
12
12
  # response (e.g. [status, headers, response_object]).
13
13
  def initialize(response)
14
- @status = response.first
15
- @headers = response[1]
14
+ @status, @headers = *response
15
+ @body = process_body(response[2])
16
+ end
17
+
18
+ private
19
+
20
+ def process_body(body_pieces)
21
+ # bodies have to respond to .each, but may otherwise
22
+ # not be suitable for JSON serialization
23
+ # (I'm looking at you, ActionDispatch::Response)
24
+ # so turn it into a string
25
+ base_body = ""
26
+ body_pieces.each {|str| base_body << str}
27
+ should_decode? ? MultiJson.load(base_body) : base_body
28
+ end
16
29
 
17
- response_object = response[2]
18
- @body = response_object.body
19
- @cookies = response_object.cookies
30
+ def should_decode?
31
+ @headers["Content-Type"] =~ /^application\/json/ &&
32
+ BatchApi.config.decode_json_responses
20
33
  end
21
34
  end
22
35
  end
@@ -0,0 +1,17 @@
1
+ module BatchApi
2
+ module Utils
3
+
4
+ def self.deep_dup(object)
5
+ if object.is_a?(Hash)
6
+ duplicate = object.dup
7
+ duplicate.each_pair do |k,v|
8
+ tv = duplicate[k]
9
+ duplicate[k] = tv.is_a?(Hash) && v.is_a?(Hash) ? deep_dup(tv) : v
10
+ end
11
+ duplicate
12
+ else
13
+ object
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,3 +1,3 @@
1
1
  module BatchApi
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.8"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: batch_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.8
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-08-13 00:00:00.000000000 Z
12
+ date: 2012-08-20 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -19,7 +19,7 @@ dependencies:
19
19
  - - ~>
20
20
  - !ruby/object:Gem::Version
21
21
  version: '3.2'
22
- type: :runtime
22
+ type: :development
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
25
25
  none: false
@@ -27,6 +27,22 @@ dependencies:
27
27
  - - ~>
28
28
  - !ruby/object:Gem::Version
29
29
  version: '3.2'
30
+ - !ruby/object:Gem::Dependency
31
+ name: sinatra
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
30
46
  - !ruby/object:Gem::Dependency
31
47
  name: rspec
32
48
  requirement: !ruby/object:Gem::Requirement
@@ -75,6 +91,22 @@ dependencies:
75
91
  - - ! '>='
76
92
  - !ruby/object:Gem::Version
77
93
  version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: rack-contrib
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
78
110
  description: A Batch API plugin that provides a RESTful syntax, allowing clients to
79
111
  make any number of REST calls with a single HTTP request.
80
112
  email:
@@ -83,12 +115,17 @@ executables: []
83
115
  extensions: []
84
116
  extra_rdoc_files: []
85
117
  files:
86
- - app/controllers/batch_api/batch_controller.rb
87
- - lib/batch_api/engine.rb
118
+ - lib/batch_api/configuration.rb
88
119
  - lib/batch_api/error.rb
120
+ - lib/batch_api/errors/base.rb
121
+ - lib/batch_api/errors/operation.rb
122
+ - lib/batch_api/errors/request.rb
123
+ - lib/batch_api/middleware.rb
89
124
  - lib/batch_api/operation.rb
125
+ - lib/batch_api/processor/strategies/sequential.rb
126
+ - lib/batch_api/processor.rb
90
127
  - lib/batch_api/response.rb
91
- - lib/batch_api/routing_helper.rb
128
+ - lib/batch_api/utils.rb
92
129
  - lib/batch_api/version.rb
93
130
  - lib/batch_api.rb
94
131
  - lib/tasks/batch_api_tasks.rake
@@ -110,7 +147,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
110
147
  version: '0'
111
148
  segments:
112
149
  - 0
113
- hash: -1071837955116255101
150
+ hash: -456340693841676630
114
151
  required_rubygems_version: !ruby/object:Gem::Requirement
115
152
  none: false
116
153
  requirements:
@@ -119,7 +156,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
119
156
  version: '0'
120
157
  segments:
121
158
  - 0
122
- hash: -1071837955116255101
159
+ hash: -456340693841676630
123
160
  requirements: []
124
161
  rubyforge_project:
125
162
  rubygems_version: 1.8.21
@@ -1,10 +0,0 @@
1
- require 'batch_api/operation'
2
-
3
- module BatchApi
4
- class BatchController < ::ApplicationController
5
- def batch
6
- ops = params[:ops].map {|o| BatchApi::Operation.new(o, request.env)}
7
- render :json => ops.map(&:execute)
8
- end
9
- end
10
- end
@@ -1,8 +0,0 @@
1
- # CURRENT FILE :: lib/team_page/engine.rb
2
- module BatchApi
3
- class Engine < Rails::Engine
4
- initializer "batch_api.add_routing_helper" do |app|
5
- ActionDispatch::Routing::Mapper.send(:include, BatchApi::RoutingHelper)
6
- end
7
- end
8
- end
@@ -1,12 +0,0 @@
1
- module BatchApi
2
- module RoutingHelper
3
- DEFAULT_VERB = :post
4
- DEFAULT_ENDPOINT = "/batch"
5
-
6
- def batch_api(options = {})
7
- endpoint = options.delete(:endpoint) || DEFAULT_ENDPOINT
8
- verb = options.delete(:via) || DEFAULT_VERB
9
- match({endpoint => "batch_api/batch#batch", via: verb}.merge(options))
10
- end
11
- end
12
- end