batch_api 0.0.1

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.
@@ -0,0 +1,20 @@
1
+ Copyright 2012 YOURNAME
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ require 'rdoc/rdoc'
11
+ require 'rake/rdoctask'
12
+ RDoc::Task = Rake::RDocTask
13
+ end
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'BatchApi'
18
+ rdoc.options << '--line-numbers'
19
+ rdoc.rdoc_files.include('README.rdoc')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+ Bundler::GemHelper.install_tasks
24
+
25
+ require 'rspec/core/rake_task'
26
+ RSpec::Core::RakeTask.new do |t|
27
+ t.rspec_opts = ["--color", '--format doc', '--order rand']
28
+ end
29
+
30
+ task :default => :spec
@@ -0,0 +1,10 @@
1
+ require 'batch_api/operation'
2
+
3
+ module BatchApi
4
+ class BatchController < ::ApplicationController
5
+ def batch
6
+ ops = params[:ops].map {|o| BatchApi::Operation.new(o, request.env)}
7
+ render :json => ops.map(&:execute)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,2 @@
1
+ v0.0.1
2
+ * Initial build
@@ -0,0 +1,6 @@
1
+ require 'batch_api/routing_helper'
2
+ require 'batch_api/engine'
3
+ require 'batch_api/version'
4
+
5
+ module BatchApi
6
+ end
@@ -0,0 +1,8 @@
1
+ # CURRENT FILE :: lib/team_page/engine.rb
2
+ module BatchApi
3
+ class Engine < Rails::Engine
4
+ initializer "batch_api.add_routing_helper" do |app|
5
+ ActionDispatch::Routing::Mapper.send(:include, BatchApi::RoutingHelper)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,32 @@
1
+ module BatchApi
2
+ # Public: 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 Error
6
+ # Public: create a new BatchError from a Rails error.
7
+ def initialize(error)
8
+ @message = error.message
9
+ @backtrace = error.backtrace
10
+ end
11
+
12
+ # Public: here for compatibility with BatchResponse interface.
13
+ attr_reader :cookies
14
+
15
+ # Public: the error details as a hash, which can be returned
16
+ # to clients as JSON.
17
+ def body
18
+ if expose_backtrace?
19
+ {
20
+ message: @message,
21
+ backtrace: @backtrace
22
+ }
23
+ else
24
+ { message: @message }
25
+ end
26
+ end
27
+
28
+ def expose_backtrace?
29
+ Rails.env.production?
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,94 @@
1
+ require 'batch_api/response'
2
+
3
+ module BatchApi
4
+ # Public: an individual batch operation.
5
+ class Operation
6
+ attr_accessor :method, :url, :params, :headers
7
+ attr_accessor :env, :result
8
+
9
+ # Public: create a new Batch Operation given the specifications for a batch
10
+ # operation (as defined above) and the request environment for the main
11
+ # batch request.
12
+ def initialize(op, base_env)
13
+ @op = op
14
+
15
+ @method = op[:method]
16
+ @url = op[:url]
17
+ @params = op[:params]
18
+ @headers = op[:headers]
19
+
20
+ # deep_dup to avoid unwanted changes across requests
21
+ @env = base_env.deep_dup
22
+ end
23
+
24
+ # Execute a batch request, returning a BatchResponse object. If an error
25
+ # occurs, it returns the same results as Rails would.
26
+ def execute
27
+ begin
28
+ action = identify_routing
29
+ process_env
30
+ BatchApi::Response.new(action.call(@env))
31
+ rescue => err
32
+ error_response(err)
33
+ end
34
+ end
35
+
36
+ # Internal: given a URL and other operation details as specified above,
37
+ # identify the appropriate controller and action to execute the action.
38
+ #
39
+ # Raises a routing error if the route doesn't exist.
40
+ #
41
+ # Returns the action object, which can be called with the environment.
42
+ def identify_routing
43
+ @path_params = Rails.application.routes.recognize_path(@url, @op)
44
+ @controller = ActionDispatch::Routing::RouteSet::Dispatcher.new.controller(@path_params)
45
+ @controller.action(@path_params[:action])
46
+ end
47
+
48
+ # Internal: customize the request environment. This is currently done
49
+ # manually and feels clunky and brittle, but is mostly likely fine, though
50
+ # there are one or two environment parameters not yet adjusted.
51
+ def process_env
52
+ path, qs = @url.split("?")
53
+
54
+ # rails routing
55
+ @env["action_dispatch.request.path_parameters"] = @path_params
56
+ # this isn't quite right, but hopefully it'll work
57
+ # since we're not executing any middleware
58
+ @env["action_controller.instance"] = @controller.new
59
+
60
+ # Headers
61
+ headrs = (@headers || {}).inject({}) do |heads, (k, v)|
62
+ heads.tap {|h| h["HTTP_" + k.gsub(/\-/, "_").upcase] = v}
63
+ end
64
+ # preserve original headers unless explicitly overridden
65
+ @env.merge!(headrs)
66
+
67
+ # method
68
+ @env["REQUEST_METHOD"] = @method.upcase
69
+
70
+ # path and query string
71
+ @env["REQUEST_URI"] = @env["REQUEST_URI"].gsub(/\/batch.*/, @url)
72
+ @env["REQUEST_PATH"] = path
73
+ @env["ORIGINAL_FULLPATH"] = @env["PATH_INFO"] = @url
74
+
75
+ @env["rack.request.query_string"] = @env["QUERY_STRING"] = qs
76
+
77
+ # parameters
78
+ @env["action_dispatch.request.parameters"] = @params
79
+ @env["action_dispatch.request.request_parameters"] = @params
80
+ @env["rack.request.query_hash"] = @method == "get" ? @params : nil
81
+ end
82
+
83
+ # Internal: create a BatchResponse for an exception thrown during batch
84
+ # processing.
85
+ def error_response(err)
86
+ wrapper = ActionDispatch::ExceptionWrapper.new(@env, err)
87
+ BatchApi::Response.new([
88
+ wrapper.status_code,
89
+ {},
90
+ BatchApi::Error.new(err)
91
+ ])
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,23 @@
1
+ require 'batch_api/error'
2
+
3
+ module BatchApi
4
+ # Public: a response from an internal operation in the Batch API.
5
+ # It contains all the details that are needed to describe the call's
6
+ # outcome.
7
+ class Response
8
+ # Public: the attributes of the HTTP response.
9
+ attr_accessor :status, :body, :headers, :cookies
10
+
11
+ # Public: create a new response representation from a Rack-compatible
12
+ # response (e.g. [status, headers, response_object]).
13
+ def initialize(response)
14
+ @status = response.first
15
+ @headers = response[1]
16
+
17
+ response_object = response[2]
18
+ @body = response_object.body
19
+ @cookies = response_object.cookies
20
+ end
21
+ end
22
+ end
23
+
@@ -0,0 +1,12 @@
1
+ module BatchApi
2
+ module RoutingHelper
3
+ DEFAULT_VERB = :post
4
+ DEFAULT_ENDPOINT = "/batch"
5
+
6
+ def batch_api(options = {})
7
+ endpoint = options.delete(:endpoint) || DEFAULT_ENDPOINT
8
+ verb = options.delete(:via) || DEFAULT_VERB
9
+ match({endpoint => "batch_api/batch#batch", via: verb}.merge(options))
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ module BatchApi
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :batch_api do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,145 @@
1
+ A proposal for a Batch API endpoint.
2
+
3
+ Batch requests take the form of a series of REST API requests,
4
+ each containing the following arguments:
5
+
6
+ * _url_ - the API endpoint to hit, formatted exactly as you would for a regular
7
+ REST API request (e.g. leading /, etc.)
8
+ * _method_ - what type of request to make -- GET, POST, PUT, etc.
9
+ * _args_ - a hash of arguments to the API. This can be used for both GET and
10
+ PUT/POST/PATCH requests.
11
+ * _headers_ - a hash of request-specific headers. (The headers sent in the
12
+ request will be included as well, with request-specific headers taking
13
+ precendence.)
14
+ * _options_ - a hash of additional batch request options. There are currently
15
+ none supported, but we plan to introduce some for dependency management,
16
+ supressing output, etc. in the future.
17
+
18
+ The Batch API endpoint itself (which lives at POST /batch) takes the
19
+ following arguments:
20
+
21
+ * _ops_ - an array of operations to perform, specified as described above.
22
+ * _sequential_ - execute all operations sequentially, rather than in parallel.
23
+ *THIS PARAMETER IS CURRENTLY REQUIRED AND MUST BE SET TO TRUE.* (In the future
24
+ we'll offer parallel processing by default, and hence this parameter must be
25
+ supplied in order topreserve expected behavior.
26
+
27
+ Other options may be defined in the future.
28
+
29
+ Users must be logged in to use the Batch API.
30
+
31
+ The Batch API returns an array of results in the same order the operations are
32
+ specified. Each result contains:
33
+
34
+ * _status_ - the HTTP status (200, 201, 400, etc.)
35
+ * _body_ - the rendered body
36
+ * _headers_ - any response headers
37
+ * _cookies_ - any cookies set by the request. (These will in the future be
38
+ pulled into the main response to be processed by the client.)
39
+
40
+ Errors in individual Batch API requests will be returned inline, with the
41
+ same status code and body they would return as individual requests. If the
42
+ Batch API itself returns a non-200 status code, that indicates a global
43
+ problem:
44
+
45
+ * _403_ - if the user isn't logged in
46
+ * _422_ - if the batch request isn't properly formatted
47
+ * _500_ - if there's an application error in the Batch API code
48
+
49
+ ** Examples **
50
+
51
+ Given the following request:
52
+
53
+ ```ruby
54
+ {
55
+ ops: [
56
+ {
57
+ method: "post",
58
+ url: "/resource/create",
59
+ args: {title: "bar", data: "foo"}
60
+ },
61
+ {
62
+ method: "get",
63
+ url: "/other_resource/123/connections"
64
+ },
65
+ {
66
+ method: "get",
67
+ url: "/i/gonna/throw/an/error",
68
+ header: { some: "headers" }
69
+ }
70
+ ]
71
+ }
72
+ ```
73
+
74
+ You'd get the following back:
75
+
76
+ ```ruby
77
+ [
78
+ {status: 201, body: "{json:\"data\"}", headers: {}, cookies: {}},
79
+ {status: 200, body: "[{json:\"data\"}, {more:\"data\"}]", headers: {}, cookies: {}},
80
+ {status: 500, body: "{error:\"message\"}", headers: {}, cookies: {}},
81
+ ]
82
+ ```
83
+
84
+ ** Implementation**
85
+
86
+ For each request, we:
87
+ * attempt to route it as Rails would (identifying controller and action)
88
+ * create a customized request.env hash with the appropriate details
89
+ * instantiate the controller and invoke the action
90
+ * parse and process the result
91
+
92
+ The overall result is then returned to the client.
93
+
94
+ **Background**
95
+
96
+ Batch APIs, though unRESTful, are useful for reducing HTTP overhead
97
+ by combining requests; this is particularly valuable for mobile clients,
98
+ which may generate groups of offline actions and which desire to
99
+ reduce battery consumption while connected by making fewer, better-compressed
100
+ requests.
101
+
102
+ Generally, such interfaces fall into two categories:
103
+
104
+ * a set of limited, specialized instructions, usually to manage resources
105
+ * a general-purpose API that can take any operation the main API can
106
+ handle
107
+
108
+ The second approach minimizes code duplication and complexity. Rather than
109
+ have two systems that manage resources (or a more complicated one that can
110
+ handle both batch and individual requests), we simply route requests as we
111
+ always would.
112
+
113
+ This approach has several benefits:
114
+
115
+ * Less complexity - non-batch endpoints don't need any extra code
116
+ * Complete flexibility - as we add new features or endpoints to the API,
117
+ they become immediately available via the Batch API.
118
+ * More RESTful - as individual operations are simply actions on RESTful
119
+ resources, we preserve an important characteristic of the API.
120
+
121
+ As well as general benefits of using the Batch API:
122
+
123
+ * Parallelizable - in the future, we could run requests in parallel (if
124
+ our Rails app is running in thread-safe mode), allowing clients to
125
+ specify explicit dependencies between operations (or run all
126
+ sequentially).
127
+ * Reuse of state - user authentication, request stack processing, and
128
+ similar processing only needs to be done once.
129
+ * Better for clients - fewer requests, better compressibility, etc.
130
+ (as described above)
131
+
132
+ There are two main downsides to our implementation:
133
+
134
+ * Rails dependency - we use only public Rails interfaces, but these could
135
+ still change with major updates. (_Resolution:_ with good testing we
136
+ can identify changes and update code as needed.)
137
+ * Reduced ability to optimize cross-request - unlike a specialized API,
138
+ each request will be treated in isolation, and so you couldn't minimize
139
+ DB updates through more complicated SQL logic. (_Resolution:_ none, but
140
+ the main pain point currently is at the HTTP connection layer, so we
141
+ accept this.)
142
+
143
+ Once the Batch API is more developed, we'll spin it off into a gem, and
144
+ possibly make it easy to create versions for Sinatra or other frameworks,
145
+ if desired.
metadata ADDED
@@ -0,0 +1,129 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: batch_api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Alex Koppel
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-08-13 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rails
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '3.2'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '3.2'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rspec
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec-rails
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: sqlite3
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ description: A Batch API plugin that provides a RESTful syntax, allowing clients to
79
+ make any number of REST calls with a single HTTP request.
80
+ email:
81
+ - alex@alexkoppel.com
82
+ executables: []
83
+ extensions: []
84
+ extra_rdoc_files: []
85
+ files:
86
+ - app/controllers/batch_api/batch_controller.rb
87
+ - lib/batch_api/engine.rb
88
+ - lib/batch_api/error.rb
89
+ - lib/batch_api/operation.rb
90
+ - lib/batch_api/response.rb
91
+ - lib/batch_api/routing_helper.rb
92
+ - lib/batch_api/version.rb
93
+ - lib/batch_api.rb
94
+ - lib/tasks/batch_api_tasks.rake
95
+ - MIT-LICENSE
96
+ - Rakefile
97
+ - changelog.md
98
+ - readme.md
99
+ homepage: http://github.com/arsduo/batch_api
100
+ licenses: []
101
+ post_install_message:
102
+ rdoc_options: []
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ none: false
107
+ requirements:
108
+ - - ! '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ segments:
112
+ - 0
113
+ hash: -1071837955116255101
114
+ required_rubygems_version: !ruby/object:Gem::Requirement
115
+ none: false
116
+ requirements:
117
+ - - ! '>='
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ segments:
121
+ - 0
122
+ hash: -1071837955116255101
123
+ requirements: []
124
+ rubyforge_project:
125
+ rubygems_version: 1.8.21
126
+ signing_key:
127
+ specification_version: 3
128
+ summary: A RESTful Batch API for Rails
129
+ test_files: []