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.
- data/MIT-LICENSE +20 -0
- data/Rakefile +30 -0
- data/app/controllers/batch_api/batch_controller.rb +10 -0
- data/changelog.md +2 -0
- data/lib/batch_api.rb +6 -0
- data/lib/batch_api/engine.rb +8 -0
- data/lib/batch_api/error.rb +32 -0
- data/lib/batch_api/operation.rb +94 -0
- data/lib/batch_api/response.rb +23 -0
- data/lib/batch_api/routing_helper.rb +12 -0
- data/lib/batch_api/version.rb +3 -0
- data/lib/tasks/batch_api_tasks.rake +4 -0
- data/readme.md +145 -0
- metadata +129 -0
data/MIT-LICENSE
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|
data/changelog.md
ADDED
data/lib/batch_api.rb
ADDED
@@ -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
|
data/readme.md
ADDED
@@ -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: []
|