rack-api 0.1.2 → 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.
- data/Gemfile.lock +5 -3
- data/README.rdoc +102 -0
- data/lib/rack/api.rb +8 -7
- data/lib/rack/api/app.rb +69 -6
- data/lib/rack/api/middleware.rb +1 -0
- data/lib/rack/api/middleware/format.rb +6 -4
- data/lib/rack/api/middleware/limit.rb +54 -0
- data/lib/rack/api/runner.rb +52 -3
- data/lib/rack/api/version.rb +2 -2
- data/rack-api.gemspec +2 -1
- data/spec/rack-api/api_throttling_spec.rb +89 -0
- data/spec/rack-api/format_spec.rb +28 -1
- data/spec/rack-api/headers_spec.rb +1 -1
- data/spec/rack-api/method_delegation_spec.rb +12 -0
- data/spec/rack-api/runner_spec.rb +29 -0
- data/spec/rack-api/url_for_spec.rb +63 -0
- data/spec/spec_helper.rb +2 -0
- metadata +23 -5
data/Gemfile.lock
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
rack-api (0.
|
5
|
-
activesupport (~> 3.0.
|
4
|
+
rack-api (0.2.0)
|
5
|
+
activesupport (~> 3.0.7)
|
6
6
|
rack (~> 1.2.1)
|
7
7
|
rack-mount (~> 0.6.14)
|
8
8
|
|
9
9
|
GEM
|
10
10
|
remote: http://rubygems.org/
|
11
11
|
specs:
|
12
|
-
activesupport (3.0.
|
12
|
+
activesupport (3.0.7)
|
13
13
|
archive-tar-minitar (0.5.2)
|
14
14
|
columnize (0.3.2)
|
15
15
|
diff-lcs (1.1.2)
|
@@ -20,6 +20,7 @@ GEM
|
|
20
20
|
rack (>= 1.0.0)
|
21
21
|
rack-test (0.5.7)
|
22
22
|
rack (>= 1.0)
|
23
|
+
redis (2.2.0)
|
23
24
|
rspec (2.5.0)
|
24
25
|
rspec-core (~> 2.5.0)
|
25
26
|
rspec-expectations (~> 2.5.0)
|
@@ -45,5 +46,6 @@ PLATFORMS
|
|
45
46
|
DEPENDENCIES
|
46
47
|
rack-api!
|
47
48
|
rack-test (~> 0.5.7)
|
49
|
+
redis (~> 2.2.0)
|
48
50
|
rspec (~> 2.5.0)
|
49
51
|
ruby-debug19
|
data/README.rdoc
CHANGED
@@ -124,6 +124,108 @@ Your <tt>Helpers</tt> module may look like this:
|
|
124
124
|
end
|
125
125
|
end
|
126
126
|
|
127
|
+
== Helpers
|
128
|
+
|
129
|
+
Every Rack::API action has several helper methods available through the
|
130
|
+
Rack::API::App class. Here's some of them:
|
131
|
+
|
132
|
+
=== logger
|
133
|
+
|
134
|
+
Logs specified message to the STDOUT.
|
135
|
+
|
136
|
+
get "/" do
|
137
|
+
logger.info "Hello index page!"
|
138
|
+
{}
|
139
|
+
end
|
140
|
+
|
141
|
+
=== headers
|
142
|
+
|
143
|
+
Define custom headers that will be sent to the client.
|
144
|
+
|
145
|
+
get "/" do
|
146
|
+
headers["X-Awesome"] = "U R Awesome"
|
147
|
+
{}
|
148
|
+
end
|
149
|
+
|
150
|
+
== params
|
151
|
+
|
152
|
+
Return current request parameters.
|
153
|
+
|
154
|
+
get "/" do
|
155
|
+
{:message => "Hello #{params[:name]}"}
|
156
|
+
end
|
157
|
+
|
158
|
+
== request
|
159
|
+
|
160
|
+
Return an object relative to the current request.
|
161
|
+
|
162
|
+
== credentials
|
163
|
+
|
164
|
+
This method will return an array container both username and password if client
|
165
|
+
sent Basic Authentication headers.
|
166
|
+
|
167
|
+
get "/" do
|
168
|
+
user, pass = credentials
|
169
|
+
{:message => "Hello #{user}"}
|
170
|
+
end
|
171
|
+
|
172
|
+
== url_for
|
173
|
+
|
174
|
+
Build an URL by merging segments, default URL options and hash with parameters.
|
175
|
+
|
176
|
+
Rack::API.app do
|
177
|
+
default_url_options :host => "example.com", :protocol => "https"
|
178
|
+
|
179
|
+
version "v1" do
|
180
|
+
get "/" do
|
181
|
+
{:url => url_for(:users, User.find(params[:id])), :format => :json}
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
== Useful middlewares
|
187
|
+
|
188
|
+
=== Rack::API::Middleware::SSL
|
189
|
+
|
190
|
+
This middleware will accept only HTTPS requests. Any request on HTTP will be dropped.
|
191
|
+
|
192
|
+
Rack::API.app do
|
193
|
+
use Rack::API::Middleware::SSL
|
194
|
+
end
|
195
|
+
|
196
|
+
=== Rack::API::Middleware::Limit
|
197
|
+
|
198
|
+
This middleware will limit access to API based on requests per hour. It requires a Redis connection.
|
199
|
+
|
200
|
+
Rack::API.app do
|
201
|
+
# Use the default settings.
|
202
|
+
# Will accept 60 requests/hour limited by IP address (REMOTE_ADDR)
|
203
|
+
use Rack::API::Middleware::Limit, :with => Redis.new
|
204
|
+
end
|
205
|
+
|
206
|
+
Other usages:
|
207
|
+
|
208
|
+
# Set custom limit/hour.
|
209
|
+
# Will accept ± 1 request/second.
|
210
|
+
use Rack::API::Middleware::Limit, :with => $redis, :limit => 3600
|
211
|
+
|
212
|
+
# Set custom string key.
|
213
|
+
# Will limit by something like env["X-Forwarded-For"].
|
214
|
+
use Rack::API::Middleware::Limit, :with => $redis, :key => "X-Forwarded-For"
|
215
|
+
|
216
|
+
# Set custom block key.
|
217
|
+
# Will limit by credential (Basic Auth).
|
218
|
+
Rack::API.app do
|
219
|
+
basic_auth do |user, pass|
|
220
|
+
User.authorize(user, pass)
|
221
|
+
end
|
222
|
+
|
223
|
+
use Rack::API::Middleware::Limit, :with => $redis, :key => proc {|env|
|
224
|
+
request = Rack::Auth::Basic::Request.new(env)
|
225
|
+
request.credentials[0]
|
226
|
+
}
|
227
|
+
end
|
228
|
+
|
127
229
|
== Maintainer
|
128
230
|
|
129
231
|
* Nando Vieira (http://nandovieira.com.br)
|
data/lib/rack/api.rb
CHANGED
@@ -1,23 +1,24 @@
|
|
1
1
|
require "rack"
|
2
2
|
require "rack/mount"
|
3
3
|
require "active_support/hash_with_indifferent_access"
|
4
|
+
require "active_support/core_ext/object/to_query"
|
4
5
|
require "json"
|
5
6
|
require "logger"
|
6
7
|
require "forwardable"
|
7
8
|
|
8
9
|
module Rack
|
9
10
|
class API
|
10
|
-
autoload :App, "rack/api/app"
|
11
|
-
autoload :Formatter, "rack/api/formatter"
|
12
|
-
autoload :Middleware, "rack/api/middleware"
|
13
|
-
autoload :Runner, "rack/api/runner"
|
14
|
-
autoload :Response, "rack/api/response"
|
15
|
-
autoload :Version, "rack/api/version"
|
11
|
+
autoload :App , "rack/api/app"
|
12
|
+
autoload :Formatter , "rack/api/formatter"
|
13
|
+
autoload :Middleware , "rack/api/middleware"
|
14
|
+
autoload :Runner , "rack/api/runner"
|
15
|
+
autoload :Response , "rack/api/response"
|
16
|
+
autoload :Version , "rack/api/version"
|
16
17
|
|
17
18
|
class << self
|
18
19
|
extend Forwardable
|
19
20
|
|
20
|
-
def_delegators :runner,
|
21
|
+
def_delegators :runner, *Runner::DELEGATE_METHODS
|
21
22
|
end
|
22
23
|
|
23
24
|
# A shortcut for defining new APIs. Instead of creating a
|
data/lib/rack/api/app.rb
CHANGED
@@ -23,13 +23,34 @@ module Rack
|
|
23
23
|
#
|
24
24
|
DEFAULT_MIME_TYPE = "application/octet-stream"
|
25
25
|
|
26
|
-
attr_reader :block
|
27
|
-
attr_reader :env
|
28
|
-
|
29
26
|
# Hold block that will be executed in case the
|
30
27
|
# route is recognized.
|
31
28
|
#
|
29
|
+
attr_accessor :handler
|
30
|
+
|
31
|
+
# Hold environment from current request.
|
32
|
+
#
|
33
|
+
attr_accessor :env
|
34
|
+
|
35
|
+
# Define which will be the default format when <tt>format=<format></tt>
|
36
|
+
# is not defined.
|
37
|
+
attr_accessor :default_format
|
38
|
+
|
39
|
+
# Set the default prefix path.
|
40
|
+
#
|
41
|
+
attr_accessor :prefix
|
42
|
+
|
43
|
+
# Specify the API version.
|
44
|
+
#
|
45
|
+
attr_accessor :version
|
46
|
+
|
47
|
+
# Hold url options.
|
48
|
+
#
|
49
|
+
attr_accessor :url_options
|
50
|
+
|
32
51
|
def initialize(options)
|
52
|
+
@url_options = {}
|
53
|
+
|
33
54
|
options.each do |name, value|
|
34
55
|
instance_variable_set("@#{name}", value)
|
35
56
|
end
|
@@ -62,7 +83,7 @@ module Rack
|
|
62
83
|
# Return the requested format. Defaults to JSON.
|
63
84
|
#
|
64
85
|
def format
|
65
|
-
params.fetch(:format,
|
86
|
+
params.fetch(:format, default_format)
|
66
87
|
end
|
67
88
|
|
68
89
|
# Stop processing by rendering the provided information.
|
@@ -126,14 +147,14 @@ module Rack
|
|
126
147
|
end
|
127
148
|
end
|
128
149
|
|
129
|
-
# Render the result of
|
150
|
+
# Render the result of handler.
|
130
151
|
#
|
131
152
|
def call(env) # :nodoc:
|
132
153
|
reset!
|
133
154
|
@env = env
|
134
155
|
|
135
156
|
response = catch(:error) do
|
136
|
-
render instance_eval(&
|
157
|
+
render instance_eval(&handler)
|
137
158
|
end
|
138
159
|
|
139
160
|
response.respond_to?(:to_rack) ? response.to_rack : response
|
@@ -149,6 +170,48 @@ module Rack
|
|
149
170
|
headers.fetch("Content-Type", mime)
|
150
171
|
end
|
151
172
|
|
173
|
+
# Return a URL path for all segments.
|
174
|
+
# You can set default options by using the
|
175
|
+
# Rack::API::Runner#default_url_options method.
|
176
|
+
#
|
177
|
+
# url_for :users
|
178
|
+
# #=> /users
|
179
|
+
#
|
180
|
+
# url_for :users, User.first
|
181
|
+
# #=> /users/1
|
182
|
+
#
|
183
|
+
# url_for :users, 1, :format => :json
|
184
|
+
# #=> /users/1?format=json
|
185
|
+
#
|
186
|
+
# url_for :users, :filters => [:name, :age]
|
187
|
+
# #=> /users?filters[]=name&filters[]=age
|
188
|
+
#
|
189
|
+
# URL segments can be any kind of object, first checking it responds to the
|
190
|
+
# <tt>to_param</tt> method. If not, converts object to string by using the
|
191
|
+
# <tt>to_s</tt> method.
|
192
|
+
#
|
193
|
+
def url_for(*args)
|
194
|
+
options = {}
|
195
|
+
options = args.pop if args.last.kind_of?(Hash)
|
196
|
+
|
197
|
+
segments = []
|
198
|
+
segments << url_options[:base_path] if url_options[:base_path]
|
199
|
+
segments << prefix if prefix
|
200
|
+
segments << version
|
201
|
+
segments += args.collect {|part| part.respond_to?(:to_param) ? part.to_param : part.to_s }
|
202
|
+
|
203
|
+
url = ""
|
204
|
+
url << url_options.fetch(:protocol, "http").to_s << "://"
|
205
|
+
url << url_options.fetch(:host, env["SERVER_NAME"])
|
206
|
+
|
207
|
+
port = url_options.fetch(:port, env["SERVER_PORT"]).to_i
|
208
|
+
url << ":" << port.to_s if port.nonzero? && port != 80
|
209
|
+
|
210
|
+
url << Rack::Mount::Utils.normalize_path(segments.join("/"))
|
211
|
+
url << "?" << options.to_param if options.any?
|
212
|
+
url
|
213
|
+
end
|
214
|
+
|
152
215
|
private
|
153
216
|
def render(response) # :nodoc:
|
154
217
|
[status, headers.merge("Content-Type" => content_type), [format_response(response)]]
|
data/lib/rack/api/middleware.rb
CHANGED
@@ -2,19 +2,21 @@ module Rack
|
|
2
2
|
class API
|
3
3
|
module Middleware
|
4
4
|
class Format
|
5
|
-
def initialize(app, formats)
|
6
|
-
@app
|
5
|
+
def initialize(app, default_format, formats)
|
6
|
+
@app = app
|
7
|
+
@default_format = default_format.to_s
|
8
|
+
@formats = formats.collect {|f| f.to_s}
|
7
9
|
end
|
8
10
|
|
9
11
|
def call(env)
|
10
12
|
request = Rack::Request.new(env)
|
11
13
|
params = request.env["rack.routing_args"].merge(request.params)
|
12
|
-
requested_format = params.fetch(:format,
|
14
|
+
requested_format = params.fetch(:format, @default_format)
|
13
15
|
|
14
16
|
if @formats.include?(requested_format)
|
15
17
|
@app.call(env)
|
16
18
|
else
|
17
|
-
[406, {"Content-Type" => "text/plain"}, ["Invalid format"]]
|
19
|
+
[406, {"Content-Type" => "text/plain"}, ["Invalid format. Accepts one of [#{@formats.join(", ")}]"]]
|
18
20
|
end
|
19
21
|
end
|
20
22
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Rack
|
2
|
+
class API
|
3
|
+
module Middleware
|
4
|
+
class Limit
|
5
|
+
attr_reader :options, :env
|
6
|
+
|
7
|
+
def initialize(app, options = {})
|
8
|
+
@app = app
|
9
|
+
@options = {
|
10
|
+
:limit => 60,
|
11
|
+
:key => "REMOTE_ADDR",
|
12
|
+
:with => Redis.new
|
13
|
+
}.merge(options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(env)
|
17
|
+
@env = env
|
18
|
+
|
19
|
+
if authorized?
|
20
|
+
@app.call(env)
|
21
|
+
else
|
22
|
+
[503, {"Content-Type" => "text/plain"}, ["Over Rate Limit."]]
|
23
|
+
end
|
24
|
+
rescue Exception => e
|
25
|
+
@app.call(env)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
def authorized?
|
30
|
+
count = redis.incr(key)
|
31
|
+
redis.expire(key, 3600)
|
32
|
+
|
33
|
+
count <= options[:limit] || redis.sismember("api:whitelist", identifier)
|
34
|
+
end
|
35
|
+
|
36
|
+
def redis
|
37
|
+
options[:with]
|
38
|
+
end
|
39
|
+
|
40
|
+
def identifier
|
41
|
+
@identifier ||= begin
|
42
|
+
options[:key].respond_to?(:call) ? options[:key].call(env).to_s : env[options[:key].to_s]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def key
|
47
|
+
@key ||= begin
|
48
|
+
"api:#{identifier}:#{Time.now.strftime("%Y%m%d%H")}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/rack/api/runner.rb
CHANGED
@@ -3,6 +3,11 @@ module Rack
|
|
3
3
|
class Runner
|
4
4
|
HTTP_METHODS = %w[get post put delete head]
|
5
5
|
|
6
|
+
DELEGATE_METHODS = %w[
|
7
|
+
version use prefix basic_auth
|
8
|
+
helper respond_to default_url_options
|
9
|
+
]
|
10
|
+
|
6
11
|
attr_accessor :settings
|
7
12
|
|
8
13
|
def initialize
|
@@ -79,6 +84,32 @@ module Rack
|
|
79
84
|
set :helpers, mod, :append
|
80
85
|
end
|
81
86
|
|
87
|
+
# Define the server endpoint. Will be used if you call the method
|
88
|
+
# Rack::API::App#url_for.
|
89
|
+
#
|
90
|
+
# The following options are supported:
|
91
|
+
#
|
92
|
+
# * <tt>:host</tt> – Specifies the host the link should be targeted at.
|
93
|
+
# * <tt>:protocol</tt> – The protocol to connect to. Defaults to 'http'.
|
94
|
+
# * <tt>:port</tt> – Optionally specify the port to connect to.
|
95
|
+
# * <tt>:base_path</tt> – Optionally specify a base path.
|
96
|
+
#
|
97
|
+
# default_url_options :host => "myhost.com"
|
98
|
+
# #=> http://myhost.com
|
99
|
+
#
|
100
|
+
# default_url_options :host => "myhost.com", :protocol => "https"
|
101
|
+
# #=> https://myhost.com
|
102
|
+
#
|
103
|
+
# default_url_options :host => "myhost.com", :port => 3000
|
104
|
+
# #=> http://myhost.com:3000
|
105
|
+
#
|
106
|
+
# default_url_options :host => "myhost.com", :base_path => "my/custom/path"
|
107
|
+
# #=> http://myhost.com/my/custom/path
|
108
|
+
#
|
109
|
+
def default_url_options(options)
|
110
|
+
set :url_options, options
|
111
|
+
end
|
112
|
+
|
82
113
|
# Create a new API version.
|
83
114
|
#
|
84
115
|
def version(name, &block)
|
@@ -157,6 +188,13 @@ module Rack
|
|
157
188
|
#
|
158
189
|
# The code above will accept only <tt>:json</tt> as format on version <tt>:v1</tt>.
|
159
190
|
#
|
191
|
+
# Also, the first value provided to this method will be used as default format,
|
192
|
+
# which means that requests that don't provide the <tt>:format</tt> param, will use
|
193
|
+
# this value.
|
194
|
+
#
|
195
|
+
# respond_to :fffuuu, :json
|
196
|
+
# #=> the default format is fffuuu
|
197
|
+
#
|
160
198
|
def respond_to(*formats)
|
161
199
|
set :formats, formats
|
162
200
|
end
|
@@ -199,8 +237,19 @@ module Rack
|
|
199
237
|
Rack::Mount::Utils.normalize_path([option(:prefix), settings[:version], path].join("/"))
|
200
238
|
end
|
201
239
|
|
202
|
-
def
|
203
|
-
|
240
|
+
def default_format # :nodoc:
|
241
|
+
(option(:formats).first || "json").to_s
|
242
|
+
end
|
243
|
+
|
244
|
+
def build_app(handler) # :nodoc:
|
245
|
+
app = App.new({
|
246
|
+
:handler => handler,
|
247
|
+
:default_format => default_format,
|
248
|
+
:version => option(:version),
|
249
|
+
:prefix => option(:prefix),
|
250
|
+
:url_options => option(:url_options)
|
251
|
+
})
|
252
|
+
|
204
253
|
builder = Rack::Builder.new
|
205
254
|
|
206
255
|
# Add middleware for basic authentication.
|
@@ -208,7 +257,7 @@ module Rack
|
|
208
257
|
builder.use Rack::Auth::Basic, auth[0], &auth[1] if auth && auth != :none
|
209
258
|
|
210
259
|
# Add middleware for format validation.
|
211
|
-
builder.use Rack::API::Middleware::Format, option(:formats)
|
260
|
+
builder.use Rack::API::Middleware::Format, default_format, option(:formats)
|
212
261
|
|
213
262
|
# Add middlewares to executation stack.
|
214
263
|
option(:middlewares, :merge).each {|middleware| builder.use(*middleware)}
|
data/lib/rack/api/version.rb
CHANGED
data/rack-api.gemspec
CHANGED
@@ -19,8 +19,9 @@ Gem::Specification.new do |s|
|
|
19
19
|
|
20
20
|
s.add_dependency "rack", "~> 1.2.1"
|
21
21
|
s.add_dependency "rack-mount", "~> 0.6.14"
|
22
|
-
s.add_dependency "activesupport", "~> 3.0.
|
22
|
+
s.add_dependency "activesupport", "~> 3.0.7"
|
23
23
|
s.add_development_dependency "rspec", "~> 2.5.0"
|
24
24
|
s.add_development_dependency "rack-test", "~> 0.5.7"
|
25
|
+
s.add_development_dependency "redis", "~> 2.2.0"
|
25
26
|
s.add_development_dependency "ruby-debug19" if RUBY_VERSION >= "1.9"
|
26
27
|
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Rack::API::Middleware::Limit do
|
4
|
+
let(:action) { proc {|env| [200, {}, ["success"]] } }
|
5
|
+
let(:env) {
|
6
|
+
Rack::MockRequest.env_for("/v1",
|
7
|
+
"REMOTE_ADDR" => "127.0.0.1",
|
8
|
+
"X-API_KEY" => "fo7hy7ra",
|
9
|
+
"HTTP_AUTHORIZATION" => basic_auth("admin", "test")
|
10
|
+
)
|
11
|
+
}
|
12
|
+
|
13
|
+
subject { Rack::API::Middleware::Limit.new(action) }
|
14
|
+
|
15
|
+
before do
|
16
|
+
Time.stub :now => Time.parse("2011-04-08 00:00:00")
|
17
|
+
@stamp = Time.now.strftime("%Y%m%d%H")
|
18
|
+
|
19
|
+
begin
|
20
|
+
$redis = Redis.new
|
21
|
+
$redis.del "api:127.0.0.1:#{@stamp}"
|
22
|
+
$redis.del "api:fo7hy7ra:#{@stamp}"
|
23
|
+
$redis.del "api:admin:#{@stamp}"
|
24
|
+
$redis.del "api:whitelist"
|
25
|
+
subject.options.merge!(:with => $redis)
|
26
|
+
rescue Errno::ECONNREFUSED => e
|
27
|
+
pending "Redis is not running"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context "using default options" do
|
32
|
+
it "renders action when limit wasn't exceeded" do
|
33
|
+
results = 60.times.collect { subject.call(env) }
|
34
|
+
|
35
|
+
$redis.get("api:127.0.0.1:#{@stamp}").to_i.should == 60
|
36
|
+
results.last[0].should == 200
|
37
|
+
end
|
38
|
+
|
39
|
+
it "renders 503 when limit was exceeded" do
|
40
|
+
results = 61.times.collect { subject.call(env) }
|
41
|
+
|
42
|
+
$redis.get("api:127.0.0.1:#{@stamp}").to_i.should == 61
|
43
|
+
results.last[0].should == 503
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context "using custom options" do
|
48
|
+
it "respects limit" do
|
49
|
+
subject.options.merge!(:limit => 20)
|
50
|
+
|
51
|
+
results = 20.times.collect { subject.call(env) }
|
52
|
+
|
53
|
+
$redis.get("api:127.0.0.1:#{@stamp}").to_i.should == 20
|
54
|
+
results.last[0].should == 200
|
55
|
+
end
|
56
|
+
|
57
|
+
it "uses custom string key" do
|
58
|
+
subject.options.merge!(:key => "X-API_KEY")
|
59
|
+
status, headers, result = subject.call(env)
|
60
|
+
|
61
|
+
$redis.get("api:fo7hy7ra:#{@stamp}").to_i.should == 1
|
62
|
+
status.should == 200
|
63
|
+
end
|
64
|
+
|
65
|
+
it "uses custom block key" do
|
66
|
+
subject.options.merge! :key => proc {|env|
|
67
|
+
request = Rack::Auth::Basic::Request.new(env)
|
68
|
+
request.credentials[0]
|
69
|
+
}
|
70
|
+
|
71
|
+
status, headers, result = subject.call(env)
|
72
|
+
|
73
|
+
$redis.get("api:admin:#{@stamp}").to_i.should == 1
|
74
|
+
status.should == 200
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
context "whitelist" do
|
79
|
+
it "bypasses API limit" do
|
80
|
+
$redis.sadd("api:whitelist", "127.0.0.1")
|
81
|
+
|
82
|
+
subject.options.merge!(:limit => 5)
|
83
|
+
results = 10.times.collect { subject.call(env) }
|
84
|
+
|
85
|
+
$redis.get("api:127.0.0.1:#{@stamp}").to_i.should == 10
|
86
|
+
results.last[0].should == 200
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -26,12 +26,39 @@ describe Rack::API, "Format" do
|
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
29
|
+
context "default format" do
|
30
|
+
it "is json" do
|
31
|
+
Rack::API.app do
|
32
|
+
version :v2 do
|
33
|
+
get("/") { {:success => true} }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
get "/v2"
|
38
|
+
last_response.headers["Content-Type"].should == "application/json"
|
39
|
+
end
|
40
|
+
|
41
|
+
it "is set to the first respond_to value" do
|
42
|
+
Rack::API::App::MIME_TYPES["fffuuu"] = "application/x-fffuuu"
|
43
|
+
|
44
|
+
Rack::API.app do
|
45
|
+
version :v2 do
|
46
|
+
respond_to :fffuuu, :json
|
47
|
+
get("/") { OpenStruct.new(:to_fffuuu => "Fffuuu") }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
get "/v2"
|
52
|
+
last_response.headers["Content-Type"].should == "application/x-fffuuu"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
29
56
|
context "invalid format" do
|
30
57
|
it "renders 406" do
|
31
58
|
get "/v1/users.invalid"
|
32
59
|
|
33
60
|
last_response.status.should == 406
|
34
|
-
last_response.body.should == "Invalid format"
|
61
|
+
last_response.body.should == "Invalid format. Accepts one of [json, jsonp, awesome, fffuuu, zomg]"
|
35
62
|
last_response.headers["Content-Type"].should == "text/plain"
|
36
63
|
end
|
37
64
|
end
|
@@ -6,7 +6,7 @@ describe Rack::API, "Headers" do
|
|
6
6
|
version :v1 do
|
7
7
|
get("/users(.:format)") do
|
8
8
|
headers["X-Awesome"] = "U R Awesome"
|
9
|
-
headers["Content-Type"] = "application/x-json"
|
9
|
+
headers["Content-Type"] = "application/x-json" # the default json header is application/json
|
10
10
|
end
|
11
11
|
end
|
12
12
|
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Rack::API, "delegators" do
|
4
|
+
subject { Rack::API }
|
5
|
+
|
6
|
+
it { should respond_to(:version) }
|
7
|
+
it { should respond_to(:use) }
|
8
|
+
it { should respond_to(:prefix) }
|
9
|
+
it { should respond_to(:basic_auth) }
|
10
|
+
it { should respond_to(:helper) }
|
11
|
+
it { should respond_to(:default_url_options) }
|
12
|
+
end
|
@@ -1,6 +1,11 @@
|
|
1
1
|
require "spec_helper"
|
2
2
|
|
3
3
|
describe Rack::API::Runner do
|
4
|
+
specify "sanity check for delegate methods" do
|
5
|
+
# remember to update spec/method_delegation_spec.rb
|
6
|
+
Rack::API::Runner::DELEGATE_METHODS.size.should == 7
|
7
|
+
end
|
8
|
+
|
4
9
|
it "responds to http methods" do
|
5
10
|
subject.should respond_to(:get)
|
6
11
|
subject.should respond_to(:post)
|
@@ -19,6 +24,11 @@ describe Rack::API::Runner do
|
|
19
24
|
subject.option(:prefix).should == "my/awesome/api"
|
20
25
|
end
|
21
26
|
|
27
|
+
it "stores default url options" do
|
28
|
+
subject.default_url_options(:host => "example.com")
|
29
|
+
subject.option(:url_options).should == {:host => "example.com"}
|
30
|
+
end
|
31
|
+
|
22
32
|
it "stores middleware" do
|
23
33
|
subject.use Rack::Auth::Basic
|
24
34
|
subject.option(:middlewares, :merge).should == [[Rack::Auth::Basic]]
|
@@ -30,4 +40,23 @@ describe Rack::API::Runner do
|
|
30
40
|
subject.basic_auth("Get out!", &handler)
|
31
41
|
subject.settings[:global][:auth].should == ["Get out!", handler]
|
32
42
|
end
|
43
|
+
|
44
|
+
it "initializes application with correct parameters" do
|
45
|
+
expected = {
|
46
|
+
:version => "v1",
|
47
|
+
:url_options => {:host => "mysite.com"},
|
48
|
+
:default_format => "fffuuu",
|
49
|
+
:prefix => "api",
|
50
|
+
:handler => proc {}
|
51
|
+
}
|
52
|
+
|
53
|
+
Rack::API::App.should_receive(:new).with(hash_including(expected)).once
|
54
|
+
subject.version("v1") do
|
55
|
+
respond_to :fffuuu
|
56
|
+
prefix "api"
|
57
|
+
default_url_options :host => "mysite.com"
|
58
|
+
|
59
|
+
get("/", &expected[:handler])
|
60
|
+
end
|
61
|
+
end
|
33
62
|
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Rack::API::App, "#url_for" do
|
4
|
+
subject { Rack::API::App.new(
|
5
|
+
:version => "v1",
|
6
|
+
:url_options => {},
|
7
|
+
:env => Rack::MockRequest.env_for("/v1")
|
8
|
+
)}
|
9
|
+
|
10
|
+
it "returns url considering prefix" do
|
11
|
+
subject.prefix = "api"
|
12
|
+
subject.url_for.should == "http://example.org/api/v1"
|
13
|
+
end
|
14
|
+
|
15
|
+
it "ignores prefix when is not set" do
|
16
|
+
subject.prefix = nil
|
17
|
+
subject.url_for.should == "http://example.org/v1"
|
18
|
+
end
|
19
|
+
|
20
|
+
it "returns host" do
|
21
|
+
subject.url_for.should == "http://example.org/v1"
|
22
|
+
end
|
23
|
+
|
24
|
+
it "uses a different host" do
|
25
|
+
subject.url_options.merge!(:host => "mysite.com")
|
26
|
+
subject.url_for.should == "http://mysite.com/v1"
|
27
|
+
end
|
28
|
+
|
29
|
+
it "uses a different protocol" do
|
30
|
+
subject.url_options.merge!(:protocol => "https")
|
31
|
+
subject.url_for.should == "https://example.org/v1"
|
32
|
+
end
|
33
|
+
|
34
|
+
it "uses a different port" do
|
35
|
+
subject.url_options.merge!(:port => "2345")
|
36
|
+
subject.url_for.should == "http://example.org:2345/v1"
|
37
|
+
end
|
38
|
+
|
39
|
+
it "uses #to_param when available" do
|
40
|
+
subject.url_for("users", mock(:user, :to_param => "1-john-doe")).should == "http://example.org/v1/users/1-john-doe"
|
41
|
+
end
|
42
|
+
|
43
|
+
it "converts other data types" do
|
44
|
+
subject.url_for(:users, 1).should == "http://example.org/v1/users/1"
|
45
|
+
end
|
46
|
+
|
47
|
+
it "adds query string" do
|
48
|
+
actual = subject.url_for(:format => :json, :filters => [:name, :age])
|
49
|
+
actual.should == "http://example.org/v1?filters[]=name&filters[]=age&format=json"
|
50
|
+
end
|
51
|
+
|
52
|
+
it "uses host from request" do
|
53
|
+
env = Rack::MockRequest.env_for("/v1", "SERVER_NAME" => "mysite.com")
|
54
|
+
subject = Rack::API::App.new(:version => "v1", :env => env)
|
55
|
+
subject.url_for.should == "http://mysite.com/v1"
|
56
|
+
end
|
57
|
+
|
58
|
+
it "uses port from request" do
|
59
|
+
env = Rack::MockRequest.env_for("/v1", "SERVER_PORT" => "2345")
|
60
|
+
subject = Rack::API::App.new(:version => "v1", :env => env)
|
61
|
+
subject.url_for.should == "http://example.org:2345/v1"
|
62
|
+
end
|
63
|
+
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: rack-api
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.
|
5
|
+
version: 0.2.0
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Nando Vieira
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2011-04-
|
13
|
+
date: 2011-04-19 00:00:00 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: rack
|
@@ -42,7 +42,7 @@ dependencies:
|
|
42
42
|
requirements:
|
43
43
|
- - ~>
|
44
44
|
- !ruby/object:Gem::Version
|
45
|
-
version: 3.0.
|
45
|
+
version: 3.0.7
|
46
46
|
type: :runtime
|
47
47
|
version_requirements: *id003
|
48
48
|
- !ruby/object:Gem::Dependency
|
@@ -68,16 +68,27 @@ dependencies:
|
|
68
68
|
type: :development
|
69
69
|
version_requirements: *id005
|
70
70
|
- !ruby/object:Gem::Dependency
|
71
|
-
name:
|
71
|
+
name: redis
|
72
72
|
prerelease: false
|
73
73
|
requirement: &id006 !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ~>
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: 2.2.0
|
79
|
+
type: :development
|
80
|
+
version_requirements: *id006
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
name: ruby-debug19
|
83
|
+
prerelease: false
|
84
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
74
85
|
none: false
|
75
86
|
requirements:
|
76
87
|
- - ">="
|
77
88
|
- !ruby/object:Gem::Version
|
78
89
|
version: "0"
|
79
90
|
type: :development
|
80
|
-
version_requirements: *
|
91
|
+
version_requirements: *id007
|
81
92
|
description: Create web app APIs that respond to one or more formats using an elegant DSL.
|
82
93
|
email:
|
83
94
|
- fnando.vieira@gmail.com
|
@@ -110,17 +121,20 @@ files:
|
|
110
121
|
- lib/rack/api/formatter/jsonp.rb
|
111
122
|
- lib/rack/api/middleware.rb
|
112
123
|
- lib/rack/api/middleware/format.rb
|
124
|
+
- lib/rack/api/middleware/limit.rb
|
113
125
|
- lib/rack/api/middleware/ssl.rb
|
114
126
|
- lib/rack/api/response.rb
|
115
127
|
- lib/rack/api/runner.rb
|
116
128
|
- lib/rack/api/version.rb
|
117
129
|
- rack-api.gemspec
|
130
|
+
- spec/rack-api/api_throttling_spec.rb
|
118
131
|
- spec/rack-api/basic_auth_spec.rb
|
119
132
|
- spec/rack-api/format_spec.rb
|
120
133
|
- spec/rack-api/headers_spec.rb
|
121
134
|
- spec/rack-api/helpers_spec.rb
|
122
135
|
- spec/rack-api/http_methods_spec.rb
|
123
136
|
- spec/rack-api/inheritance_spec.rb
|
137
|
+
- spec/rack-api/method_delegation_spec.rb
|
124
138
|
- spec/rack-api/middlewares_spec.rb
|
125
139
|
- spec/rack-api/params_spec.rb
|
126
140
|
- spec/rack-api/paths_spec.rb
|
@@ -128,6 +142,7 @@ files:
|
|
128
142
|
- spec/rack-api/settings_spec.rb
|
129
143
|
- spec/rack-api/short_circuit_spec.rb
|
130
144
|
- spec/rack-api/ssl_spec.rb
|
145
|
+
- spec/rack-api/url_for_spec.rb
|
131
146
|
- spec/spec_helper.rb
|
132
147
|
- spec/support/awesome_middleware.rb
|
133
148
|
- spec/support/core_ext.rb
|
@@ -162,12 +177,14 @@ signing_key:
|
|
162
177
|
specification_version: 3
|
163
178
|
summary: Create web app APIs that respond to one or more formats using an elegant DSL.
|
164
179
|
test_files:
|
180
|
+
- spec/rack-api/api_throttling_spec.rb
|
165
181
|
- spec/rack-api/basic_auth_spec.rb
|
166
182
|
- spec/rack-api/format_spec.rb
|
167
183
|
- spec/rack-api/headers_spec.rb
|
168
184
|
- spec/rack-api/helpers_spec.rb
|
169
185
|
- spec/rack-api/http_methods_spec.rb
|
170
186
|
- spec/rack-api/inheritance_spec.rb
|
187
|
+
- spec/rack-api/method_delegation_spec.rb
|
171
188
|
- spec/rack-api/middlewares_spec.rb
|
172
189
|
- spec/rack-api/params_spec.rb
|
173
190
|
- spec/rack-api/paths_spec.rb
|
@@ -175,6 +192,7 @@ test_files:
|
|
175
192
|
- spec/rack-api/settings_spec.rb
|
176
193
|
- spec/rack-api/short_circuit_spec.rb
|
177
194
|
- spec/rack-api/ssl_spec.rb
|
195
|
+
- spec/rack-api/url_for_spec.rb
|
178
196
|
- spec/spec_helper.rb
|
179
197
|
- spec/support/awesome_middleware.rb
|
180
198
|
- spec/support/core_ext.rb
|