rack-api 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|