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,100 @@
1
+ require 'spec_helper'
2
+ require 'batch_api/operation'
3
+
4
+ describe BatchApi::Operation::Rails 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::Rails.new(op_params, env, app) }
15
+ let(:app) { stub("application", call: [200, {}, ["foo"]]) }
16
+ let(:path_params) { {controller: "batch_api/batch", action: "batch"} }
17
+ let(:mixed_params) { op_params["params"].merge(path_params) }
18
+
19
+ before :each do
20
+ ::Rails.application.routes.stub(:recognize_path).and_return(path_params)
21
+ end
22
+
23
+ describe "#initialize" do
24
+ it "merges in the Rails path params" do
25
+ ::Rails.application.routes.should_receive(:recognize_path).with(
26
+ op_params["url"],
27
+ op_params
28
+ ).and_return(path_params)
29
+
30
+ operation.params.should include(path_params)
31
+ end
32
+
33
+ it "doesn't change the params if the path isn't recognized" do
34
+ ::Rails.application.routes.stub(:recognize_path).and_raise(StandardError)
35
+ operation.params.should == op_params["params"]
36
+ end
37
+ end
38
+
39
+ describe "#process_env" do
40
+ let(:processed_env) { operation.tap {|o| o.process_env}.env }
41
+
42
+ it "updates the ActionDispatch params" do
43
+ key = "action_dispatch.request.parameters"
44
+ processed_env[key].should_not == env[key]
45
+ processed_env[key].should == mixed_params
46
+ end
47
+
48
+ it "updates the ActionDispatch request params" do
49
+ key = "action_dispatch.request.request_parameters"
50
+ processed_env[key].should_not == env[key]
51
+ processed_env[key].should == mixed_params
52
+ end
53
+ end
54
+
55
+ let(:env) {
56
+ {
57
+ "CONTENT_LENGTH"=>"10",
58
+ "CONTENT_TYPE"=>"application/x-www-form-urlencoded",
59
+ "GATEWAY_INTERFACE"=>"CGI/1.1",
60
+ "PATH_INFO"=>"/foo",
61
+ "QUERY_STRING"=>"",
62
+ "REMOTE_ADDR"=>"127.0.0.1",
63
+ "REMOTE_HOST"=>"1035.spotilocal.com",
64
+ "REQUEST_METHOD"=>"REPORT",
65
+ "REQUEST_URI"=>"http://localhost:3000/batch",
66
+ "SCRIPT_NAME"=>"",
67
+ "SERVER_NAME"=>"localhost",
68
+ "SERVER_PORT"=>"3000",
69
+ "SERVER_PROTOCOL"=>"HTTP/1.1",
70
+ "SERVER_SOFTWARE"=>"WEBrick/1.3.1 (Ruby/1.9.3/2012-02-16)",
71
+ "HTTP_USER_AGENT"=>"curl/7.21.4 (universal-apple-darwin11.0) libcurl/7.21.4 OpenSSL/0.9.8r zlib/1.2.5",
72
+ "HTTP_HOST"=>"localhost:3000",
73
+ "HTTP_ACCEPT"=>"*/*",
74
+ "HTTP_PREVIOUS_HEADERS" => "value",
75
+ "rack.version"=>[1,1],
76
+ "rack.input"=>StringIO.new("{\"ops\":{}}"),
77
+ "rack.errors"=>$stderr,
78
+ "rack.multithread"=>false,
79
+ "rack.multiprocess"=>false,
80
+ "rack.run_once"=>false,
81
+ "rack.url_scheme"=>"http",
82
+ "HTTP_VERSION"=>"HTTP/1.1",
83
+ "REQUEST_PATH"=>"/batch",
84
+ "ORIGINAL_FULLPATH"=>"/batch",
85
+ "action_dispatch.routes"=>Rails.application.routes,
86
+ "action_dispatch.parameter_filter"=>[:password],
87
+ "action_dispatch.secret_token"=>"fc6fbc81b3204410da8389",
88
+ "action_dispatch.show_exceptions"=>true,
89
+ "action_dispatch.show_detailed_exceptions"=>true,
90
+ "action_dispatch.logger"=>Rails.logger,
91
+ "action_dispatch.backtrace_cleaner"=>nil,
92
+ "action_dispatch.request_id"=>"2e7c988bea73e13dca4fac059a1bb187",
93
+ "action_dispatch.remote_ip"=>"127.0.0.1",
94
+ "action_dispatch.request.content_type"=>"application/x-www-form-urlencoded",
95
+ "action_dispatch.request.path_parameters"=> {},
96
+ "rack.request.query_string"=>"",
97
+ "rack.request.query_hash"=>{}
98
+ }
99
+ }
100
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+ require 'batch_api/processor/executor'
3
+
4
+ describe BatchApi::Processor::Executor do
5
+
6
+ let(:app) { stub("app", call: stub) }
7
+ let(:executor) { BatchApi::Processor::Executor.new(app) }
8
+ let(:result) { stub("result") }
9
+ let(:op) { stub("operation", execute: result) }
10
+ let(:env) { {op: op} }
11
+
12
+ describe "#call" do
13
+ it "executes the operation" do
14
+ op.should_receive(:execute)
15
+ executor.call(env)
16
+ end
17
+
18
+ it "returns the result" do
19
+ executor.call(env).should == result
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+
3
+ describe BatchApi::Processor::Sequential do
4
+
5
+ let(:app) { stub("app", call: stub) }
6
+ let(:sequential) { BatchApi::Processor::Sequential.new(app) }
7
+
8
+ describe "#call" do
9
+ let(:call_results) { 3.times.collect {|i| stub("called #{i}") } }
10
+ let(:env) { {
11
+ ops: 3.times.collect {|i| stub("op #{i}") }
12
+ } }
13
+ let(:op_middleware) { stub("middleware", call: {}) }
14
+
15
+ before :each do
16
+ BatchApi::InternalMiddleware.
17
+ stub(:operation_stack).and_return(op_middleware)
18
+ op_middleware.stub(:call).and_return(*call_results)
19
+ end
20
+
21
+ it "creates an operation middleware stack and calls it for each op" do
22
+ env[:ops].each {|op|
23
+ op_middleware.should_receive(:call).
24
+ with(hash_including(op: op)).ordered
25
+ }
26
+ sequential.call(env)
27
+ end
28
+
29
+ it "includes the rest of the env in the calls" do
30
+ op_middleware.should_receive(:call).
31
+ with(hash_including(env)).exactly(3).times
32
+ sequential.call(env)
33
+ end
34
+
35
+ it "returns the results of the calls" do
36
+ sequential.call(env).should == call_results
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,134 @@
1
+ require 'spec_helper'
2
+
3
+ describe BatchApi::Processor do
4
+
5
+ let(:ops) { [ {"url" => "/endpoint", "method" => "get"} ] }
6
+ let(:options) { { "sequential" => true } }
7
+ let(:env) { {
8
+ "CONTENT_TYPE"=>"application/x-www-form-urlencoded",
9
+ "GATEWAY_INTERFACE"=>"CGI/1.1",
10
+ "PATH_INFO"=>"/foo",
11
+ "QUERY_STRING"=>"",
12
+ "REMOTE_ADDR"=>"127.0.0.1",
13
+ "REMOTE_HOST"=>"1035.spotilocal.com",
14
+ "REQUEST_METHOD"=>"REPORT",
15
+ "REQUEST_URI"=>"http://localhost:3000/batch",
16
+ "SCRIPT_NAME"=>"",
17
+ "rack.input" => StringIO.new,
18
+ "rack.errors" => StringIO.new,
19
+ "SERVER_NAME"=>"localhost",
20
+ "SERVER_PORT"=>"3000",
21
+ "SERVER_PROTOCOL"=>"HTTP/1.1",
22
+ "SERVER_SOFTWARE"=>"WEBrick/1.3.1 (Ruby/1.9.3/2012-02-16)",
23
+ "HTTP_USER_AGENT"=>"curl/7.21.4 (universal-apple-darwin11.0) libcurl/7.21",
24
+ "HTTP_HOST"=>"localhost:3000"
25
+ } }
26
+
27
+ let(:request) {
28
+ Rack::Request.new(env).tap do |r|
29
+ r.stub(:params).and_return({}.merge("ops" => ops).merge(options))
30
+ end
31
+ }
32
+ let(:app) { stub("application", call: [200, {}, ["foo"]]) }
33
+ let(:processor) { BatchApi::Processor.new(request, app) }
34
+
35
+ describe "#initialize" do
36
+ # this may be brittle...consider refactoring?
37
+ it "turns the ops params into processed operations at #ops" do
38
+ # simulate receiving several operations
39
+ klass = stub("op class")
40
+ BatchApi::Processor.stub(:operation_klass).and_return(klass)
41
+ operation_objects = 3.times.collect { stub("operation object") }
42
+ operation_params = 3.times.collect do |i|
43
+ stub("raw operation").tap do |o|
44
+ klass.should_receive(:new)
45
+ .with(o, env, app).and_return(operation_objects[i])
46
+ end
47
+ end
48
+
49
+ request.params["ops"] = operation_params
50
+ BatchApi::Processor.new(request, app).ops.should == operation_objects
51
+ end
52
+
53
+ it "makes the options available" do
54
+ BatchApi::Processor.new(request, app).options.should == options
55
+ end
56
+
57
+ it "makes the app available" do
58
+ BatchApi::Processor.new(request, app).app.should == app
59
+ end
60
+
61
+ context "error conditions" do
62
+ it "(currently) throws an error if sequential is not true" do
63
+ request.params.delete("sequential")
64
+ expect {
65
+ BatchApi::Processor.new(request, app)
66
+ }.to raise_exception(BatchApi::Errors::BadOptionError)
67
+ end
68
+
69
+ it "raise a OperationLimitExceeded error if too many ops provided" do
70
+ ops = (BatchApi.config.limit + 1).to_i.times.collect {|i| i}
71
+ request.params["ops"] = ops
72
+ expect {
73
+ BatchApi::Processor.new(request, app)
74
+ }.to raise_exception(BatchApi::Errors::OperationLimitExceeded)
75
+ end
76
+
77
+ it "raises a NoOperationError if operations.blank?" do
78
+ request.params["ops"] = nil
79
+ expect {
80
+ BatchApi::Processor.new(request, app)
81
+ }.to raise_exception(BatchApi::Errors::NoOperationsError)
82
+ request.params["ops"] = []
83
+ expect {
84
+ BatchApi::Processor.new(request, app)
85
+ }.to raise_exception(BatchApi::Errors::NoOperationsError)
86
+ end
87
+ end
88
+ end
89
+
90
+ describe "#strategy" do
91
+ it "returns BatchApi::Processor::Sequential" do
92
+ processor.strategy.should == BatchApi::Processor::Sequential
93
+ end
94
+ end
95
+
96
+ describe "#execute!" do
97
+ let(:result) { stub("result") }
98
+ let(:stack) { stub("stack", call: result) }
99
+ let(:middleware_env) { {
100
+ ops: processor.ops, # the processed Operation objects
101
+ rack_env: env,
102
+ rack_app: app,
103
+ options: options
104
+ } }
105
+
106
+ before :each do
107
+ BatchApi::InternalMiddleware.stub(:batch_stack).and_return(stack)
108
+ end
109
+
110
+ it "calls an internal middleware stacks with the appropriate data" do
111
+ stack.should_receive(:call).with(middleware_env)
112
+ processor.execute!
113
+ end
114
+
115
+ it "returns the formatted result of the strategy" do
116
+ stack.stub(:call).and_return(stubby = stub)
117
+ processor.execute!["results"].should == stubby
118
+ end
119
+ end
120
+
121
+ describe ".operation_klass" do
122
+ it "returns BatchApi::Operation::Rack if !Rails" do
123
+ BatchApi.stub(:rails?).and_return(false)
124
+ BatchApi::Processor.operation_klass.should ==
125
+ BatchApi::Operation::Rack
126
+ end
127
+
128
+ it "returns BatchApi::Operation::Rails if Rails" do
129
+ BatchApi.stub(:rails?).and_return(true)
130
+ BatchApi::Processor.operation_klass.should ==
131
+ BatchApi::Operation::Rails
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,103 @@
1
+ require 'spec_helper'
2
+
3
+ describe BatchApi::RackMiddleware do
4
+ describe "#initialize" do
5
+ it "allows access to the BatchApi configuration" do
6
+ limit = rand * 100
7
+ middleware = BatchApi::RackMiddleware.new(stub("app")) do |conf|
8
+ conf.limit = limit
9
+ end
10
+ BatchApi.config.limit.should == limit
11
+ end
12
+ end
13
+
14
+ describe "#call" do
15
+ let(:endpoint) { "/foo/bar" }
16
+ let(:verb) { "run" }
17
+ let(:app) { stub("app") }
18
+
19
+ let(:middleware) {
20
+ BatchApi::RackMiddleware.new(app) do |conf|
21
+ conf.endpoint = endpoint
22
+ conf.verb = verb
23
+ end
24
+ }
25
+
26
+ context "if it's a batch call" do
27
+ let(:env) { {
28
+ "PATH_INFO" => endpoint,
29
+ "REQUEST_METHOD" => verb.upcase,
30
+ # other stuff
31
+ "CONTENT_TYPE"=>"application/x-www-form-urlencoded",
32
+ "GATEWAY_INTERFACE"=>"CGI/1.1",
33
+ "QUERY_STRING"=>"",
34
+ "REMOTE_ADDR"=>"127.0.0.1",
35
+ "REMOTE_HOST"=>"1035.spotilocal.com",
36
+ "REQUEST_URI"=>"http://localhost:3000/batch",
37
+ "SCRIPT_NAME"=>"",
38
+ "rack.input" => StringIO.new,
39
+ "rack.errors" => StringIO.new,
40
+ "SERVER_NAME"=>"localhost",
41
+ "SERVER_PORT"=>"3000",
42
+ "SERVER_PROTOCOL"=>"HTTP/1.1",
43
+ "SERVER_SOFTWARE"=>"WEBrick/1.3.1 (Ruby/1.9.3/2012-02-16)",
44
+ "HTTP_USER_AGENT"=>"curl/7.21.4 (universal-apple-darwin11.0) libcurl/7.21.4 OpenSSL/0.9.8r zlib/1.2.5",
45
+ "HTTP_HOST"=>"localhost:3000"
46
+ } }
47
+
48
+ let(:request) { Rack::Request.new(env) }
49
+ let(:result) { {a: 2, b: {c: 3}} }
50
+ let(:processor) { stub("processor", :execute! => result) }
51
+
52
+ before :each do
53
+ BatchApi::Processor.stub(:new).and_return(processor)
54
+ end
55
+
56
+ it "processes the batch request" do
57
+ Rack::Request.stub(:new).with(env).and_return(request)
58
+ BatchApi::Processor.should_receive(:new).with(request, app).and_return(processor)
59
+ middleware.call(env)
60
+ end
61
+
62
+ context "for a successful set of calls" do
63
+ it "returns the JSON-encoded result as the body" do
64
+ output = middleware.call(env)
65
+ output[2].should == [MultiJson.dump(result)]
66
+ end
67
+
68
+ it "returns a 200" do
69
+ middleware.call(env)[0].should == 200
70
+ end
71
+
72
+ it "sets the content type" do
73
+ middleware.call(env)[1].should include("Content-Type" => "application/json")
74
+ end
75
+ end
76
+
77
+ context "for BatchApi errors" do
78
+ it "returns a rendered ErrorWrapper" do
79
+ err, result = StandardError.new, stub
80
+ error = stub("error object", render: result)
81
+ BatchApi::Processor.stub(:new).and_raise(err)
82
+ BatchApi::ErrorWrapper.should_receive(:new).with(err).and_return(
83
+ error
84
+ )
85
+ middleware.call(env).should == result
86
+ end
87
+ end
88
+ end
89
+
90
+ context "if it's not a batch request" do
91
+ let(:env) { {
92
+ "PATH_INFO" => "/not/batch",
93
+ "REQUEST_METHOD" => verb.upcase
94
+ } }
95
+
96
+ it "just calls the app onward and returns the result" do
97
+ output = stub("output")
98
+ app.should_receive(:call).with(env).and_return(output)
99
+ middleware.call(env)
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,53 @@
1
+ require 'spec_helper'
2
+ require 'batch_api/response'
3
+
4
+ describe BatchApi::Response do
5
+
6
+ let(:raw_response) { [200, {}, ["ab", "cd", "ef"]] }
7
+ let(:response) { BatchApi::Response.new(raw_response) }
8
+
9
+ [:status, :body, :headers].each do |attr|
10
+ local_attr = attr
11
+ it "has an accessor for #{local_attr}" do
12
+ response.should respond_to(local_attr)
13
+ end
14
+ end
15
+
16
+ describe "#initialize" do
17
+ it "sets status to the HTTP status code" do
18
+ response.status.should == raw_response.first
19
+ end
20
+
21
+ it "sets body to the HTTP body turned into a string" do
22
+ response.body.should == raw_response[2].join
23
+ end
24
+
25
+ it "sets headers to the HTTP headers" do
26
+ response.headers.should == raw_response[1]
27
+ end
28
+ end
29
+
30
+ describe "#as_json" do
31
+ it "creates the expected hash" do
32
+ response.as_json.should == {
33
+ body: response.body,
34
+ status: response.status,
35
+ headers: response.headers
36
+ }
37
+ end
38
+
39
+ it "accepts options" do
40
+ response.as_json(foo: :bar).should_not be_nil
41
+ end
42
+
43
+ it "leaves out items that are blank" do
44
+ response.status = response.body = nil
45
+ response.as_json.should == {headers: raw_response[1]}
46
+ end
47
+
48
+ it "includes items that are false" do
49
+ response.body = false
50
+ response.as_json[:body].should == false
51
+ end
52
+ end
53
+ end