batch_api 0.0.1 → 0.0.8

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.
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