batch_api 0.1.1 → 0.2.0

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