batch_api 0.1.1 → 0.2.0

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.
Files changed (81) hide show
  1. data/changelog.md +17 -0
  2. data/lib/batch_api.rb +6 -1
  3. data/lib/batch_api/batch_error.rb +41 -0
  4. data/lib/batch_api/configuration.rb +31 -21
  5. data/lib/batch_api/error_wrapper.rb +44 -0
  6. data/lib/batch_api/internal_middleware.rb +87 -0
  7. data/lib/batch_api/internal_middleware/decode_json_body.rb +24 -0
  8. data/lib/batch_api/internal_middleware/response_filter.rb +27 -0
  9. data/lib/batch_api/operation/rack.rb +4 -5
  10. data/lib/batch_api/processor.rb +22 -20
  11. data/lib/batch_api/processor/executor.rb +18 -0
  12. data/lib/batch_api/processor/sequential.rb +29 -0
  13. data/lib/batch_api/{middleware.rb → rack_middleware.rb} +2 -2
  14. data/lib/batch_api/response.rb +10 -8
  15. data/lib/batch_api/version.rb +1 -1
  16. data/readme.md +179 -106
  17. data/spec/dummy/README.rdoc +261 -0
  18. data/spec/dummy/Rakefile +15 -0
  19. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  20. data/spec/dummy/app/assets/javascripts/endpoints.js +2 -0
  21. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  22. data/spec/dummy/app/assets/stylesheets/endpoints.css +4 -0
  23. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  24. data/spec/dummy/app/controllers/endpoints_controller.rb +36 -0
  25. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  26. data/spec/dummy/app/helpers/endpoints_helper.rb +2 -0
  27. data/spec/dummy/app/views/endpoints/get.html.erb +2 -0
  28. data/spec/dummy/app/views/endpoints/post.html.erb +2 -0
  29. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  30. data/spec/dummy/config.ru +4 -0
  31. data/spec/dummy/config/application.rb +63 -0
  32. data/spec/dummy/config/boot.rb +10 -0
  33. data/spec/dummy/config/database.yml +25 -0
  34. data/spec/dummy/config/environment.rb +5 -0
  35. data/spec/dummy/config/environments/development.rb +37 -0
  36. data/spec/dummy/config/environments/production.rb +67 -0
  37. data/spec/dummy/config/environments/test.rb +37 -0
  38. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  39. data/spec/dummy/config/initializers/inflections.rb +15 -0
  40. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  41. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  42. data/spec/dummy/config/initializers/session_store.rb +8 -0
  43. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  44. data/spec/dummy/config/locales/en.yml +5 -0
  45. data/spec/dummy/config/routes.rb +64 -0
  46. data/spec/dummy/db/development.sqlite3 +0 -0
  47. data/spec/dummy/db/test.sqlite3 +0 -0
  48. data/spec/dummy/log/development.log +1742 -0
  49. data/spec/dummy/log/test.log +48237 -0
  50. data/spec/dummy/public/404.html +26 -0
  51. data/spec/dummy/public/422.html +26 -0
  52. data/spec/dummy/public/500.html +25 -0
  53. data/spec/dummy/public/favicon.ico +0 -0
  54. data/spec/dummy/script/rails +6 -0
  55. data/spec/dummy/test/functional/endpoints_controller_test.rb +14 -0
  56. data/spec/dummy/test/unit/helpers/endpoints_helper_test.rb +4 -0
  57. data/spec/integration/rails_spec.rb +10 -0
  58. data/spec/integration/shared_examples.rb +256 -0
  59. data/spec/integration/sinatra_integration_spec.rb +14 -0
  60. data/spec/lib/batch_api_spec.rb +20 -0
  61. data/spec/lib/batch_error_spec.rb +23 -0
  62. data/spec/lib/configuration_spec.rb +30 -0
  63. data/spec/lib/error_wrapper_spec.rb +68 -0
  64. data/spec/lib/internal_middleware/decode_json_body_spec.rb +37 -0
  65. data/spec/lib/internal_middleware/response_filter_spec.rb +61 -0
  66. data/spec/lib/internal_middleware_spec.rb +91 -0
  67. data/spec/lib/operation/rack_spec.rb +243 -0
  68. data/spec/lib/operation/rails_spec.rb +100 -0
  69. data/spec/lib/processor/executor_spec.rb +22 -0
  70. data/spec/lib/processor/sequential_spec.rb +39 -0
  71. data/spec/lib/processor_spec.rb +134 -0
  72. data/spec/lib/rack_middleware_spec.rb +103 -0
  73. data/spec/lib/response_spec.rb +53 -0
  74. data/spec/spec_helper.rb +28 -0
  75. data/spec/support/sinatra_app.rb +54 -0
  76. metadata +148 -12
  77. data/lib/batch_api/error.rb +0 -3
  78. data/lib/batch_api/errors/base.rb +0 -45
  79. data/lib/batch_api/errors/operation.rb +0 -7
  80. data/lib/batch_api/errors/request.rb +0 -26
  81. data/lib/batch_api/processor/strategies/sequential.rb +0 -18
@@ -1,3 +1,20 @@
1
+ v0.2.0
2
+ * Refactor app to use internal middlewares for handling operations
3
+ * Refactor JSON decoding to a middleware
4
+ * Remove timestamp option
5
+
6
+ v0.1.4
7
+ * Refactor errors into ErrorWrapper/BatchError
8
+ * Allow specification of custom status codes raised for errors
9
+
10
+ v0.1.3
11
+ * Refactor config to use a struct
12
+ * Update readme to cover HTTP pipelining
13
+
14
+ v0.1.2
15
+ * Rewrite the readme
16
+ * Add travis icon
17
+
1
18
  v0.1.1
2
19
  * Fix dumb error
3
20
 
@@ -2,7 +2,12 @@ require 'batch_api/configuration'
2
2
  require 'batch_api/version'
3
3
  require 'batch_api/utils'
4
4
  require 'batch_api/processor'
5
- require 'batch_api/middleware'
5
+
6
+ require 'batch_api/internal_middleware'
7
+ require 'batch_api/rack_middleware'
8
+
9
+ require 'batch_api/error_wrapper'
10
+ require 'batch_api/batch_error'
6
11
 
7
12
  module BatchApi
8
13
 
@@ -0,0 +1,41 @@
1
+ module BatchApi
2
+ module Errors
3
+ # Public: a module that tags Batch API errors and provides a default
4
+ # status.
5
+ module BatchError
6
+ # Public: the status code for this type of error.
7
+ # Subclasses can change this as desired.
8
+ def status_code; 500; end
9
+ end
10
+
11
+ # Public: an error thrown if an invalid option is
12
+ # specificed.
13
+ class BadOptionError < ArgumentError
14
+ include BatchError
15
+ # Public: the status code for this error.
16
+ def status_code; 422; end
17
+ end
18
+
19
+ # Public: an error thrown if too many operations are provided.
20
+ class OperationLimitExceeded < ArgumentError
21
+ include BatchError
22
+ # Public: the status code for this error.
23
+ def status_code; 422; end
24
+ end
25
+
26
+ # Public: an error thrown if no operations are provided.
27
+ class NoOperationsError < ArgumentError
28
+ include BatchError
29
+ # Public: the status code for this error.
30
+ def status_code; 422; end
31
+ end
32
+
33
+ # Public: an error thrown if one of the batch operations
34
+ # is somehow invalid (missing key parameters, etc.).
35
+ class MalformedOperationError < ArgumentError
36
+ include BatchError
37
+ # Public: the status code for this error.
38
+ def status_code; 422; end
39
+ end
40
+ end
41
+ end
@@ -1,27 +1,37 @@
1
+ require 'batch_api/internal_middleware'
2
+
1
3
  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
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
+ #
15
+ # There are also two middleware-related options -- check out middleware.rb
16
+ # for more information.
17
+ # - global_middleware: any middlewares to use round the entire batch request
18
+ # (such as authentication, etc.)
19
+ # - per_op_middleware: any middlewares to run around each individual request
20
+ # (adding headers, decoding JSON, etc.)
21
+ CONFIGURATION_OPTIONS = {
22
+ verb: :post,
23
+ endpoint: "/batch",
24
+ limit: 50,
25
+ batch_middleware: InternalMiddleware::DEFAULT_BATCH_MIDDLEWARE,
26
+ operation_middleware: InternalMiddleware::DEFAULT_OPERATION_MIDDLEWARE
27
+ }
17
28
 
18
- # Default values for configuration variables
29
+ # Batch API Configuration
30
+ class Configuration < Struct.new(*CONFIGURATION_OPTIONS.keys)
31
+ # Public: initialize a new configuration option and apply the defaults.
19
32
  def initialize
20
- @verb = :post
21
- @endpoint = "/batch"
22
- @limit = 50
23
- @decode_json_responses = true
24
- @add_timestamp = true
33
+ super
34
+ CONFIGURATION_OPTIONS.each {|k, v| self[k] = v}
25
35
  end
26
36
  end
27
37
  end
@@ -0,0 +1,44 @@
1
+ module BatchApi
2
+ # Public: wrap 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 ErrorWrapper
6
+ # Public: create a new ErrorWrapper from an error object.
7
+ def initialize(error)
8
+ @error = error
9
+ @status_code = error.status_code if error.respond_to?(:status_code)
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 self.class.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, RackMiddleware.content_type, [MultiJson.dump(body)]]
31
+ end
32
+
33
+ # Public: the status code to return for the given error.
34
+ def status_code
35
+ @status_code || 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 self.expose_backtrace?
41
+ !Rails.env.production?
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,87 @@
1
+ require 'middleware'
2
+ require 'batch_api/processor/sequential'
3
+ require 'batch_api/processor/executor'
4
+ require 'batch_api/internal_middleware/decode_json_body'
5
+ require 'batch_api/internal_middleware/response_filter'
6
+
7
+ module BatchApi
8
+ # Public: the internal middleware system used to process batch requests.
9
+ # (Not to be confused with RackMiddleware, which handles incoming Rack
10
+ # request.) Based on Mitchel Hashimoto's middleware gem.
11
+ #
12
+ # There are actually two internal middleware stacks, one run around the
13
+ # entire request and the other run around each individual operation.
14
+ #
15
+ # The batch_middleware stack consists of two parts:
16
+ # 1) Custom middlewares - these middlewares will be run around the entire
17
+ # sequence of batch requests. Useful for global processing on a batch
18
+ # request; for instance, the timestamp middleware adds a timestamp based on
19
+ # when the original request was started. This should return an array of
20
+ # BatchApi::Response objects.
21
+ #
22
+ # 2) Processor - this automatically-provided middleware will execute all
23
+ # batch requests either sequentially (currently the only option) or in
24
+ # parallel (in the future). This will return an array of
25
+ # BatchApi::Response objects.
26
+ #
27
+ # The operation_middleware stack also consists of two parts:
28
+ # 1) Operation - these middlewares will run once per each operation, giving
29
+ # you a chance to alter the results or details of an op -- for instance,
30
+ # decoding the body if it's JSON. This should return an individual
31
+ # BatchApi::Response object.
32
+ #
33
+ # 2) Executor - this automatically-provided middleware actually executes
34
+ # the individual Rack request, and returns a BatchApi::Response object.
35
+ #
36
+ # Symetry, right?
37
+ #
38
+ # All middlewares#call will receive the following as an env hash:
39
+ # {
40
+ # # for batch middlewares
41
+ # ops: [], # the total set of operations
42
+ # # for operation middlewares
43
+ # op: obj, # the specific operation being executed
44
+ # # all middlewares get the following:
45
+ # rack_env: {}, # the Rack environment
46
+ # rack_app: app # the Rack application
47
+ # }
48
+ #
49
+ # All middlewares should return the result of their individual operation or
50
+ # the array of operation results, depending on where they are in the chain.
51
+ # (See above.)
52
+ module InternalMiddleware
53
+ # Public: the default internal middlewares to be run around the entire
54
+ # operation.
55
+ DEFAULT_BATCH_MIDDLEWARE = Proc.new {}
56
+
57
+ # Public: the default internal middlewares to be run around each batch
58
+ # operation.
59
+ DEFAULT_OPERATION_MIDDLEWARE = Proc.new do
60
+ # Decode JSON response bodies, so they're not double-encoded.
61
+ use InternalMiddleware::ResponseFilter
62
+ use InternalMiddleware::DecodeJsonBody
63
+ end
64
+
65
+ # Public: the middleware stack to use for processing the batch request as a
66
+ # whole..
67
+ def self.batch_stack(processor)
68
+ Middleware::Builder.new do
69
+ # evaluate these in the context of the middleware stack
70
+ self.instance_eval &BatchApi.config.batch_middleware
71
+ # for now, everything's sequential, but that will change
72
+ use processor.strategy
73
+ end
74
+ end
75
+ #
76
+ # Public: the middleware stack to use for processing each batch operation.
77
+ def self.operation_stack
78
+ Middleware::Builder.new do
79
+ # evaluate the operation middleware in the context of the middleware
80
+ # stack
81
+ self.instance_eval &BatchApi.config.operation_middleware
82
+ # and end with actually executing the batch request
83
+ use Processor::Executor
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,24 @@
1
+ module BatchApi
2
+ module InternalMiddleware
3
+ # Public: a middleware that decodes the body of any individual batch
4
+ # operation if the it's JSON.
5
+ class DecodeJsonBody
6
+ # Public: initialize the middleware.
7
+ def initialize(app)
8
+ @app = app
9
+ end
10
+
11
+ def call(env)
12
+ @app.call(env).tap do |result|
13
+ result.body = MultiJson.load(result.body) if should_decode?(result)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def should_decode?(result)
20
+ result.headers["Content-Type"] =~ /^application\/json/
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,27 @@
1
+ module BatchApi
2
+ module InternalMiddleware
3
+ # Public: a batch middleware which surpresses the response from a call. If you
4
+ # know you don't need a response (for instance, for a POST or PUT), you can
5
+ # add silent: true (or any truthy value, like 1) to your operation to
6
+ # surpress all output for successful requests. Failed requests (status !=
7
+ # 2xx) will still return information.
8
+ class ResponseFilter
9
+ # Public: init the middleware.
10
+ def initialize(app)
11
+ @app = app
12
+ end
13
+
14
+ # Public: execute the call. If env[:op].options[:silent] is true, it'll
15
+ # remove any output for a successful response.
16
+ def call(env)
17
+ @app.call(env).tap do |result|
18
+ if env[:op].options["silent"] && (200...299).include?(result.status)
19
+ # we have success and a request for silence
20
+ # so remove all the content before proceeding
21
+ result.status = result.body = result.headers = nil
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -3,11 +3,9 @@ require 'batch_api/response'
3
3
  module BatchApi
4
4
  # Public: an individual batch operation.
5
5
  module Operation
6
- class MalformedOperationError < ArgumentError; end
7
-
8
6
  class Rack
9
7
  attr_accessor :method, :url, :params, :headers
10
- attr_accessor :env, :app, :result
8
+ attr_accessor :env, :app, :result, :options
11
9
 
12
10
  # Public: create a new Batch Operation given the specifications for a batch
13
11
  # operation (as defined above) and the request environment for the main
@@ -19,8 +17,9 @@ module BatchApi
19
17
  @url = op["url"]
20
18
  @params = op["params"] || {}
21
19
  @headers = op["headers"] || {}
20
+ @options = op
22
21
 
23
- raise MalformedOperationError,
22
+ raise Errors::MalformedOperationError,
24
23
  "BatchAPI operation must include method (received #{@method.inspect}) " +
25
24
  "and url (received #{@url.inspect})" unless @method && @url
26
25
 
@@ -36,7 +35,7 @@ module BatchApi
36
35
  begin
37
36
  response = @app.call(@env)
38
37
  rescue => err
39
- response = BatchApi::Errors::Operation.new(err).render
38
+ response = BatchApi::ErrorWrapper.new(err).render
40
39
  end
41
40
  BatchApi::Response.new(response)
42
41
  end
@@ -1,16 +1,8 @@
1
- require 'batch_api/processor/strategies/sequential'
1
+ require 'batch_api/processor/sequential'
2
2
  require 'batch_api/operation'
3
3
 
4
4
  module BatchApi
5
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
6
  attr_reader :ops, :options, :app
15
7
 
16
8
  # Public: create a new Processor.
@@ -20,7 +12,7 @@ module BatchApi
20
12
  #
21
13
  # Raises OperationLimitExceeded if more operations are requested than
22
14
  # allowed by the BatchApi configuration.
23
- # Raises BadOptionError if other provided options are invalid.
15
+ # Raises Errors::BadOptionError if other provided options are invalid.
24
16
  # Raises ArgumentError if no operations are provided (nil or []).
25
17
  #
26
18
  # Returns the new Processor instance.
@@ -30,26 +22,34 @@ module BatchApi
30
22
  @env = request.env
31
23
  @ops = self.process_ops
32
24
  @options = self.process_options
33
-
34
- @start_time = Time.now.to_i
35
25
  end
36
26
 
37
27
  # Public: the processing strategy to use, based on the options
38
28
  # provided in BatchApi setup and the request.
39
29
  # Currently only Sequential is supported.
40
30
  def strategy
41
- BatchApi::Processor::Strategies::Sequential
31
+ BatchApi::Processor::Sequential
42
32
  end
43
33
 
44
34
  # Public: run the batch operations according to the appropriate strategy.
45
35
  #
46
36
  # Returns a set of BatchResponses
47
37
  def execute!
48
- format_response(strategy.execute!(@ops, @options))
38
+ stack = InternalMiddleware.batch_stack(self)
39
+ format_response(stack.call(middleware_env))
49
40
  end
50
41
 
51
42
  protected
52
43
 
44
+ def middleware_env
45
+ {
46
+ ops: @ops,
47
+ rack_env: @env,
48
+ rack_app: @app,
49
+ options: @options
50
+ }
51
+ end
52
+
53
53
  # Internal: format the result of the operations, and include
54
54
  # any other appropriate information (such as timestamp).
55
55
  #
@@ -58,8 +58,7 @@ module BatchApi
58
58
  # Returns a hash ready to go to the user
59
59
  def format_response(operation_results)
60
60
  {
61
- "results" => operation_results,
62
- "timestamp" => @start_time.to_s
61
+ "results" => operation_results
63
62
  }
64
63
  end
65
64
 
@@ -68,16 +67,17 @@ module BatchApi
68
67
  #
69
68
  # ops - a series of operations
70
69
  #
71
- # Raises OperationLimitExceeded if more operations are requested than
70
+ # Raises Errors::OperationLimitExceeded if more operations are requested than
72
71
  # allowed by the BatchApi configuration.
72
+ # Raises Errors::NoOperationsError if no operations are provided.
73
73
  #
74
74
  # Returns an array of BatchApi::Operation objects
75
75
  def process_ops
76
76
  ops = @request.params.delete("ops")
77
77
  if !ops || ops.empty?
78
- raise NoOperationsError, "No operations provided"
78
+ raise Errors::NoOperationsError, "No operations provided"
79
79
  elsif ops.length > BatchApi.config.limit
80
- raise OperationLimitExceeded,
80
+ raise Errors::OperationLimitExceeded,
81
81
  "Only #{BatchApi.config.limit} operations can be submitted at once, " +
82
82
  "#{ops.length} were provided"
83
83
  else
@@ -100,10 +100,12 @@ module BatchApi
100
100
  #
101
101
  # options - an options hash
102
102
  #
103
+ # Raises Errors::BadOptionError if sequential is not provided.
104
+ #
103
105
  # Returns the valid options hash.
104
106
  def process_options
105
107
  unless @request.params["sequential"]
106
- raise BadOptionError, "Sequential flag is currently required"
108
+ raise Errors::BadOptionError, "Sequential flag is currently required"
107
109
  end
108
110
  @request.params
109
111
  end