adavidev_batch_api 0.2.1.pre.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/Rakefile +30 -0
  4. data/changelog.md +60 -0
  5. data/lib/adavidev_batch_api.rb +28 -0
  6. data/lib/batch_api/batch_error.rb +41 -0
  7. data/lib/batch_api/configuration.rb +36 -0
  8. data/lib/batch_api/error_wrapper.rb +44 -0
  9. data/lib/batch_api/internal_middleware/decode_json_body.rb +24 -0
  10. data/lib/batch_api/internal_middleware/response_filter.rb +27 -0
  11. data/lib/batch_api/internal_middleware.rb +87 -0
  12. data/lib/batch_api/operation/rack.rb +74 -0
  13. data/lib/batch_api/operation/rails.rb +42 -0
  14. data/lib/batch_api/operation.rb +2 -0
  15. data/lib/batch_api/processor/executor.rb +18 -0
  16. data/lib/batch_api/processor/sequential.rb +29 -0
  17. data/lib/batch_api/processor.rb +114 -0
  18. data/lib/batch_api/rack_middleware.rb +37 -0
  19. data/lib/batch_api/response.rb +38 -0
  20. data/lib/batch_api/utils.rb +17 -0
  21. data/lib/batch_api/version.rb +3 -0
  22. data/lib/batch_api.rb +28 -0
  23. data/lib/tasks/batch_api_tasks.rake +4 -0
  24. data/readme.md +243 -0
  25. data/spec/dummy/README.rdoc +261 -0
  26. data/spec/dummy/Rakefile +15 -0
  27. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  28. data/spec/dummy/app/assets/javascripts/endpoints.js +2 -0
  29. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  30. data/spec/dummy/app/assets/stylesheets/endpoints.css +4 -0
  31. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  32. data/spec/dummy/app/controllers/endpoints_controller.rb +36 -0
  33. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  34. data/spec/dummy/app/helpers/endpoints_helper.rb +2 -0
  35. data/spec/dummy/app/views/endpoints/get.html.erb +2 -0
  36. data/spec/dummy/app/views/endpoints/post.html.erb +2 -0
  37. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  38. data/spec/dummy/config/application.rb +63 -0
  39. data/spec/dummy/config/boot.rb +10 -0
  40. data/spec/dummy/config/database.yml +25 -0
  41. data/spec/dummy/config/environment.rb +5 -0
  42. data/spec/dummy/config/environments/development.rb +37 -0
  43. data/spec/dummy/config/environments/production.rb +67 -0
  44. data/spec/dummy/config/environments/test.rb +37 -0
  45. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  46. data/spec/dummy/config/initializers/inflections.rb +15 -0
  47. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  48. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  49. data/spec/dummy/config/initializers/session_store.rb +8 -0
  50. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  51. data/spec/dummy/config/locales/en.yml +5 -0
  52. data/spec/dummy/config/routes.rb +64 -0
  53. data/spec/dummy/config.ru +4 -0
  54. data/spec/dummy/db/test.sqlite3 +0 -0
  55. data/spec/dummy/public/404.html +26 -0
  56. data/spec/dummy/public/422.html +26 -0
  57. data/spec/dummy/public/500.html +25 -0
  58. data/spec/dummy/public/favicon.ico +0 -0
  59. data/spec/dummy/script/rails +6 -0
  60. data/spec/dummy/test/functional/endpoints_controller_test.rb +14 -0
  61. data/spec/dummy/test/unit/helpers/endpoints_helper_test.rb +4 -0
  62. data/spec/integration/rails_spec.rb +10 -0
  63. data/spec/integration/shared_examples.rb +267 -0
  64. data/spec/integration/sinatra_integration_spec.rb +14 -0
  65. data/spec/lib/batch_api_spec.rb +20 -0
  66. data/spec/lib/batch_error_spec.rb +23 -0
  67. data/spec/lib/configuration_spec.rb +30 -0
  68. data/spec/lib/error_wrapper_spec.rb +68 -0
  69. data/spec/lib/internal_middleware/decode_json_body_spec.rb +37 -0
  70. data/spec/lib/internal_middleware/response_filter_spec.rb +61 -0
  71. data/spec/lib/internal_middleware_spec.rb +91 -0
  72. data/spec/lib/operation/rack_spec.rb +240 -0
  73. data/spec/lib/operation/rails_spec.rb +100 -0
  74. data/spec/lib/processor/executor_spec.rb +22 -0
  75. data/spec/lib/processor/sequential_spec.rb +39 -0
  76. data/spec/lib/processor_spec.rb +134 -0
  77. data/spec/lib/rack_middleware_spec.rb +103 -0
  78. data/spec/lib/response_spec.rb +53 -0
  79. data/spec/spec_helper.rb +28 -0
  80. data/spec/support/sinatra_app.rb +54 -0
  81. metadata +264 -0
@@ -0,0 +1,114 @@
1
+ require 'batch_api/processor/sequential'
2
+ require 'batch_api/operation'
3
+
4
+ module BatchApi
5
+ class Processor
6
+ attr_reader :ops, :options, :app
7
+
8
+ # Public: create a new Processor.
9
+ #
10
+ # env - a Rack environment hash
11
+ # app - a Rack application
12
+ #
13
+ # Raises OperationLimitExceeded if more operations are requested than
14
+ # allowed by the BatchApi configuration.
15
+ # Raises Errors::BadOptionError if other provided options are invalid.
16
+ # Raises ArgumentError if no operations are provided (nil or []).
17
+ #
18
+ # Returns the new Processor instance.
19
+ def initialize(request, app)
20
+ @app = app
21
+ @request = request
22
+ @env = request.env
23
+ @ops = self.process_ops
24
+ @options = self.process_options
25
+ end
26
+
27
+ # Public: the processing strategy to use, based on the options
28
+ # provided in BatchApi setup and the request.
29
+ # Currently only Sequential is supported.
30
+ def strategy
31
+ BatchApi::Processor::Sequential
32
+ end
33
+
34
+ # Public: run the batch operations according to the appropriate strategy.
35
+ #
36
+ # Returns a set of BatchResponses
37
+ def execute!
38
+ stack = InternalMiddleware.batch_stack(self)
39
+ format_response(stack.call(middleware_env))
40
+ end
41
+
42
+ protected
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
+ # 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
+ }
63
+ end
64
+
65
+ # Internal: Validate that an allowable number of operations have been
66
+ # provided, and turn them into BatchApi::Operation objects.
67
+ #
68
+ # ops - a series of operations
69
+ #
70
+ # Raises Errors::OperationLimitExceeded if more operations are requested than
71
+ # allowed by the BatchApi configuration.
72
+ # Raises Errors::NoOperationsError if no operations are provided.
73
+ #
74
+ # Returns an array of BatchApi::Operation objects
75
+ def process_ops
76
+ ops = JSON.parse(@request.body.string)["ops"]
77
+ if !ops || ops.empty?
78
+ raise Errors::NoOperationsError, "No operations provided"
79
+ elsif ops.length > BatchApi.config.limit
80
+ raise Errors::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
+ self.class.operation_klass.new(op, @env, @app)
86
+ end
87
+ end
88
+ end
89
+
90
+ # Internal: which operation class to used.
91
+ #
92
+ # Returns Batch::Operation::(Rack|Rails) depending on the environment
93
+ def self.operation_klass
94
+ BatchApi.rails? ? Operation::Rails : Operation::Rack
95
+ end
96
+
97
+ # Internal: Processes any other provided options for validity.
98
+ # Currently, the :sequential option is REQUIRED (until parallel
99
+ # implementation is created).
100
+ #
101
+ # options - an options hash
102
+ #
103
+ # Raises Errors::BadOptionError if sequential is not provided.
104
+ #
105
+ # Returns the valid options hash.
106
+ def process_options
107
+ # unless @request.params["sequential"]
108
+ # raise Errors::BadOptionError, "Sequential flag is currently required"
109
+ # end
110
+ @request.params["sequential"] ||= true
111
+ @request.params
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,37 @@
1
+ module BatchApi
2
+ class RackMiddleware
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
+ ErrorWrapper.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
@@ -0,0 +1,38 @@
1
+ module BatchApi
2
+ # Public: a response from an internal operation in the Batch API.
3
+ # It contains all the details that are needed to describe the call's
4
+ # outcome.
5
+ class Response
6
+ # Public: the attributes of the HTTP response.
7
+ attr_accessor :status, :body, :headers
8
+
9
+ # Public: create a new response representation from a Rack-compatible
10
+ # response (e.g. [status, headers, response_object]).
11
+ def initialize(response)
12
+ @status, @headers = *response
13
+ @body = process_body(response[2])
14
+ end
15
+
16
+ # Public: convert the response to JSON. nil values are ignored.
17
+ def as_json(options = {})
18
+ {}.tap do |result|
19
+ result[:body] = @body unless @body.nil?
20
+ result[:headers] = @headers unless @headers.nil?
21
+ result[:status] = @status unless @status.nil?
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def process_body(body_pieces)
28
+ # bodies have to respond to .each, but may otherwise
29
+ # not be suitable for JSON serialization
30
+ # (I'm looking at you, ActionDispatch::Response)
31
+ # so turn it into a string
32
+ base_body = ""
33
+ body_pieces.each {|str| base_body << str}
34
+ base_body
35
+ end
36
+ end
37
+ end
38
+
@@ -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
@@ -0,0 +1,3 @@
1
+ module BatchApi
2
+ VERSION = "0.2.1-2"
3
+ end
data/lib/batch_api.rb ADDED
@@ -0,0 +1,28 @@
1
+ require 'batch_api/configuration'
2
+ require 'batch_api/version'
3
+ require 'batch_api/utils'
4
+ require 'batch_api/processor'
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'
11
+
12
+ module BatchApi
13
+
14
+ # Public: access the main Batch API configuration object.
15
+ #
16
+ # Returns a BatchApi::Configuration instance
17
+ def self.config
18
+ @config ||= Configuration.new
19
+ end
20
+
21
+ # Public: are we in Rails? This partly exists just so that you
22
+ # can stub it in the tests.
23
+ #
24
+ # Returns true if Rails is a defined constant, false otherwise.
25
+ def self.rails?
26
+ defined?(Rails)
27
+ end
28
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :batch_api do
3
+ # # Task goes here
4
+ # end
data/readme.md ADDED
@@ -0,0 +1,243 @@
1
+ [![Build Status](https://secure.travis-ci.org/arsduo/batch_api.png?branch=master)](http://travis-ci.org/arsduo/batch_api)
2
+
3
+ ## What's this?
4
+
5
+ A gem that provides a RESTful Batch API for Rails and other Rack applications.
6
+ In this system, batch requests are simply collections of regular REST calls,
7
+ whose results are returned as an equivalent collection of regular REST results.
8
+
9
+ This is heavily inspired by [Facebook's Batch API](http://developers.facebook.com/docs/reference/api/batch/).
10
+
11
+ ## A Quick Example
12
+
13
+ Making a batch request:
14
+
15
+ ```
16
+ # POST /batch
17
+ # Content-Type: application/json
18
+
19
+ {
20
+ ops: [
21
+ {method: "get", url: "/patrons"},
22
+ {method: "post", url: "/orders/new", params: {dish_id: 123}},
23
+ {method: "get", url: "/oh/no/error", headers: {break: "fast"}},
24
+ {method: "delete", url: "/patrons/456"}
25
+ ],
26
+ sequential: true
27
+ }
28
+ ```
29
+
30
+ Reading the response:
31
+
32
+ ```
33
+ {
34
+ results: [
35
+ {status: 200, body: [{id: 1, name: "Jim-Bob"}, ...], headers: {}},
36
+ {status: 201, body: {id: 4, dish_name: "Spicy Crab Legs"}, headers: {}},
37
+ {status: 500, body: {error: {oh: "noes!"}}, headers: {Problem: "woops"}},
38
+ {status: 200, body: null, headers: {}}}
39
+ ]
40
+ }
41
+ ```
42
+
43
+ ### How It Works
44
+
45
+ #### Requests
46
+
47
+ As you can see from the example above, each request in the batch (an
48
+ "operation", in batch parlance) describes the same features any HTTP request
49
+ would include:
50
+
51
+ * _url_ - the API endpoint to hit, formatted exactly as you would for a regular
52
+ REST API request, leading / and all. (required)
53
+ * _method_ - what type of request to make -- GET, POST, PUT, etc. If no method
54
+ is supplied, GET is assumed. (optional)
55
+ * _args_ - a hash of arguments to the API. This can be used for both GET and
56
+ PUT/POST/PATCH requests. (optional)
57
+ * _headers_ - a hash of request-specific headers. The headers sent in the
58
+ request will be included as well, with operation-specific headers taking
59
+ precendence. (optional)
60
+ * _silent_ - whether to return a response for this request. You can save on
61
+ transfer if, for instance, you're making several PUT/POST requests, then
62
+ executing a GET at the end.
63
+
64
+ These individual operations are supplied as the "ops" parameter in the
65
+ overall request. Other options include:
66
+
67
+ * _sequential_ - execute all operations sequentially, rather than in parallel.
68
+ *This parameter is currently REQUIRED and must be set to true.* (In the future
69
+ the Batch API will offer parallel processing for thread-safe apps, and hence
70
+ this parameter must be supplied in order to explicitly preserve expected
71
+ behavior.)
72
+
73
+ Other options may be provided in the future for both the global request
74
+ and individual operations.
75
+
76
+ ### Responses
77
+
78
+ The Batch API will always return a 200, with a JSON body containing the
79
+ individual responses under the "results" key. Those responses, in turn,
80
+ contain the same main components of any HTTP response:
81
+
82
+ * _status_ - the HTTP status (200, 201, 400, etc.)
83
+ * _body_ - the rendered body
84
+ * _headers_ - any response headers
85
+
86
+ ### Errors
87
+
88
+ Errors in individual Batch API requests will be returned inline, with the
89
+ same status code and body they would return as individual requests.
90
+
91
+ If the Batch API itself returns a non-200 status code, that indicates a global
92
+ problem.
93
+
94
+ ## Installation
95
+
96
+ Setting up the Batch API is simple. Just add the gem to your middlewares:
97
+
98
+ ```ruby
99
+ # in application.rb
100
+ config.middleware.use BatchApi::RackMiddleware do |batch_config|
101
+ # you can set various configuration options:
102
+ batch_config.verb = :put # default :post
103
+ batch_config.endpoint = "/batchapi" # default /batch
104
+ batch_config.limit = 100 # how many operations max per request, default 50
105
+
106
+ # default middleware stack run for each batch request
107
+ batch_config.batch_middleware = Proc.new { }
108
+ # default middleware stack run for each individual operation
109
+ batch_config.operation_middleware = Proc.new { }
110
+ end
111
+ ```
112
+
113
+ That's it! Just fire up your curl, hit your endpoint with the right verb and a properly formatted request, and enjoy some batch API action.
114
+
115
+ ## Why a Batch API?
116
+
117
+ Batch APIs, though unRESTful, are useful for reducing HTTP overhead
118
+ by combining requests; this is particularly valuable for mobile clients,
119
+ which may generate groups of offline actions and which desire to
120
+ reduce battery consumption while connected by making fewer, better-compressed
121
+ requests.
122
+
123
+ ### Why not HTTP Pipelining?
124
+
125
+ HTTP pipelining is an awesome and promising technology, and would provide a
126
+ simple and effortless way to parallel process many requests; however, using
127
+ pipelining raised several issues for us, one of which was a blocker:
128
+
129
+ * [Lack of browser
130
+ support](http://en.wikipedia.org/wiki/HTTP_pipelining#Implementation_in_web_browsers):
131
+ a number of key browsers do not yet support HTTP pipelining (or have it
132
+ disabled by default). This will of course change in time,
133
+ but for now this takes pipelining out of consideration. (There a similar but
134
+ more minor issue
135
+ with [many web
136
+ proxies](http://en.wikipedia.org/wiki/HTTP_pipelining#Implementation_in_web_proxies).)
137
+ * The HTTP pipelining specification states that non-idempotent requests (e.g.
138
+ [POST](http://en.wikipedia.org/wiki/HTTP_pipelining) and
139
+ [in some
140
+ descriptions](http://www-archive.mozilla.org/projects/netlib/http/pipelining-faq.html) PUT)
141
+ shouldn't be made via pipelining. Though I have heard that some server
142
+ implementations do support POST requests (putting all subsequent requests on
143
+ hold until it's done), for applications that submit a lot of POSTs this raised
144
+ concerns as well.
145
+
146
+ Given this state of affairs -- and my desire to hack up a Batch API gem :P --,
147
+ we decided to implement an API-based solution.
148
+
149
+ ### Why this Approach?
150
+
151
+ There are two main approaches to writing batch APIs:
152
+
153
+ * A limited, specialized batch endpoint (or endpoints), which usually handles
154
+ updates and creates. DHH sketched out such a bulk update/create endpoint
155
+ for Rails 3.2 [in a gist](https://gist.github.com/981520) last year.
156
+ * A general-purpose RESTful API that can handle anything in your application,
157
+ a la the Facebook Batch API.
158
+
159
+ The second approach, IMO, minimizes code duplication and complexity. Rather
160
+ than have two systems that manage resources (or a more complicated one that
161
+ can handle both batch and individual requests), we simply route requests as we
162
+ always would.
163
+
164
+ This solution has several specific benefits:
165
+
166
+ * Less complexity - non-batch endpoints don't need any extra code, which means
167
+ less to maintain on your end.
168
+ * Complete flexibility - as you add new features to your application,
169
+ they become immediately and automatically available via the Batch API.
170
+ * More RESTful - as individual operations are simply actions on RESTful
171
+ resources, you preserve an important characteristic of your API.
172
+
173
+ As well as the general benefits of all batch operations:
174
+
175
+ * Reuse of state - user authentication, request stack processing, and
176
+ similar processing only needs to be done once.
177
+ * Better for clients - clients need to make fewer requests, as described above.
178
+ * Parallelizable - in the future, we could run requests in parallel (if
179
+ our app is thread-safe). Clients would be able to explicitly specify
180
+ dependencies between operations (or simply run all sequentially). This
181
+ should make for some fun experimentation :)
182
+
183
+ There's only one downside I can think of to this approach as opposed to a
184
+ specialized endpoint:
185
+
186
+ * Reduced ability to optimize - unlike a specialized API endpoint, each request
187
+ will be treated in isolation, which makes it harder to optimize the
188
+ underlying database queries via more efficient (read: complicated) SQL logic.
189
+ (Better identity maps would help with this, and since the main pain point
190
+ this approach addresses is at the HTTP connection layer, I submit we can
191
+ accept this.)
192
+
193
+ ## Implementation
194
+
195
+ The Batch API is implemented as a Rack middleware. Here's how it works:
196
+
197
+ First, if the request isn't a batch request (as defined by the endpoint and
198
+ method in BatchApi.config), it gets processed normally by your app.
199
+
200
+ If it is a batch request, we:
201
+ * Read and validate the parameters for the request, constructing a
202
+ representation of the operation.
203
+ * Compile a customized Rack environment hash with the appropriate parameters,
204
+ so that your app interprets the request as being for the appropriate action.
205
+ (This is requires a bit of extra processing for Rails.)
206
+ * Send each request up the middleware stack as normal, collecting the results.
207
+ Errors are caught and recorded appropriately.
208
+ * Send you back the results.
209
+
210
+ At both the batch level (processing all requests) and the individual operation
211
+ request, there is an internal, customizable midleware stack that you can
212
+ customize to insert additional custom behavior, such as handling authentication
213
+ or decoding JSON bodies for individual requests (this latter comes
214
+ pre-included). Check out the lib/batch_api/internal_middleware.rb for more
215
+ information.
216
+
217
+ ## To Do
218
+
219
+ The core of the Batch API is complete and solid, and so ready to go that it's
220
+ in use at 6Wunderkinder already :P
221
+
222
+ Here are some immediate tasks:
223
+
224
+ * Test against additional frameworks (beyond Rails and Sinatra)
225
+ * Write more usage docs / create a wiki.
226
+ * Add additional features inspired by the Facebook API, such as the ability to
227
+ surpress output for individual requests, etc.
228
+ * Add RDoc to the spec task and ensure all methods are documented.
229
+ * Research and implement parallelization and dependency management.
230
+
231
+ ## Thanks
232
+
233
+ To 6Wunderkinder, for all their support for this open-source project, and their
234
+ general awesomeness.
235
+
236
+ To Facebook, for providing inspiration and a great implementation in this and
237
+ many other things.
238
+
239
+ To [JT Archie](http://github.com/jtarchie) for his help and feedback.
240
+
241
+ ## Issues? Questions? Ideas?
242
+
243
+ Open a ticket or send a pull request!