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
@@ -0,0 +1,68 @@
1
+ require 'spec_helper'
2
+ require 'batch_api/error_wrapper'
3
+
4
+ describe BatchApi::ErrorWrapper do
5
+ let(:exception) {
6
+ StandardError.new(Faker::Lorem.words(3)).tap do |e|
7
+ e.set_backtrace(Kernel.caller)
8
+ end
9
+ }
10
+
11
+ let(:error) { BatchApi::ErrorWrapper.new(exception) }
12
+
13
+ describe "#body" do
14
+ it "includes the message in the body" do
15
+ error.body[:error][:message].should == exception.message
16
+ end
17
+
18
+ it "includes the backtrace if it should be there" do
19
+ error.stub(:expose_backtrace?).and_return(true)
20
+ error.body[:error][:backtrace].should == exception.backtrace
21
+ end
22
+
23
+ it "includes the backtrace if it should be there" do
24
+ error.stub(:expose_backtrace?).and_return(false)
25
+ error.body[:backtrace].should be_nil
26
+ end
27
+ end
28
+
29
+ describe "#render" do
30
+ it "returns the appropriate status" do
31
+ status = stub
32
+ error.stub(:status_code).and_return(status)
33
+ error.render[0].should == status
34
+ end
35
+
36
+ it "returns appropriate content type" do
37
+ ctype = stub
38
+ BatchApi::RackMiddleware.stub(:content_type).and_return(ctype)
39
+ error.render[1].should == ctype
40
+ end
41
+
42
+ it "returns the JSONified body as the 2nd" do
43
+ error.render[2].should == [MultiJson.dump(error.body)]
44
+ end
45
+ end
46
+
47
+ describe "#status_code" do
48
+ it "returns 500 by default" do
49
+ error.status_code.should == 500
50
+ end
51
+
52
+ it "returns another status code if the error supports that" do
53
+ err = StandardError.new
54
+ code = stub
55
+ err.stub(:status_code).and_return(code)
56
+ BatchApi::ErrorWrapper.new(err).status_code.should == code
57
+ end
58
+ end
59
+
60
+ describe ".expose_backtrace?" do
61
+ it "returns false if Rails.env.production?" do
62
+ Rails.env.stub(:production?).and_return(true)
63
+ BatchApi::ErrorWrapper.expose_backtrace?.should be_false
64
+ Rails.env.stub(:production?).and_return(false)
65
+ BatchApi::ErrorWrapper.expose_backtrace?.should be_true
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+
3
+ describe BatchApi::InternalMiddleware::DecodeJsonBody do
4
+ let(:app) { stub("app", call: result) }
5
+ let(:decoder) { BatchApi::InternalMiddleware::DecodeJsonBody.new(app) }
6
+ let(:env) { stub("env") }
7
+ let(:json) { {"data" => "is_json", "more" => {"hi" => "there"} } }
8
+ let(:result) {
9
+ BatchApi::Response.new([
10
+ 200,
11
+ {"Content-Type" => "application/json"},
12
+ [MultiJson.dump(json)]
13
+ ])
14
+ }
15
+
16
+ describe "#call" do
17
+ context "for json results" do
18
+ it "decodes JSON results for application/json responses" do
19
+ result = decoder.call(env)
20
+ result.body.should == json
21
+ end
22
+
23
+ it "doesn't change anything else" do
24
+ result = decoder.call(env)
25
+ result.status.should == 200
26
+ result.headers.should == {"Content-Type" => "application/json"}
27
+ end
28
+ end
29
+
30
+ context "for non-JSON responses" do
31
+ it "doesn't decode" do
32
+ result.headers = {"Content-Type" => "text/html"}
33
+ decoder.call(env).body.should == MultiJson.dump(json)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,61 @@
1
+ require 'spec_helper'
2
+
3
+ describe BatchApi::InternalMiddleware::ResponseFilter do
4
+ let(:app) { stub("app", call: result) }
5
+ let(:surpressor) { BatchApi::InternalMiddleware::ResponseFilter.new(app) }
6
+ let(:env) { {
7
+ op: stub("operation", options: {"silent" => true})
8
+ } }
9
+
10
+ let(:result) {
11
+ BatchApi::Response.new([
12
+ 200,
13
+ {"Content-Type" => "application/json"},
14
+ ["{}"]
15
+ ])
16
+ }
17
+
18
+ describe "#call" do
19
+ context "for results with silent" do
20
+ context "for successful (200-299) results" do
21
+ it "empties the response so its as_json is empty" do
22
+ surpressor.call(env)
23
+ result.as_json.should == {}
24
+ end
25
+ end
26
+
27
+ context "for non-successful responses" do
28
+ it "doesn't change anything else" do
29
+ result.status = 301
30
+ expect {
31
+ surpressor.call(env)
32
+ }.not_to change(result, :to_s)
33
+ end
34
+ end
35
+ end
36
+
37
+ context "for results without silent" do
38
+ before :each do
39
+ env[:op].options[:silent] = nil
40
+ end
41
+
42
+ context "for successful (200-299) results" do
43
+ it "does nothing" do
44
+ expect {
45
+ surpressor.call(env)
46
+ }.not_to change(result, :to_s)
47
+ end
48
+ end
49
+
50
+ context "for non-successful responses" do
51
+ it "doesn't change anything else" do
52
+ result.status = 301
53
+ expect {
54
+ surpressor.call(env)
55
+ }.not_to change(result, :to_s)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+
@@ -0,0 +1,91 @@
1
+ require 'spec_helper'
2
+
3
+ describe BatchApi::InternalMiddleware do
4
+ class FakeBuilder
5
+ attr_accessor :middlewares
6
+
7
+ def initialize(&block)
8
+ @middlewares = []
9
+ instance_eval(&block) if block_given?
10
+ end
11
+
12
+ def use(middleware, *args)
13
+ @middlewares << [middleware, args]
14
+ end
15
+ end
16
+
17
+ let(:builder) { FakeBuilder.new }
18
+
19
+ it "builds an empty default global middleware" do
20
+ builder.instance_eval(
21
+ &BatchApi::InternalMiddleware::DEFAULT_BATCH_MIDDLEWARE
22
+ )
23
+ builder.middlewares.should be_empty
24
+ end
25
+
26
+ describe "internal middleware defaults" do
27
+ before :each do
28
+ builder.instance_eval(
29
+ &BatchApi::InternalMiddleware::DEFAULT_OPERATION_MIDDLEWARE
30
+ )
31
+ end
32
+
33
+ it "builds a per-op middleware with the response silencer" do
34
+ builder.middlewares[0].should ==
35
+ [BatchApi::InternalMiddleware::ResponseFilter, []]
36
+ end
37
+
38
+ it "builds a per-op middleware with the JSON decoder" do
39
+ builder.middlewares[1].should ==
40
+ [BatchApi::InternalMiddleware::DecodeJsonBody, []]
41
+ end
42
+ end
43
+
44
+ describe ".batch_stack" do
45
+ # we can't use stubs inside the procs since they're instance_eval'd
46
+ let(:global_config) { Proc.new { use "Global" } }
47
+ let(:strategy) { stub("strategy") }
48
+ let(:processor) { stub("processor", strategy: strategy) }
49
+ let(:stack) { BatchApi::InternalMiddleware.batch_stack(processor) }
50
+
51
+ before :each do
52
+ BatchApi.config.stub(:batch_middleware).and_return(global_config)
53
+ stub_const("Middleware::Builder", FakeBuilder)
54
+ end
55
+
56
+ it "builds the stack with the right number of wares" do
57
+ stack.middlewares.length.should == 2
58
+ end
59
+
60
+ it "builds a middleware stack starting with the configured global wares" do
61
+ stack.middlewares[0].first.should == "Global"
62
+ end
63
+
64
+ it "inserts the appropriate strategy from the processor" do
65
+ stack.middlewares[1].first.should == strategy
66
+ end
67
+ end
68
+
69
+ describe ".operation_stack" do
70
+ # we can't use stubs inside the procs since they're instance_eval'd
71
+ let(:op_config) { Proc.new { use "Op" } }
72
+ let(:stack) { BatchApi::InternalMiddleware.operation_stack }
73
+
74
+ before :each do
75
+ BatchApi.config.stub(:operation_middleware).and_return(op_config)
76
+ stub_const("Middleware::Builder", FakeBuilder)
77
+ end
78
+
79
+ it "builds the stack with the right number of wares" do
80
+ stack.middlewares.length.should == 2
81
+ end
82
+
83
+ it "builds a middleware stack including the configured per-op wares" do
84
+ stack.middlewares[0].first.should == "Op"
85
+ end
86
+
87
+ it "builds a middleware stack ending with the executor" do
88
+ stack.middlewares[1].first.should == BatchApi::Processor::Executor
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,243 @@
1
+ require 'spec_helper'
2
+ require 'batch_api/operation'
3
+
4
+ describe BatchApi::Operation::Rack do
5
+ let(:op_params) { {
6
+ "method" => "POST",
7
+ # this matches a route in our dummy application
8
+ "url" => "/endpoint?foo=baz",
9
+ "params" => {a: 2},
10
+ "headers" => {"foo" => "bar"}
11
+ } }
12
+
13
+ # for env, see bottom of file - it's long
14
+ let(:operation) { BatchApi::Operation::Rack.new(op_params, env, app) }
15
+ let(:app) { stub("application", call: [200, {}, ["foo"]]) }
16
+
17
+ describe "accessors" do
18
+ [
19
+ :method, :url, :params, :headers,
20
+ :env, :app, :result, :options
21
+ ].each do |a|
22
+ attr = a
23
+ it "has an accessor for #{attr}" do
24
+ value = stub
25
+ operation.send("#{attr}=", value)
26
+ operation.send(attr).should == value
27
+ end
28
+ end
29
+ end
30
+
31
+ describe "#initialize" do
32
+ ["method", "url", "params", "headers"].each do |a|
33
+ attr = a
34
+ it "extracts the #{attr} information from the operation params" do
35
+ operation.send(attr).should == op_params[attr]
36
+ end
37
+ end
38
+
39
+ it "sets options to the op" do
40
+ operation.options.should == op_params
41
+ end
42
+
43
+ it "defaults params to {} if not provided" do
44
+ op = BatchApi::Operation::Rack.new(op_params.except("params"), env, app)
45
+ op.params.should == {}
46
+ end
47
+
48
+ it "defaults headers to {} if not provided" do
49
+ op = BatchApi::Operation::Rack.new(op_params.except("headers"), env, app)
50
+ op.headers.should == {}
51
+ end
52
+
53
+ it "does a deep dup of the env" do
54
+ operation.env.should == env
55
+
56
+ flat_env = env.to_a.flatten
57
+ operation.env.to_a.flatten.each_with_index do |obj, index|
58
+ # this is a rough test for deep dup -- make sure the objects
59
+ # that aren't symbols aren't actually the same objects in memory
60
+ if obj.is_a?(Hash) || obj.is_a?(Array)
61
+ obj.object_id.should_not == flat_env[index].object_id
62
+ end
63
+ end
64
+ end
65
+
66
+ it "raises a MalformedOperationError if method or URL are missing" do
67
+ no_method = op_params.dup.tap {|o| o.delete("method") }
68
+ expect {
69
+ BatchApi::Operation::Rack.new(no_method, env, app)
70
+ }.to raise_exception(BatchApi::Errors::MalformedOperationError)
71
+
72
+ no_url = op_params.dup.tap {|o| o.delete("url") }
73
+ expect {
74
+ BatchApi::Operation::Rack.new(no_url, env, app)
75
+ }.to raise_exception(BatchApi::Errors::MalformedOperationError)
76
+
77
+ nothing = op_params.dup.tap {|o| o.delete("url"); o.delete("method") }
78
+ expect {
79
+ BatchApi::Operation::Rack.new(nothing, env, app)
80
+ }.to raise_exception(BatchApi::Errors::MalformedOperationError)
81
+ end
82
+ end
83
+
84
+ describe "#process_env" do
85
+ let(:processed_env) { operation.tap {|o| o.process_env}.env }
86
+
87
+ it "merges any headers in in the right format" do
88
+ key = "HTTP_FOO" # as defined above in op_params
89
+
90
+ processed_env[key].should_not == env[key]
91
+ # in this case, it's a batch controller
92
+ processed_env[key].should == op_params["headers"]["foo"]
93
+ end
94
+
95
+ it "preserves existing headers" do
96
+ processed_env["HTTP_PREVIOUS_HEADERS"].should == env["HTTP_PREVIOUS_HEADERS"]
97
+ end
98
+
99
+ it "updates the method" do
100
+ key = "REQUEST_METHOD"
101
+ processed_env[key].should_not == env[key]
102
+ processed_env[key].should == "POST"
103
+ end
104
+
105
+ it "updates the REQUEST_URI" do
106
+ key = "REQUEST_URI"
107
+ processed_env[key].should_not == env[key]
108
+ processed_env[key].should == env["REQUEST_URI"].gsub(/\/batch.*/, op_params["url"])
109
+ end
110
+
111
+ it "updates the REQUEST_PATH with the path component (w/o params)" do
112
+ key = "REQUEST_PATH"
113
+ processed_env[key].should_not == env[key]
114
+ processed_env[key].should == op_params["url"].split("?").first
115
+ end
116
+
117
+ it "updates the original fullpath" do
118
+ key = "ORIGINAL_FULLPATH"
119
+ processed_env[key].should_not == env[key]
120
+ processed_env[key].should == op_params["url"]
121
+ end
122
+
123
+ it "updates the PATH_INFO" do
124
+ key = "PATH_INFO"
125
+ processed_env[key].should_not == env[key]
126
+ processed_env[key].should == op_params["url"]
127
+ end
128
+
129
+ it "updates the rack query string" do
130
+ key = "rack.request.query_string"
131
+ processed_env[key].should_not == env[key]
132
+ processed_env[key].should == op_params["url"].split("?").last
133
+ end
134
+
135
+ it "updates the QUERY_STRING" do
136
+ key = "QUERY_STRING"
137
+ processed_env[key].should_not == env[key]
138
+ processed_env[key].should == op_params["url"].split("?").last
139
+ end
140
+
141
+ it "updates the form hash" do
142
+ key = "rack.request.form_hash"
143
+ processed_env[key].should_not == env[key]
144
+ processed_env[key].should == op_params["params"]
145
+ end
146
+
147
+ context "query_hash" do
148
+ it "sets it to params for a GET" do
149
+ operation.method = "get"
150
+ processed_env = operation.tap {|o| o.process_env}.env
151
+ key = "rack.request.query_hash"
152
+ processed_env[key].should_not == env[key]
153
+ processed_env[key].should == op_params["params"]
154
+ end
155
+
156
+ it "sets it to nil for a POST" do
157
+ key = "rack.request.query_hash"
158
+ processed_env[key].should_not == env[key]
159
+ processed_env[key].should be_nil
160
+ end
161
+ end
162
+ end
163
+
164
+ describe "#execute" do
165
+ context "when it works" do
166
+ let(:result) { [
167
+ 200,
168
+ {header: "footer"},
169
+ stub(body: "{\"data\":2}", cookies: nil)
170
+ ] }
171
+ let(:processed_env) { stub }
172
+
173
+ before :each do
174
+ operation.stub(:process_env) { operation.env = processed_env }
175
+ end
176
+
177
+ it "executes the call with the application" do
178
+ app.should_receive(:call).with(processed_env)
179
+ operation.execute
180
+ end
181
+
182
+ it "returns a BatchAPI::Response made from the result" do
183
+ response = stub
184
+ app.stub(:call).and_return(result)
185
+ BatchApi::Response.should_receive(:new).with(result).and_return(response)
186
+ operation.execute.should == response
187
+ end
188
+
189
+ it "returns a BatchApi::Response from an ErrorWrapper for errors" do
190
+ err = StandardError.new
191
+ result, rendered, response = stub, stub, stub
192
+ b_err = stub("batch error", render: rendered)
193
+
194
+ # simulate the error
195
+ app.stub(:call).and_raise(err)
196
+ # we'll create the BatchError
197
+ BatchApi::ErrorWrapper.should_receive(:new).with(err).and_return(b_err)
198
+ # render that as the response
199
+ BatchApi::Response.should_receive(:new).with(rendered).and_return(response)
200
+ # and return the response overall
201
+ operation.execute.should == response
202
+ end
203
+ end
204
+ end
205
+
206
+ let(:env) {
207
+ {
208
+ "CONTENT_LENGTH"=>"10",
209
+ "CONTENT_TYPE"=>"application/x-www-form-urlencoded",
210
+ "GATEWAY_INTERFACE"=>"CGI/1.1",
211
+ "PATH_INFO"=>"/foo",
212
+ "QUERY_STRING"=>"",
213
+ "REMOTE_ADDR"=>"127.0.0.1",
214
+ "REMOTE_HOST"=>"1035.spotilocal.com",
215
+ "REQUEST_METHOD"=>"REPORT",
216
+ "REQUEST_URI"=>"http://localhost:3000/batch",
217
+ "SCRIPT_NAME"=>"",
218
+ "SERVER_NAME"=>"localhost",
219
+ "SERVER_PORT"=>"3000",
220
+ "SERVER_PROTOCOL"=>"HTTP/1.1",
221
+ "SERVER_SOFTWARE"=>"WEBrick/1.3.1 (Ruby/1.9.3/2012-02-16)",
222
+ "HTTP_USER_AGENT"=>"curl/7.21.4 (universal-apple-darwin11.0) libcurl/7.21.4 OpenSSL/0.9.8r zlib/1.2.5",
223
+ "HTTP_HOST"=>"localhost:3000",
224
+ "HTTP_ACCEPT"=>"*/*",
225
+ "HTTP_PREVIOUS_HEADERS" => "value",
226
+ "rack.version"=>[1,1],
227
+ "rack.input"=>StringIO.new("{\"ops\":{}}"),
228
+ "rack.errors"=>$stderr,
229
+ "rack.multithread"=>false,
230
+ "rack.multiprocess"=>false,
231
+ "rack.run_once"=>false,
232
+ "rack.url_scheme"=>"http",
233
+ "HTTP_VERSION"=>"HTTP/1.1",
234
+ "REQUEST_PATH"=>"/batch",
235
+ "ORIGINAL_FULLPATH"=>"/batch",
236
+ "rack.request.form_input"=>StringIO.new("{\"ops\":{}}"),
237
+ "rack.request.form_hash"=>{"{\"ops\":{}}"=>nil},
238
+ "rack.request.form_vars"=>"{\"ops\":{}}",
239
+ "rack.request.query_string"=>"",
240
+ "rack.request.query_hash"=>{}
241
+ }
242
+ }
243
+ end