├── .gitignore
├── .rspec
├── Gemfile
├── Gemfile.lock
├── README.rdoc
├── Rakefile
├── examples
├── basic_auth.rb
├── config.ru
├── controller.rb
├── custom_class.rb
├── custom_format.rb
├── custom_headers.rb
├── formats.rb
├── helpers.rb
├── middleware.rb
├── multiple_versions.rb
├── params.rb
├── rescue_from.rb
├── simple.rb
└── without_version_and_prefix.rb
├── lib
└── rack
│ ├── api.rb
│ └── api
│ ├── controller.rb
│ ├── formatter.rb
│ ├── formatter
│ ├── base.rb
│ └── jsonp.rb
│ ├── middleware.rb
│ ├── middleware
│ ├── format.rb
│ ├── limit.rb
│ └── ssl.rb
│ ├── response.rb
│ ├── runner.rb
│ └── version.rb
├── rack-api.gemspec
└── spec
├── rack-api
├── api_throttling_spec.rb
├── basic_auth_spec.rb
├── controller_spec.rb
├── format_spec.rb
├── headers_spec.rb
├── helpers_spec.rb
├── http_methods_spec.rb
├── inheritance_spec.rb
├── method_delegation_spec.rb
├── middlewares_spec.rb
├── params_spec.rb
├── paths_spec.rb
├── rescue_from_spec.rb
├── runner_spec.rb
├── separators_spec.rb
├── settings_spec.rb
├── short_circuit_spec.rb
├── ssl_spec.rb
└── url_for_spec.rb
├── spec_helper.rb
└── support
├── awesome_middleware.rb
├── core_ext.rb
├── helpers.rb
├── myapp.rb
├── mycontroller.rb
└── zomg_middleware.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | pkg
3 | tmp
4 | docs
5 | *.rdb
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color --format documentation
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source :rubygems
2 | gem "rack-test", :git => "https://github.com/brynary/rack-test.git"
3 |
4 | gemspec
5 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GIT
2 | remote: https://github.com/brynary/rack-test.git
3 | revision: cab8eb929b45d9e32474fad1b6806c2ab8f6bea8
4 | specs:
5 | rack-test (0.6.1)
6 | rack (>= 1.0)
7 |
8 | PATH
9 | remote: .
10 | specs:
11 | rack-api (1.1.0)
12 | activesupport (>= 3.0)
13 | i18n
14 | rack (>= 1.0.0)
15 | rack-mount (>= 0.6.0)
16 |
17 | GEM
18 | remote: http://rubygems.org/
19 | specs:
20 | activesupport (3.2.2)
21 | i18n (~> 0.6)
22 | multi_json (~> 1.0)
23 | awesome_print (1.0.2)
24 | coderay (1.0.5)
25 | diff-lcs (1.1.3)
26 | i18n (0.6.0)
27 | method_source (0.7.1)
28 | multi_json (1.2.0)
29 | pry (0.9.8.4)
30 | coderay (~> 1.0.5)
31 | method_source (~> 0.7.1)
32 | slop (>= 2.4.4, < 3)
33 | rack (1.4.1)
34 | rack-mount (0.8.3)
35 | rack (>= 1.0.0)
36 | rake (0.9.2.2)
37 | redis (2.2.2)
38 | rspec (2.9.0)
39 | rspec-core (~> 2.9.0)
40 | rspec-expectations (~> 2.9.0)
41 | rspec-mocks (~> 2.9.0)
42 | rspec-core (2.9.0)
43 | rspec-expectations (2.9.0)
44 | diff-lcs (~> 1.1.3)
45 | rspec-mocks (2.9.0)
46 | slop (2.4.4)
47 |
48 | PLATFORMS
49 | ruby
50 |
51 | DEPENDENCIES
52 | awesome_print
53 | pry
54 | rack-api!
55 | rack-test!
56 | rake (~> 0.9)
57 | redis (~> 2.2.0)
58 | rspec (~> 2.6)
59 |
--------------------------------------------------------------------------------
/README.rdoc:
--------------------------------------------------------------------------------
1 | = Rack::API
2 |
3 | Create web app APIs that respond to one or more formats using an elegant DSL.
4 |
5 | == Installation
6 |
7 | gem install rack-api
8 |
9 | == Usage
10 |
11 | === Basic example
12 |
13 | Rack::API.app do
14 | prefix "api"
15 |
16 | version :v1 do
17 | get "users(.:format)" do
18 | User.all
19 | end
20 |
21 | get "users/:id(.:format)" do
22 | User.find(params[:id])
23 | end
24 | end
25 | end
26 |
27 | === Starting a server with Rack
28 |
29 | To run Rack::API through Rack (`config.ru`), you just need to provide your class. If
30 | you're using the DSL format, you need to provide the Rack::API class.
31 |
32 | require "rack/api"
33 | run Rack::API
34 |
35 | Otherwise, just provide your custom class.
36 |
37 | require "rack/api"
38 |
39 | class MyApp < Rack::API
40 | get "/" do
41 | {:message => "Hello World"}
42 | end
43 | end
44 |
45 | run MyApp
46 |
47 | Now, you can execute `rackup` and your app will be available through the 9292 port.
48 |
49 | $ rackup -p 3000
50 | [2011-08-05 20:38:11] INFO WEBrick 1.3.1
51 | [2011-08-05 20:38:11] INFO ruby 1.9.3 (2011-07-31) [x86_64-darwin11.0.0]
52 | [2011-08-05 20:38:11] INFO WEBrick::HTTPServer#start: pid=95318 port=3000
53 |
54 | $ curl localhost:3000
55 | {"message" => "Hello World"}
56 |
57 | You can also run other application servers that recognize Rack, like Thin.
58 |
59 | $ thin -R config.ru start
60 | >> Thin web server (v1.2.11 codename Bat-Shit Crazy)
61 | >> Maximum connections set to 1024
62 | >> Listening on 0.0.0.0:3000, CTRL+C to stop
63 |
64 | $ curl localhost:9292
65 | {"message" => "Hello World"}
66 |
67 | === Rails Integration
68 |
69 | First, set up your Gemfile like this:
70 |
71 | gem "rack-api", "~> 1.0", :require => "rack/api"
72 |
73 | Create your API somewhere. In this example, we'll add it to lib/api.rb.
74 |
75 | Rack::API.app do
76 | prefix "api"
77 |
78 | version :v1 do
79 | get "status(.:format)" do
80 | {:success => true, :time => Time.now}
81 | end
82 | end
83 | end
84 |
85 | Load this file somehow. I'd create a config/initializers/dependencies.rb with something like
86 |
87 | require "lib/api"
88 |
89 | Finally, you can set up the API routing. Open config/routes.rb and add the following line:
90 |
91 | mount Rack::API => "/"
92 |
93 | If you define your API by inheriting from the Rack::API class, remember to mount your class instead.
94 |
95 | mount MyAPI => "/"
96 |
97 | For additional examples, see https://github.com/fnando/rack-api/tree/master/examples.
98 |
99 | === Using RSpec with Rack::API
100 |
101 | You can easily test Rack::API apps by using Rack::Test. This applies to both RSpec and Test Unit. See what you need to do if you want to use it with RSpec.
102 |
103 | First, open your spec/spec_helper.rb and add something like this:
104 |
105 | require "rspec"
106 | require "rack/test"
107 |
108 | RSpec.configure do |config|
109 | config.include Rack::Test::Methods
110 | end
111 |
112 | Then you can go to your spec file, say, spec/api_spec.rb. You need to define a helper method called +app+, which will point to your Rack::API (the class itself or your own class).
113 |
114 | require "spec_helper"
115 |
116 | describe Rack::API do
117 | # Remember to use your own class if you
118 | # inherited from Rack::API
119 | def app; Rack::API; end
120 |
121 | it "renders status page" do
122 | get "/api/v1/status"
123 | last_response.body.should == {:status => "running"}.to_json
124 | last_response.status.should == 200
125 | end
126 | end
127 |
128 | If you want to do expectations over basic authentication, you'll have some like this:
129 |
130 | require "spec_helper"
131 |
132 | describe Rack::API do
133 | def basic_auth(username, password)
134 | "Basic " + Base64.encode64("#{username}:#{password}")
135 | end
136 |
137 | it "requires authentication" do
138 | get "/api/v1/status"
139 | last_response.status.should == 401
140 | end
141 |
142 | it "grants access" do
143 | get "/api/v1/status", {"HTTP_AUTHORIZATION" => basic_auth("john", "test")}
144 | last_response.status.should == 200
145 | end
146 | end
147 |
148 | To reduce duplication, you can move both basic_auth and app methods to a module, which will be included on RSpec.
149 |
150 | RSpec.configure do |config|
151 | config.include Rack::Test::Methods
152 | config.include Helpers
153 | end
154 |
155 | Your Helpers module may look like this:
156 |
157 | module Helpers
158 | def app
159 | Rack::API
160 | end
161 |
162 | def basic_auth(username, password)
163 | "Basic " + Base64.encode64("#{username}:#{password}")
164 | end
165 | end
166 |
167 | == Helpers
168 |
169 | Every Rack::API action has several helper methods available through the
170 | Rack::API::Controller class. Here's some of them:
171 |
172 | === logger
173 |
174 | Logs specified message to the STDOUT.
175 |
176 | get "/" do
177 | logger.info "Hello index page!"
178 | {}
179 | end
180 |
181 | === headers
182 |
183 | Define custom headers that will be sent to the client.
184 |
185 | get "/" do
186 | headers["X-Awesome"] = "U R Awesome"
187 | {}
188 | end
189 |
190 | == params
191 |
192 | Return current request parameters.
193 |
194 | get "/" do
195 | {:message => "Hello #{params[:name]}"}
196 | end
197 |
198 | == request
199 |
200 | Return an object relative to the current request.
201 |
202 | == credentials
203 |
204 | This method will return an array container both username and password if client
205 | sent Basic Authentication headers.
206 |
207 | get "/" do
208 | user, pass = credentials
209 | {:message => "Hello #{user}"}
210 | end
211 |
212 | == url_for
213 |
214 | Build an URL by merging segments, default URL options and hash with parameters.
215 |
216 | Rack::API.app do
217 | default_url_options :host => "example.com", :protocol => "https"
218 |
219 | version "v1" do
220 | get "/" do
221 | {:url => url_for(:users, User.find(params[:id])), :format => :json}
222 | end
223 | end
224 | end
225 |
226 | == Useful middlewares
227 |
228 | === Rack::API::Middleware::SSL
229 |
230 | This middleware will accept only HTTPS requests. Any request over HTTP will be dropped.
231 |
232 | Rack::API.app do
233 | use Rack::API::Middleware::SSL
234 | end
235 |
236 | === Rack::API::Middleware::Limit
237 |
238 | This middleware will limit access to API based on requests per hour. It requires a Redis connection.
239 |
240 | Rack::API.app do
241 | # Use the default settings.
242 | # Will accept 60 requests/hour limited by IP address (REMOTE_ADDR)
243 | use Rack::API::Middleware::Limit, :with => Redis.new
244 | end
245 |
246 | Other usages:
247 |
248 | # Set custom limit/hour.
249 | # Will accept ± 1 request/second.
250 | use Rack::API::Middleware::Limit, :with => $redis, :limit => 3600
251 |
252 | # Set custom string key.
253 | # Will limit by something like env["X-Forwarded-For"].
254 | use Rack::API::Middleware::Limit, :with => $redis, :key => "X-Forwarded-For"
255 |
256 | # Set custom block key.
257 | # Will limit by credential (Basic Auth).
258 | Rack::API.app do
259 | basic_auth do |user, pass|
260 | User.authorize(user, pass)
261 | end
262 |
263 | use Rack::API::Middleware::Limit, :with => $redis, :key => proc {|env|
264 | request = Rack::Auth::Basic::Request.new(env)
265 | request.credentials[0]
266 | }
267 | end
268 |
269 | == Maintainer
270 |
271 | * Nando Vieira (http://nandovieira.com.br)
272 |
273 | == License
274 |
275 | (The MIT License)
276 |
277 | Permission is hereby granted, free of charge, to any person obtaining
278 | a copy of this software and associated documentation files (the
279 | 'Software'), to deal in the Software without restriction, including
280 | without limitation the rights to use, copy, modify, merge, publish,
281 | distribute, sublicense, and/or sell copies of the Software, and to
282 | permit persons to whom the Software is furnished to do so, subject to
283 | the following conditions:
284 |
285 | The above copyright notice and this permission notice shall be
286 | included in all copies or substantial portions of the Software.
287 |
288 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
289 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
290 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
291 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
292 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
293 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
294 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
295 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler"
2 | Bundler::GemHelper.install_tasks
3 |
4 | require "rspec/core/rake_task"
5 | RSpec::Core::RakeTask.new
6 |
7 | require "rdoc/task"
8 | Rake::RDocTask.new do |t|
9 | t.rdoc_dir = "docs"
10 | t.main = "README.rdoc"
11 | t.rdoc_files.include "README.rdoc", *Dir["lib/**/*.rb"]
12 | end
13 |
--------------------------------------------------------------------------------
/examples/basic_auth.rb:
--------------------------------------------------------------------------------
1 | $:.push(File.dirname(__FILE__) + "/../lib")
2 |
3 | # Just run `ruby examples/basic_auth.rb` and then use something like
4 | # `curl -u admin:test http://localhost:2345/api/v1/`.
5 |
6 | require "rack/api"
7 |
8 | Rack::API.app do
9 | prefix "api"
10 |
11 | basic_auth do |user, pass|
12 | user == "admin" && pass == "test"
13 | end
14 |
15 | version :v1 do
16 | get "/" do
17 | {:message => "Hello, awesome API!"}
18 | end
19 | end
20 | end
21 |
22 | Rack::Handler::Thin.run Rack::API, :Port => 2345
23 |
--------------------------------------------------------------------------------
/examples/config.ru:
--------------------------------------------------------------------------------
1 | $:.push(File.dirname(__FILE__) + "/../lib")
2 |
3 | # Just run `rackup -p 2345` and then use something like
4 | # `curl -u admin:test http://localhost:2345/`.
5 | #
6 | # You can also use Thin. Just run it with `thin -R config.ru -p 2345 -DV start`.
7 | #
8 |
9 | require "rack/api"
10 |
11 | class Sample < Rack::API
12 | get "/" do
13 | {:message => "Hello World, from Rack!"}
14 | end
15 | end
16 |
17 | run Sample
18 |
--------------------------------------------------------------------------------
/examples/controller.rb:
--------------------------------------------------------------------------------
1 | $:.push(File.dirname(__FILE__) + "/../lib")
2 |
3 | # Just run `ruby examples/controller.rb` and then use something like
4 | # `curl http://localhost:2345/api/v1/`.
5 |
6 | require "rack/api"
7 |
8 | class Hello < Rack::API::Controller
9 | def index
10 | {:message => "Hello, awesome API!"}
11 | end
12 | end
13 |
14 | Rack::API.app do
15 | prefix "api"
16 |
17 | version :v1 do
18 | get "/", :to => "hello#index"
19 | end
20 | end
21 |
22 | Rack::Handler::Thin.run Rack::API, :Port => 2345
23 |
--------------------------------------------------------------------------------
/examples/custom_class.rb:
--------------------------------------------------------------------------------
1 | $:.push(File.dirname(__FILE__) + "/../lib")
2 |
3 | # Just run `ruby examples/custom_class.rb` and then use something like
4 | # `curl http://localhost:2345/api/v1/` and `curl http://localhost:2345/api/v2/`.
5 |
6 | require "rack/api"
7 |
8 | class MyApp < Rack::API
9 | prefix "api"
10 |
11 | version :v1 do
12 | get "/" do
13 | {:message => "Using API v1"}
14 | end
15 | end
16 | end
17 |
18 | class MyApp < Rack::API
19 | prefix "api"
20 |
21 | version :v2 do
22 | get "/" do
23 | {:message => "Using API v2"}
24 | end
25 | end
26 | end
27 |
28 | Rack::Handler::Thin.run MyApp, :Port => 2345
29 |
--------------------------------------------------------------------------------
/examples/custom_format.rb:
--------------------------------------------------------------------------------
1 | $:.push(File.dirname(__FILE__) + "/../lib")
2 |
3 | # Just run `ruby examples/formats.rb` and then use something like
4 | # `curl http://localhost:2345/api/v1/hello.xml`.
5 |
6 | require "rack/api"
7 | require "active_support/all"
8 |
9 | module Rack
10 | class API
11 | module Formatter
12 | class Xml < Base
13 | def to_format
14 | object.to_xml(:root => :messages)
15 | end
16 | end
17 | end
18 | end
19 | end
20 |
21 | Rack::API.app do
22 | prefix "api"
23 | respond_to :xml
24 |
25 | version :v1 do
26 | get "/hello(.:format)" do
27 | {:message => "Hello from Rack API"}
28 | end
29 | end
30 | end
31 |
32 | Rack::Handler::Thin.run Rack::API, :Port => 2345
33 |
--------------------------------------------------------------------------------
/examples/custom_headers.rb:
--------------------------------------------------------------------------------
1 | $:.push(File.dirname(__FILE__) + "/../lib")
2 |
3 | # Just run `ruby examples/custom_headers.rb` and then use something like
4 | # `curl -i http://localhost:2345/api/v1/`.
5 |
6 | require "rack/api"
7 |
8 | Rack::API.app do
9 | prefix "api"
10 |
11 | version :v1 do
12 | get "/" do
13 | headers["X-Awesome"] = "U R Awesome!"
14 | {:message => "Hello, awesome API!"}
15 | end
16 | end
17 | end
18 |
19 | Rack::Handler::Thin.run Rack::API, :Port => 2345
20 |
--------------------------------------------------------------------------------
/examples/formats.rb:
--------------------------------------------------------------------------------
1 | $:.push(File.dirname(__FILE__) + "/../lib")
2 |
3 | # Just run `ruby examples/formats.rb` and then use something like
4 | # `curl http://localhost:2345/api/v1/hello.json` or
5 | # `curl http://localhost:2345/api/v1/hello.jsonp?callback=myJSHandler`.
6 |
7 | require "rack/api"
8 |
9 | Rack::API.app do
10 | prefix "api"
11 |
12 | version :v1 do
13 | get "/hello(.:format)" do
14 | {:message => "Hello from Rack API"}
15 | end
16 | end
17 | end
18 |
19 | Rack::Handler::Thin.run Rack::API, :Port => 2345
20 |
--------------------------------------------------------------------------------
/examples/helpers.rb:
--------------------------------------------------------------------------------
1 | $:.push(File.dirname(__FILE__) + "/../lib")
2 |
3 | # Just run `ruby examples/basic_auth.rb` and then use something like
4 | # `curl http://localhost:2345/api/v1/` or
5 | # `curl http://localhost:2345/api/v1/This%20is%20so%20cool`.
6 |
7 | require "rack/api"
8 |
9 | Rack::API.app do
10 | prefix "api"
11 |
12 | helper do
13 | def default_message
14 | "Hello from Rack API"
15 | end
16 | end
17 |
18 | version :v1 do
19 | get "/(:message)" do
20 | {:message => params.fetch(:message, default_message)}
21 | end
22 | end
23 | end
24 |
25 | Rack::Handler::Thin.run Rack::API, :Port => 2345
26 |
--------------------------------------------------------------------------------
/examples/middleware.rb:
--------------------------------------------------------------------------------
1 | $:.push(File.dirname(__FILE__) + "/../lib")
2 |
3 | # Just run `ruby examples/middleware.rb` and then use something like
4 | # `curl http://localhost:2345/api/v1/`.
5 |
6 | require "rack/api"
7 | require "json"
8 |
9 | class ResponseTime
10 | def initialize(app)
11 | @app = app
12 | end
13 |
14 | def call(env)
15 | start = Time.now
16 | status, headers, response = @app.call(env)
17 | elapsed = Time.now - start
18 | response = JSON.load(response.first).merge(:response_time => elapsed)
19 | [status, headers, [response.to_json]]
20 | end
21 | end
22 |
23 | Rack::API.app do
24 | prefix "api"
25 | use ResponseTime
26 |
27 | version :v1 do
28 | get "/" do
29 | {:message => "Hello, awesome API!"}
30 | end
31 | end
32 | end
33 |
34 | Rack::Handler::Thin.run Rack::API, :Port => 2345
35 |
--------------------------------------------------------------------------------
/examples/multiple_versions.rb:
--------------------------------------------------------------------------------
1 | $:.push(File.dirname(__FILE__) + "/../lib")
2 |
3 | # Just run `ruby examples/multiple_versions.rb` and then use something like
4 | # `curl http://localhost:2345/api/v1/` and `curl http://localhost:2345/api/v2`.
5 |
6 | require "rack/api"
7 |
8 | Rack::API.app do
9 | prefix "api"
10 |
11 | version :v1 do
12 | get "/" do
13 | {:message => "You're using API v1"}
14 | end
15 | end
16 |
17 | version :v2 do
18 | get "/" do
19 | {:message => "You're using API v2"}
20 | end
21 | end
22 | end
23 |
24 | Rack::Handler::Thin.run Rack::API, :Port => 2345
25 |
--------------------------------------------------------------------------------
/examples/params.rb:
--------------------------------------------------------------------------------
1 | $:.push(File.dirname(__FILE__) + "/../lib")
2 |
3 | # Just run `ruby examples/params.rb` and then use something like
4 | # `curl http://localhost:2345/api/v1/hello/John`.
5 |
6 | require "rack/api"
7 |
8 | Rack::API.app do
9 | prefix "api"
10 |
11 | version :v1 do
12 | get "/hello/:name" do
13 | {:message => "Hello, #{params[:name]}"}
14 | end
15 | end
16 | end
17 |
18 | Rack::Handler::Thin.run Rack::API, :Port => 2345
19 |
--------------------------------------------------------------------------------
/examples/rescue_from.rb:
--------------------------------------------------------------------------------
1 | $:.push(File.dirname(__FILE__) + "/../lib")
2 |
3 | # Just run `ruby examples/simple.rb` and then use something like
4 | # `curl http://localhost:2345/api/v1/`.
5 |
6 | require "rack/api"
7 | require "logger"
8 |
9 | $logger = Logger.new(STDOUT)
10 |
11 | # Simulate ActiveRecord's exception.
12 | module ActiveRecord
13 | class RecordNotFound < StandardError
14 | end
15 | end
16 |
17 | Rack::API.app do
18 | prefix "api"
19 |
20 | rescue_from ActiveRecord::RecordNotFound, :status => 404
21 | rescue_from Exception do |error|
22 | $logger.error error.message
23 | [500, {"Content-Type" => "text/plain"}, []]
24 | end
25 |
26 | version :v1 do
27 | get "/" do
28 | raise "Oh no! Something is really wrong!"
29 | end
30 | end
31 | end
32 |
33 | Rack::Handler::Thin.run Rack::API, :Port => 2345
34 |
--------------------------------------------------------------------------------
/examples/simple.rb:
--------------------------------------------------------------------------------
1 | $:.push(File.dirname(__FILE__) + "/../lib")
2 |
3 | # Just run `ruby examples/simple.rb` and then use something like
4 | # `curl http://localhost:2345/api/v1/`.
5 |
6 | require "rack/api"
7 |
8 | Rack::API.app do
9 | prefix "api"
10 |
11 | version :v1 do
12 | get "/" do
13 | {:message => "Hello, awesome API!"}
14 | end
15 | end
16 | end
17 |
18 | Rack::Handler::Thin.run Rack::API, :Port => 2345
19 |
--------------------------------------------------------------------------------
/examples/without_version_and_prefix.rb:
--------------------------------------------------------------------------------
1 | $:.push(File.dirname(__FILE__) + "/../lib")
2 |
3 | # Just run `ruby examples/without_version_and_prefix.rb` and then use something like
4 | # `curl http://localhost:2345/`.
5 |
6 | require "rack/api"
7 |
8 | Rack::API.app do
9 | get "/" do
10 | {:message => "Hello, awesome API!"}
11 | end
12 | end
13 |
14 | Rack::Handler::Thin.run Rack::API, :Port => 2345
15 |
--------------------------------------------------------------------------------
/lib/rack/api.rb:
--------------------------------------------------------------------------------
1 | require "rack"
2 | require "rack/mount"
3 | require "active_support/hash_with_indifferent_access"
4 | require "active_support/core_ext/object/to_query"
5 | require "active_support/core_ext/string/inflections"
6 | require "json"
7 | require "logger"
8 | require "forwardable"
9 |
10 | module Rack
11 | class API
12 | autoload :Controller , "rack/api/controller"
13 | autoload :Formatter , "rack/api/formatter"
14 | autoload :Middleware , "rack/api/middleware"
15 | autoload :Runner , "rack/api/runner"
16 | autoload :Response , "rack/api/response"
17 | autoload :Version , "rack/api/version"
18 |
19 | class << self
20 | extend Forwardable
21 |
22 | def_delegators :runner, *Runner::DELEGATE_METHODS
23 | end
24 |
25 | # A shortcut for defining new APIs. Instead of creating a
26 | # class that inherits from Rack::API, you can simply pass a
27 | # block to the Rack::API.app method.
28 | #
29 | # Rack::API.app do
30 | # # define your API
31 | # end
32 | #
33 | def self.app(&block)
34 | runner.instance_eval(&block)
35 | runner
36 | end
37 |
38 | # Reset all API definitions while using the Rack::API.app method.
39 | #
40 | def self.reset!
41 | @runner = nil
42 | end
43 |
44 | # Required by Rack.
45 | #
46 | def self.call(env) # :nodoc:
47 | runner.call(env)
48 | end
49 |
50 | private
51 | # Initialize a new Rack::API::Middleware instance, so
52 | # we can use it on other class methods.
53 | #
54 | def self.runner # :nodoc:
55 | @runner ||= Runner.new
56 | end
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/lib/rack/api/controller.rb:
--------------------------------------------------------------------------------
1 | module Rack
2 | class API
3 | class Controller
4 | # Registered content types. If you want to use
5 | # a custom formatter that is not listed here,
6 | # you have to manually add it. Otherwise,
7 | # Rack::API::Controller::DEFAULT_MIME_TYPE will be used
8 | # as the content type.
9 | #
10 | MIME_TYPES = {
11 | "json" => "application/json",
12 | "jsonp" => "application/javascript",
13 | "xml" => "application/xml",
14 | "rss" => "application/rss+xml",
15 | "atom" => "application/atom+xml",
16 | "html" => "text/html",
17 | "yaml" => "application/x-yaml",
18 | "txt" => "text/plain"
19 | }
20 |
21 | # Default content type. Will be used when a given format
22 | # hasn't been registered on Rack::API::Controller::MIME_TYPES.
23 | #
24 | DEFAULT_MIME_TYPE = "application/octet-stream"
25 |
26 | # Hold block that will be executed in case the
27 | # route is recognized.
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 format=
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 |
51 | # Hold handlers, that will wrap exceptions
52 | # into a normalized response.
53 | #
54 | attr_accessor :rescuers
55 |
56 | def initialize(options = {})
57 | options.each do |name, value|
58 | instance_variable_set("@#{name}", value)
59 | end
60 |
61 | @url_options ||= {}
62 | end
63 |
64 | # Always log to the standard output.
65 | #
66 | def logger
67 | @logger ||= Logger.new(STDOUT)
68 | end
69 |
70 | # Hold headers that will be sent on the response.
71 | #
72 | def headers
73 | @headers ||= {}
74 | end
75 |
76 | # Merge all params into one single hash.
77 | #
78 | def params
79 | @params ||= HashWithIndifferentAccess.new(request.params.merge(env["rack.routing_args"]))
80 | end
81 |
82 | # Return a request object.
83 | #
84 | def request
85 | @request ||= Rack::Request.new(env)
86 | end
87 |
88 | # Return the requested format. Defaults to JSON.
89 | #
90 | def format
91 | params.fetch(:format, default_format)
92 | end
93 |
94 | # Stop processing by rendering the provided information.
95 | #
96 | # Rack::API.app do
97 | # version :v1 do
98 | # get "/" do
99 | # error(:status => 403, :message => "Not here!")
100 | # end
101 | # end
102 | # end
103 | #
104 | # Valid options are:
105 | #
106 | # * :status: a HTTP status code. Defaults to 403.
107 | # * :message: a message that will be rendered as the response body. Defaults to "Forbidden".
108 | # * :headers: the response headers. Defaults to {"Content-Type" => "text/plain"}.
109 | #
110 | # You can also provide a object that responds to to_rack. In this case, this
111 | # method must return a valid Rack response (a 3-item array).
112 | #
113 | # class MyError
114 | # def self.to_rack
115 | # [500, {"Content-Type" => "text/plain"}, ["Internal Server Error"]]
116 | # end
117 | # end
118 | #
119 | # Rack::API.app do
120 | # version :v1 do
121 | # get "/" do
122 | # error(MyError)
123 | # end
124 | # end
125 | # end
126 | #
127 | def error(options = {})
128 | throw :error, Response.new(options)
129 | end
130 |
131 | # Set response status code.
132 | #
133 | def status(*args)
134 | @status = args.first unless args.empty?
135 | @status || 200
136 | end
137 |
138 | # Reset environment between requests.
139 | #
140 | def reset! # :nodoc:
141 | @params = nil
142 | @request = nil
143 | @headers = nil
144 | end
145 |
146 | # Return credentials for Basic Authentication request.
147 | #
148 | def credentials
149 | @credentials ||= begin
150 | request = Rack::Auth::Basic::Request.new(env)
151 | request.provided? ? request.credentials : []
152 | end
153 | end
154 |
155 | # Render the result of handler.
156 | #
157 | def call(env) # :nodoc:
158 | reset!
159 | @env = env
160 |
161 | response = catch(:error) do
162 | render instance_eval(&handler)
163 | end
164 |
165 | response.respond_to?(:to_rack) ? response.to_rack : response
166 | rescue Exception => exception
167 | handle_exception exception
168 | end
169 |
170 | # Return response content type based on extension.
171 | # If you're using an unknown extension that wasn't registered on
172 | # Rack::API::Controller::MIME_TYPES, it will return Rack::API::Controller::DEFAULT_MIME_TYPE,
173 | # which defaults to application/octet-stream.
174 | #
175 | def content_type
176 | mime = MIME_TYPES.fetch(format, DEFAULT_MIME_TYPE)
177 | headers.fetch("Content-Type", mime)
178 | end
179 |
180 | # Return a URL path for all segments.
181 | # You can set default options by using the
182 | # Rack::API::Runner#default_url_options method.
183 | #
184 | # url_for :users
185 | # #=> /users
186 | #
187 | # url_for :users, User.first
188 | # #=> /users/1
189 | #
190 | # url_for :users, 1, :format => :json
191 | # #=> /users/1?format=json
192 | #
193 | # url_for :users, :filters => [:name, :age]
194 | # #=> /users?filters[]=name&filters[]=age
195 | #
196 | # URL segments can be any kind of object. First it'll be checked if it responds to
197 | # the to_param method. If not, converts object to string by using the
198 | # to_s method.
199 | #
200 | def url_for(*args)
201 | options = {}
202 | options = args.pop if args.last.kind_of?(Hash)
203 |
204 | segments = []
205 | segments << url_options[:base_path] if url_options[:base_path]
206 | segments << prefix if prefix
207 | segments << version
208 | segments += args.collect {|part| part.respond_to?(:to_param) ? part.to_param : part.to_s }
209 |
210 | url = ""
211 | url << url_options.fetch(:protocol, "http").to_s << "://"
212 | url << url_options.fetch(:host, env["SERVER_NAME"])
213 |
214 | port = url_options.fetch(:port, env["SERVER_PORT"]).to_i
215 | url << ":" << port.to_s if port.nonzero? && port != 80
216 |
217 | url << Rack::Mount::Utils.normalize_path(segments.join("/"))
218 | url << "?" << options.to_param if options.any?
219 | url
220 | end
221 |
222 | private
223 | def render(response) # :nodoc:
224 | [status, headers.merge("Content-Type" => content_type), [format_response(response)]]
225 | end
226 |
227 | def format_response(response) # :nodoc:
228 | formatter_name = format.split("_").collect {|word| word[0,1].upcase + word[1,word.size].downcase}.join("")
229 |
230 | if Rack::API::Formatter.const_defined?(formatter_name)
231 | formatter = Rack::API::Formatter.const_get(formatter_name).new(response, env, params)
232 | formatter.to_format
233 | elsif response.respond_to?("to_#{format}")
234 | response.__send__("to_#{format}")
235 | else
236 | throw :error, Response.new(:status => 406, :message => "Unknown format")
237 | end
238 | end
239 |
240 | def handle_exception(error) # :nodoc:
241 | rescuer = rescuers.find do |r|
242 | error_class = eval("::#{r[:class_name]}") rescue nil
243 | error_class && error.kind_of?(error_class)
244 | end
245 |
246 | raise error unless rescuer
247 |
248 | if rescuer[:block]
249 | instance_exec(error, &rescuer[:block])
250 | else
251 | [rescuer[:options].fetch(:status, 500), {"Content-Type" => "text/plain"}, []]
252 | end
253 | end
254 | end
255 | end
256 | end
257 |
--------------------------------------------------------------------------------
/lib/rack/api/formatter.rb:
--------------------------------------------------------------------------------
1 | module Rack
2 | class API
3 | module Formatter
4 | autoload :Base, "rack/api/formatter/base"
5 | autoload :Jsonp, "rack/api/formatter/jsonp"
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/rack/api/formatter/base.rb:
--------------------------------------------------------------------------------
1 | module Rack
2 | class API
3 | module Formatter
4 | class Base
5 | attr_accessor :object
6 | attr_accessor :params
7 | attr_accessor :env
8 |
9 | class AbstractMethodError < StandardError; end
10 |
11 | def initialize(object, env, params)
12 | @object, @env, @params = object, env, params
13 | end
14 |
15 | def to_format
16 | raise AbstractMethodError
17 | end
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/rack/api/formatter/jsonp.rb:
--------------------------------------------------------------------------------
1 | module Rack
2 | class API
3 | module Formatter
4 | class Jsonp < Base
5 | def to_format
6 | params.fetch(:callback, "callback") + "(#{object.to_json});"
7 | end
8 | end
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/rack/api/middleware.rb:
--------------------------------------------------------------------------------
1 | module Rack
2 | class API
3 | module Middleware
4 | autoload :Format, "rack/api/middleware/format"
5 | autoload :SSL, "rack/api/middleware/ssl"
6 | autoload :Limit, "rack/api/middleware/limit"
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/rack/api/middleware/format.rb:
--------------------------------------------------------------------------------
1 | module Rack
2 | class API
3 | module Middleware
4 | class Format
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}
9 | end
10 |
11 | def call(env)
12 | request = Rack::Request.new(env)
13 | params = request.env["rack.routing_args"].merge(request.params)
14 | requested_format = params.fetch(:format, @default_format)
15 |
16 | if @formats.include?(requested_format)
17 | @app.call(env)
18 | else
19 | [406, {"Content-Type" => "text/plain"}, ["Invalid format. Accepts one of [#{@formats.join(", ")}]"]]
20 | end
21 | end
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/rack/api/middleware/limit.rb:
--------------------------------------------------------------------------------
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? # :nodoc:
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 # :nodoc:
37 | options[:with]
38 | end
39 |
40 | def identifier # :nodoc:
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 # :nodoc:
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
55 |
--------------------------------------------------------------------------------
/lib/rack/api/middleware/ssl.rb:
--------------------------------------------------------------------------------
1 | module Rack
2 | class API
3 | module Middleware
4 | class SSL
5 | def initialize(app)
6 | @app = app
7 | end
8 |
9 | def call(env)
10 | request = Rack::Request.new(env)
11 |
12 | if env["rack.url_scheme"] == "https"
13 | @app.call(env)
14 | else
15 | [400, {"Content-Type" => "text/plain"}, ["Only HTTPS requests are supported by now."]]
16 | end
17 | end
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/rack/api/response.rb:
--------------------------------------------------------------------------------
1 | module Rack
2 | class API
3 | class Response
4 | attr_reader :options
5 |
6 | def initialize(options)
7 | @options = options
8 | end
9 |
10 | def to_rack
11 | return options.to_rack if options.respond_to?(:to_rack)
12 |
13 | [
14 | options.fetch(:status, 403),
15 | {"Content-Type" => "text/plain"}.merge(options.fetch(:headers, {})),
16 | [options.fetch(:message, "Forbidden")]
17 | ]
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/rack/api/runner.rb:
--------------------------------------------------------------------------------
1 | module Rack
2 | class API
3 | class Runner
4 | HTTP_METHODS = %w[get post put delete head patch options]
5 |
6 | DELEGATE_METHODS = %w[
7 | version use prefix basic_auth rescue_from
8 | helper respond_to default_url_options
9 | get post put delete head patch options
10 | ]
11 |
12 | attr_accessor :settings
13 |
14 | def initialize
15 | @settings = {
16 | :middlewares => [],
17 | :helpers => [],
18 | :rescuers => [],
19 | :global => {
20 | :prefix => "/",
21 | :formats => %w[json jsonp],
22 | :middlewares => [],
23 | :helpers => [],
24 | :rescuers => []
25 | }
26 | }
27 | end
28 |
29 | # Set configuration based on scope. When defining values outside version block,
30 | # will set configuration using settings[:global] namespace.
31 | #
32 | # Use the Rack::API::Runner#option method to access a given setting.
33 | #
34 | def set(name, value, mode = :override)
35 | target = settings[:version] ? settings : settings[:global]
36 |
37 | if mode == :override
38 | target[name] = value
39 | else
40 | target[name] << value
41 | end
42 | end
43 |
44 | # Try to fetch local configuration, defaulting to the global setting.
45 | # Return +nil+ when no configuration is defined.
46 | #
47 | def option(name, mode = :any)
48 | if mode == :merge && (settings[name].kind_of?(Array) || settings[:global][name].kind_of?(Array))
49 | settings[:global].fetch(name, []) | settings.fetch(name, [])
50 | else
51 | settings.fetch(name, settings[:global][name])
52 | end
53 | end
54 |
55 | # Add a middleware to the execution stack.
56 | #
57 | # Global middlewares will be merged with local middlewares.
58 | #
59 | # Rack::API.app do
60 | # use ResponseTime
61 | #
62 | # version :v1 do
63 | # use Gzip
64 | # end
65 | # end
66 | #
67 | # The middleware stack will be something like [ResponseTime, Gzip].
68 | #
69 | def use(middleware, *args)
70 | set :middlewares, [middleware, *args], :append
71 | end
72 |
73 | # Set an additional url prefix.
74 | #
75 | def prefix(name)
76 | set :prefix, name
77 | end
78 |
79 | # Add a helper to application.
80 | #
81 | # helper MyHelpers
82 | # helper { }
83 | #
84 | def helper(mod = nil, &block)
85 | mod = Module.new(&block) if block_given?
86 | raise ArgumentError, "you need to pass a module or block" unless mod
87 | set :helpers, mod, :append
88 | end
89 |
90 | # Define the server endpoint. Will be used if you call the method
91 | # Rack::API::Controller#url_for.
92 | #
93 | # The following options are supported:
94 | #
95 | # * :host – Specifies the host the link should be targeted at.
96 | # * :protocol – The protocol to connect to. Defaults to 'http'.
97 | # * :port – Optionally specify the port to connect to.
98 | # * :base_path – Optionally specify a base path.
99 | #
100 | # Some usage examples:
101 | #
102 | # default_url_options :host => "myhost.com"
103 | # #=> http://myhost.com
104 | #
105 | # default_url_options :host => "myhost.com", :protocol => "https"
106 | # #=> https://myhost.com
107 | #
108 | # default_url_options :host => "myhost.com", :port => 3000
109 | # #=> http://myhost.com:3000
110 | #
111 | # default_url_options :host => "myhost.com", :base_path => "my/custom/path"
112 | # #=> http://myhost.com/my/custom/path
113 | #
114 | def default_url_options(options)
115 | set :url_options, options
116 | end
117 |
118 | # Create a new API version.
119 | #
120 | def version(name, &block)
121 | raise ArgumentError, "you need to pass a block" unless block_given?
122 | settings[:version] = name.to_s
123 | instance_eval(&block)
124 | settings.delete(:version)
125 | end
126 |
127 | # Run all routes.
128 | #
129 | def call(env) # :nodoc:
130 | route_set.freeze.call(env)
131 | end
132 |
133 | # Require basic authentication before processing other requests.
134 | # The authentication reques must be defined before routes.
135 | #
136 | # Rack::API.app do
137 | # basic_auth "Protected Area" do |user, pass|
138 | # User.authenticate(user, pass)
139 | # end
140 | # end
141 | #
142 | # You can disable basic authentication by providing :none as
143 | # realm.
144 | #
145 | # Rack::API.app do
146 | # basic_auth "Protected Area" do |user, pass|
147 | # User.authenticate(user, pass)
148 | # end
149 | #
150 | # version :v1 do
151 | # # this version is protected by the
152 | # # global basic auth block above.
153 | # end
154 | #
155 | # version :v2 do
156 | # basic_auth :none
157 | # # this version is now public
158 | # end
159 | #
160 | # version :v3 do
161 | # basic_auth "Admin" do |user, pass|
162 | # user == "admin" && pass == "test"
163 | # end
164 | # end
165 | # end
166 | #
167 | def basic_auth(realm = "Restricted Area", &block)
168 | set :auth, (realm == :none ? :none : [realm, block])
169 | end
170 |
171 | # Define the formats that this app implements.
172 | # Respond only to :json by default.
173 | #
174 | # When setting a format you have some alternatives on how this object
175 | # will be formated.
176 | #
177 | # First, Rack::API will look for a formatter defined on Rack::API::Formatter
178 | # namespace. If there's no formatter, it will look for a method to_.
179 | # It will raise an exception if no formatter method has been defined.
180 | #
181 | # See Rack::API::Formatter::Jsonp for an example on how to create additional
182 | # formatters.
183 | #
184 | # Local formats will override the global configuration on that context.
185 | #
186 | # Rack::API.app do
187 | # respond_to :json, :xml, :jsonp
188 | #
189 | # version :v1 do
190 | # respond_to :json
191 | # end
192 | # end
193 | #
194 | # The code above will accept only :json as format on version :v1.
195 | #
196 | # Also, the first value provided to this method will be used as default format,
197 | # which means that requests that don't provide the :format param, will use
198 | # this value.
199 | #
200 | # respond_to :fffuuu, :json
201 | # #=> the default format is fffuuu
202 | #
203 | def respond_to(*formats)
204 | set :formats, formats
205 | end
206 |
207 | # Hold all routes.
208 | #
209 | def route_set # :nodoc:
210 | @route_set ||= Rack::Mount::RouteSet.new
211 | end
212 |
213 | # Define a new routing that will be triggered when both request method and
214 | # path are recognized.
215 | #
216 | # You're better off using all verb shortcut methods. Implemented verbs are
217 | # +get+, +post+, +put+, +delete+, +head+ and +patch+.
218 | #
219 | # class MyAPI < Rack::API
220 | # version "v1" do
221 | # get "users(.:format)" do
222 | # # do something
223 | # end
224 | # end
225 | # end
226 | #
227 | # You don't have to use +version+ or +prefix+.
228 | #
229 | # class MyAPI < Rack::API
230 | # get "users(.:format)" do
231 | # # do something
232 | # end
233 | # end
234 | #
235 | # Alternatively, you can define your routes pretty much like Rails.
236 | #
237 | # class MyAPI < Rack::API
238 | # get "users(.:format)", :to => "users#index"
239 | # end
240 | #
241 | # The route above will require a class +Users+ with an instance method +index+.
242 | #
243 | # class Users < Rack::API::Controller
244 | # def index
245 | # # do something
246 | # end
247 | # end
248 | #
249 | # Note that your controller must inherit from Rack::API::Controller. Otherwise,
250 | # your world will explode.
251 | #
252 | def route(method, path, requirements = {}, &block)
253 | separator = requirements.delete(:separator) { %w[ / . ? ] }
254 |
255 | path = Rack::Mount::Strexp.compile mount_path(path), requirements, separator
256 | controller_class = Controller
257 |
258 | if requirements[:to]
259 | controller_name, action_name = requirements.delete(:to).split("#")
260 | controller_class = controller_name.camelize.constantize
261 | block = proc { __send__(action_name) }
262 | end
263 |
264 | route_set.add_route(build_app(controller_class, block), :path_info => path, :request_method => method)
265 | end
266 |
267 | HTTP_METHODS.each do |method|
268 | class_eval <<-RUBY, __FILE__, __LINE__
269 | def #{method}(*args, &block) # def get(*args, &block)
270 | route("#{method.upcase}", *args, &block) # route("GET", *args, &block)
271 | end # end
272 | RUBY
273 | end
274 |
275 | # Rescue from the specified exception.
276 | #
277 | # rescue_from ActiveRecord::RecordNotFound, :status => 404
278 | # rescue_from Exception, :status => 500
279 | # rescue_from Exception do |error|
280 | # $logger.error error.inspect
281 | # [500, {"Content-Type" => "text/plain"}, []]
282 | # end
283 | #
284 | def rescue_from(exception, options = {}, &block)
285 | set :rescuers, {:class_name => exception.name, :options => options, :block => block}, :append
286 | end
287 |
288 | private
289 | def mount_path(path) # :nodoc:
290 | Rack::Mount::Utils.normalize_path([option(:prefix), settings[:version], path].join("/"))
291 | end
292 |
293 | def default_format # :nodoc:
294 | (option(:formats).first || "json").to_s
295 | end
296 |
297 | def build_app(controller, handler) # :nodoc:
298 | app = controller.new({
299 | :handler => handler,
300 | :default_format => default_format,
301 | :version => option(:version),
302 | :prefix => option(:prefix),
303 | :url_options => option(:url_options),
304 | :rescuers => option(:rescuers, :merge)
305 | })
306 |
307 | builder = Rack::Builder.new
308 |
309 | # Add middleware for basic authentication.
310 | auth = option(:auth)
311 | builder.use Rack::Auth::Basic, auth[0], &auth[1] if auth && auth != :none
312 |
313 | # Add middleware for format validation.
314 | builder.use Rack::API::Middleware::Format, default_format, option(:formats)
315 |
316 | # Add middlewares to execution stack.
317 | option(:middlewares, :merge).each {|middleware| builder.use(*middleware)}
318 |
319 | # Apply helpers to app.
320 | helpers = option(:helpers, :merge)
321 | app.extend *helpers unless helpers.empty?
322 |
323 | builder.run(app)
324 | builder.to_app
325 | end
326 | end
327 | end
328 | end
329 |
--------------------------------------------------------------------------------
/lib/rack/api/version.rb:
--------------------------------------------------------------------------------
1 | module Rack
2 | class API
3 | module Version
4 | MAJOR = 1
5 | MINOR = 1
6 | PATCH = 0
7 | STRING = "#{MAJOR}.#{MINOR}.#{PATCH}"
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/rack-api.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | $:.push File.expand_path("../lib", __FILE__)
3 | require "rack/api/version"
4 |
5 | Gem::Specification.new do |s|
6 | s.name = "rack-api"
7 | s.version = Rack::API::Version::STRING
8 | s.platform = Gem::Platform::RUBY
9 | s.authors = ["Nando Vieira"]
10 | s.email = ["fnando.vieira@gmail.com"]
11 | s.homepage = "http://rubygems.org/gems/rack-api"
12 | s.summary = "Create web app APIs that respond to one or more formats using an elegant DSL."
13 | s.description = s.summary
14 |
15 | s.files = `git ls-files`.split("\n")
16 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18 | s.require_paths = ["lib"]
19 |
20 | s.add_dependency "rack", ">= 1.0.0"
21 | s.add_dependency "rack-mount", ">= 0.6.0"
22 | s.add_dependency "activesupport", ">= 3.0"
23 | s.add_dependency "i18n"
24 | s.add_development_dependency "rspec", "~> 2.6"
25 | s.add_development_dependency "rack-test", "~> 0.5.7"
26 | s.add_development_dependency "redis", "~> 2.2.0"
27 | s.add_development_dependency "rake", "~> 0.9"
28 | s.add_development_dependency "pry"
29 | s.add_development_dependency "awesome_print"
30 | end
31 |
--------------------------------------------------------------------------------
/spec/rack-api/api_throttling_spec.rb:
--------------------------------------------------------------------------------
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
90 |
--------------------------------------------------------------------------------
/spec/rack-api/basic_auth_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe Rack::API, "Basic Authentication" do
4 | before do
5 | Rack::API.app do
6 | basic_auth do |user, pass|
7 | user == "admin" && pass == "test"
8 | end
9 |
10 | version :v1 do
11 | get("/") { {:success => true} }
12 | end
13 |
14 | version :v2 do
15 | basic_auth :none
16 | get("/") { {:success => true} }
17 | get("/credentials") { credentials }
18 | end
19 |
20 | version :v3 do
21 | basic_auth do |user, pass|
22 | user == "john" && pass == "test"
23 | end
24 |
25 | get("/") { {:success => true} }
26 | end
27 | end
28 | end
29 |
30 | context "global authorization" do
31 | it "denies access" do
32 | get "/v1/"
33 | last_response.status.should == 401
34 |
35 | get "/v1/", {}, "HTTP_AUTHORIZATION" => basic_auth("admin", "invalid")
36 | last_response.status.should == 401
37 |
38 | get "/v1/", {}, "HTTP_AUTHORIZATION" => basic_auth("john", "test")
39 | last_response.status.should == 401
40 | end
41 |
42 | it "grants access" do
43 | get "/v1/", {}, "HTTP_AUTHORIZATION" => basic_auth("admin", "test")
44 |
45 | last_response.status.should == 200
46 | last_response.body.should == {"success" => true}.to_json
47 | end
48 | end
49 |
50 | context "no authorization" do
51 | it "grants access" do
52 | get "/v2/"
53 |
54 | last_response.status.should == 200
55 | last_response.body.should == {"success" => true}.to_json
56 | end
57 | end
58 |
59 | context "local authorization" do
60 | it "denies access" do
61 | get "/v3/"
62 | last_response.status.should == 401
63 |
64 | get "/v3/", {}, "HTTP_AUTHORIZATION" => basic_auth("admin", "test")
65 | last_response.status.should == 401
66 | end
67 |
68 | it "grants access" do
69 | get "/v3/", {}, "HTTP_AUTHORIZATION" => basic_auth("john", "test")
70 |
71 | last_response.status.should == 200
72 | last_response.body.should == {"success" => true}.to_json
73 | end
74 | end
75 |
76 | it "returns credentials" do
77 | get "/v2/credentials", {}, "HTTP_AUTHORIZATION" => basic_auth("admin", "test")
78 | last_response.body.should == ["admin", "test"].to_json
79 | end
80 |
81 | it "returns empty array when no credentials are provided" do
82 | get "/v2/credentials"
83 | last_response.body.should == [].to_json
84 | end
85 | end
86 |
--------------------------------------------------------------------------------
/spec/rack-api/controller_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe Rack::API do
4 | before do
5 | Rack::API.app do
6 | get("/", :to => "my_controller#index")
7 | end
8 | end
9 |
10 | it "renders action from MyApp" do
11 | get "/", :name => "John"
12 | last_response.body.should == {"name" => "John"}.to_json
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/spec/rack-api/format_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe Rack::API, "Format" do
4 | before do
5 | Rack::API.app do
6 | version :v1 do
7 | respond_to :json, :jsonp, :awesome, :fffuuu, :zomg
8 | get("/") { {:success => true} }
9 | get("users(.:format)") { {:users => []} }
10 | end
11 | end
12 | end
13 |
14 | it "ignores unknown paths/formats" do
15 | get "/users.xml"
16 | last_response.status.should == 404
17 | end
18 |
19 | context "missing formatter" do
20 | it "renders 406" do
21 | get "/v1/users.zomg"
22 |
23 | last_response.status.should == 406
24 | last_response.body.should == "Unknown format"
25 | last_response.headers["Content-Type"].should == "text/plain"
26 | end
27 | end
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::Controller::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 |
56 | context "invalid format" do
57 | it "renders 406" do
58 | get "/v1/users.invalid"
59 |
60 | last_response.status.should == 406
61 | last_response.body.should == "Invalid format. Accepts one of [json, jsonp, awesome, fffuuu, zomg]"
62 | last_response.headers["Content-Type"].should == "text/plain"
63 | end
64 | end
65 |
66 | context "JSONP" do
67 | it "renders when set through query string" do
68 | get "/v1", :format => "jsonp"
69 |
70 | last_response.status.should == 200
71 | last_response.body.should == %[callback({"success":true});]
72 | end
73 |
74 | it "renders when set through extension" do
75 | get "/v1/users.jsonp"
76 |
77 | last_response.status.should == 200
78 | last_response.body.should == %[callback({"users":[]});]
79 | end
80 |
81 | it "sends header" do
82 | get "/v1/users.jsonp"
83 | last_response.headers["Content-Type"].should == "application/javascript"
84 | end
85 | end
86 |
87 | context "JSON" do
88 | it "renders when set through query string" do
89 | get "/v1", :format => "json"
90 |
91 | last_response.status.should == 200
92 | last_response.body.should == {"success" => true}.to_json
93 | end
94 |
95 | it "renders when set through extension" do
96 | get "/v1/users.json"
97 |
98 | last_response.status.should == 200
99 | last_response.body.should == {"users" => []}.to_json
100 | end
101 |
102 | it "sends header" do
103 | get "/v1/users.json"
104 | last_response.headers["Content-Type"].should == "application/json"
105 | end
106 | end
107 |
108 | context "custom formatter extension" do
109 | it "renders when set through query string" do
110 | get "/v1", :format => "awesome"
111 |
112 | last_response.status.should == 200
113 | last_response.body.should == "U R Awesome"
114 | end
115 |
116 | it "renders when set through extension" do
117 | get "/v1/users.awesome"
118 |
119 | last_response.status.should == 200
120 | last_response.body.should == "U R Awesome"
121 | end
122 | end
123 |
124 | context "custom formatter class" do
125 | before :all do
126 | Rack::API::Formatter::Fffuuu = Class.new(Rack::API::Formatter::Base) do
127 | def to_format
128 | "ZOMG! Fffuuu!"
129 | end
130 | end
131 | end
132 |
133 | it "renders when set through query string" do
134 | get "/v1", :format => "fffuuu"
135 |
136 | last_response.status.should == 200
137 | last_response.body.should == "ZOMG! Fffuuu!"
138 | end
139 |
140 | it "renders when set through extension" do
141 | get "/v1/users.fffuuu"
142 |
143 | last_response.status.should == 200
144 | last_response.body.should == "ZOMG! Fffuuu!"
145 | end
146 | end
147 |
148 | context "formatter helper methods" do
149 | it "sets params" do
150 | Rack::API.app do
151 | version :v2 do
152 | respond_to :json
153 | get("/") { params }
154 | end
155 | end
156 |
157 | get "/v2", :foo => "bar"
158 | last_response.body.should == {foo: "bar"}.to_json
159 | end
160 |
161 | it "sets env" do
162 | Rack::API.app do
163 | version :v2 do
164 | respond_to :json
165 | get("/") { env }
166 | end
167 | end
168 |
169 | get "/v2", :foo => "bar"
170 | JSON.load(last_response.body)["REQUEST_METHOD"].should == "GET"
171 | end
172 | end
173 | end
174 |
--------------------------------------------------------------------------------
/spec/rack-api/headers_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe Rack::API, "Headers" do
4 | before do
5 | Rack::API.app do
6 | version :v1 do
7 | get("/users(.:format)") do
8 | headers["X-Awesome"] = "U R Awesome"
9 | headers["Content-Type"] = "application/x-json" # the default json header is application/json
10 | end
11 | end
12 | end
13 | end
14 |
15 | it "sends custom headers" do
16 | get "/v1/users"
17 | last_response.headers["X-Awesome"].should == "U R Awesome"
18 | end
19 |
20 | it "overrides inferred content type" do
21 | get "/v1/users.json"
22 | last_response.headers["Content-Type"].should == "application/x-json"
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/spec/rack-api/helpers_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe Rack::API, "Helpers" do
4 | before do
5 | Rack::API.app do
6 | version :v1 do
7 | helper Module.new {
8 | def helper_from_module
9 | "module"
10 | end
11 | }
12 |
13 | helper do
14 | def helper_from_block
15 | "block"
16 | end
17 | end
18 |
19 | get("/") do
20 | [helper_from_block, helper_from_module]
21 | end
22 | end
23 | end
24 | end
25 |
26 | it "adds module helper" do
27 | get "/v1"
28 | json(last_response.body).should include("module")
29 | end
30 |
31 | it "adds block helper" do
32 | get "/v1"
33 | json(last_response.body).should include("block")
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/spec/rack-api/http_methods_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe Rack::API, "HTTP Methods" do
4 | before do
5 | Rack::API.app do
6 | version :v1 do
7 | get("get") { {:get => true} }
8 | post("post") { {:post => true} }
9 | put("put") { {:put => true} }
10 | delete("delete") { {:delete => true} }
11 | head("head") { {:head => true} }
12 | patch("patch") { {:patch => true} }
13 | options("options") { {:options => true} }
14 | end
15 | end
16 | end
17 |
18 | Rack::API::Runner::HTTP_METHODS.each do |method|
19 | it "renders #{method}" do
20 | send method, "/v1/#{method}"
21 | last_response.status.should == 200
22 | last_response.body.should == {method => true}.to_json
23 | end
24 | end
25 |
26 | it "does not render unknown methods" do
27 | post "/get"
28 | last_response.status.should == 404
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/spec/rack-api/inheritance_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe Rack::API do
4 | before do
5 | @app = MyApp
6 | end
7 |
8 | it "renders action from MyApp" do
9 | get "/v1"
10 | last_response.body.should == {"myapp" => true}.to_json
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/spec/rack-api/method_delegation_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe Rack::API, "delegators" do
4 | subject { Rack::API }
5 |
6 | specify "sanity check for delegate methods" do
7 | Rack::API::Runner::DELEGATE_METHODS.size.should == 15
8 | end
9 |
10 | it { should respond_to(:version) }
11 | it { should respond_to(:use) }
12 | it { should respond_to(:prefix) }
13 | it { should respond_to(:basic_auth) }
14 | it { should respond_to(:helper) }
15 | it { should respond_to(:default_url_options) }
16 | it { should respond_to(:rescue_from) }
17 | it { should respond_to(:get) }
18 | it { should respond_to(:post) }
19 | it { should respond_to(:put) }
20 | it { should respond_to(:delete) }
21 | it { should respond_to(:head) }
22 | it { should respond_to(:patch) }
23 | it { should respond_to(:options) }
24 | end
25 |
--------------------------------------------------------------------------------
/spec/rack-api/middlewares_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe Rack::API, "Middlewares" do
4 | before do
5 | Rack::API.app do
6 | use ZOMGMiddleware
7 |
8 | version :v1 do
9 | use AwesomeMiddleware
10 | get("/") {}
11 | end
12 | end
13 | end
14 |
15 | it "sends custom headers" do
16 | get "/v1"
17 | last_response.headers["X-Awesome"].should == "U R Awesome"
18 | last_response.headers["X-ZOMG"].should == "ZOMG!"
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/spec/rack-api/params_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe Rack::API, "Params" do
4 | before do
5 | Rack::API.app do
6 | version :v1 do
7 | get("users/:id(.:format)") { params }
8 | post("users") { params }
9 | end
10 | end
11 | end
12 |
13 | it "detects optional names from routing params" do
14 | get "/v1/users/1.json"
15 | json(last_response.body).should == {"id" => "1", "format" => "json"}
16 | end
17 |
18 | it "detects query string params" do
19 | get "/v1/users/1?include=articles"
20 | json(last_response.body).should == {"id" => "1", "include" => "articles"}
21 | end
22 |
23 | it "detects post params" do
24 | post "/v1/users", :name => "John Doe"
25 | last_response.body.should == {"name" => "John Doe"}.to_json
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/spec/rack-api/paths_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe Rack::API, "Paths" do
4 | before do
5 | Rack::API.app do
6 | version :v1 do
7 | prefix "api"
8 | get("users") { {:users => []} }
9 | end
10 |
11 | version :v2 do
12 | prefix "/"
13 | get("users") { {:users => []} }
14 | end
15 | end
16 | end
17 |
18 | it "does not render root" do
19 | get "/"
20 | last_response.status.should == 404
21 | end
22 |
23 | it "does not render unknown paths" do
24 | get "/api/v1/users/index"
25 | last_response.status.should == 404
26 | end
27 |
28 | it "renders known paths" do
29 | get "/api/v1/users"
30 | last_response.status.should == 200
31 |
32 | get "/v2/users"
33 | last_response.status.should == 200
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/spec/rack-api/rescue_from_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe Rack::API, "Rescue from exceptions" do
4 | class NotFound < StandardError; end
5 |
6 | it "rescues from NotFound exception" do
7 | Rack::API.app do
8 | rescue_from NotFound, :status => 404
9 |
10 | version :v1 do
11 | get("/404") { raise NotFound }
12 | end
13 | end
14 |
15 | get "/v1/404"
16 | last_response.headers["Content-Type"].should == "text/plain"
17 | last_response.body.should == ""
18 | last_response.status.should == 404
19 | end
20 |
21 | it "rescues from all exceptions" do
22 | Rack::API.app do
23 | rescue_from Exception
24 |
25 | version :v1 do
26 | get("/500") { raise "Oops!" }
27 | end
28 | end
29 |
30 | get "/v1/500"
31 | last_response.headers["Content-Type"].should == "text/plain"
32 | last_response.body.should == ""
33 | last_response.status.should == 500
34 | end
35 |
36 | it "rescues from exception by using a block" do
37 | Rack::API.app do
38 | rescue_from Exception do
39 | [501, {"Content-Type" => "application/json"}, [{:error => true}.to_json]]
40 | end
41 |
42 | version :v1 do
43 | get("/501") { raise "Oops!" }
44 | end
45 | end
46 |
47 | get "/v1/501"
48 | last_response.headers["Content-Type"].should == "application/json"
49 | last_response.body.should == {:error => true}.to_json
50 | last_response.status.should == 501
51 | end
52 |
53 | it "rescues from exception in app's context" do
54 | Rack::API.app do
55 | version :v1 do
56 | rescue_from Exception do
57 | [500, {"Content-Type" => "text/plain"}, [self.class.name]]
58 | end
59 |
60 | get("/500") { raise "Oops!" }
61 | end
62 | end
63 |
64 | get "/v1/500"
65 | last_response.body.should == "Rack::API::Controller"
66 | end
67 |
68 | it "yields the exception object" do
69 | Rack::API.app do
70 | version :v1 do
71 | rescue_from Exception do |error|
72 | [500, {"Content-Type" => "text/plain"}, [error.message]]
73 | end
74 |
75 | get("/500") { raise "Oops!" }
76 | end
77 | end
78 |
79 | get "/v1/500"
80 | last_response.body.should == "Oops!"
81 | end
82 | end
83 |
--------------------------------------------------------------------------------
/spec/rack-api/runner_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe Rack::API::Runner do
4 | it "responds to http methods" do
5 | subject.should respond_to(:get)
6 | subject.should respond_to(:post)
7 | subject.should respond_to(:put)
8 | subject.should respond_to(:delete)
9 | subject.should respond_to(:head)
10 | subject.should respond_to(:options)
11 | subject.should respond_to(:patch)
12 | end
13 |
14 | it "sets available formats" do
15 | subject.respond_to(:json, :jsonp, :atom)
16 | subject.option(:formats).should == [:json, :jsonp, :atom]
17 | end
18 |
19 | it "sets prefix option" do
20 | subject.prefix("my/awesome/api")
21 | subject.option(:prefix).should == "my/awesome/api"
22 | end
23 |
24 | it "stores default url options" do
25 | subject.default_url_options(:host => "example.com")
26 | subject.option(:url_options).should == {:host => "example.com"}
27 | end
28 |
29 | it "stores middleware" do
30 | subject.use Rack::Auth::Basic
31 | subject.option(:middlewares, :merge).should == [[Rack::Auth::Basic]]
32 | end
33 |
34 | it "stores basic auth info" do
35 | handler = proc {}
36 |
37 | subject.basic_auth("Get out!", &handler)
38 | subject.settings[:global][:auth].should == ["Get out!", handler]
39 | end
40 |
41 | it "initializes application with correct parameters" do
42 | expected = {
43 | :version => "v1",
44 | :url_options => {:host => "mysite.com"},
45 | :default_format => "fffuuu",
46 | :prefix => "api",
47 | :handler => proc {}
48 | }
49 |
50 | Rack::API::Controller
51 | .should_receive(:new)
52 | .with(hash_including(expected))
53 | .and_return(mock.as_null_object)
54 |
55 | subject.version("v1") do
56 | respond_to :fffuuu
57 | prefix "api"
58 | default_url_options :host => "mysite.com"
59 |
60 | get("/", &expected[:handler])
61 | end
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/spec/rack-api/separators_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe Rack::API, "separators" do
4 | before do
5 | Rack::API.app do
6 | get("/no-dots/:name(.:format)") { params }
7 | get("/dots/:name", :separator => %w[/?]) { params }
8 | end
9 | end
10 |
11 | context "/no-dots/:name" do
12 | it "stops on /" do
13 | get "/no-dots/foo/"
14 | last_response.body.should == {:name => "foo"}.to_json
15 | end
16 |
17 | it "stops on ?" do
18 | get "/no-dots/foo?a=1"
19 | last_response.body.should == {:a => "1", :name => "foo"}.to_json
20 | end
21 |
22 | it "stops on ." do
23 | get "/no-dots/foo.json"
24 | last_response.body.should == {:name => "foo", :format => "json"}.to_json
25 | end
26 | end
27 |
28 | context "/dots/:name" do
29 | it "stops on /" do
30 | get "/dots/foo/"
31 | last_response.body.should == {:name => "foo"}.to_json
32 | end
33 |
34 | it "stops on ?" do
35 | get "/dots/foo?a=1"
36 | last_response.body.should == {:a => "1", :name => "foo"}.to_json
37 | end
38 |
39 | it "stops on ." do
40 | get "/dots/foo.json"
41 | last_response.body.should == {:name => "foo.json"}.to_json
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/spec/rack-api/settings_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe Rack::API::Runner, "Settings" do
4 | it "uses global namespace when no version is defined" do
5 | subject.set :foo, :bar
6 | subject.settings[:global][:foo].should == :bar
7 | end
8 |
9 | it "uses local namespace when version is defined" do
10 | subject.settings[:version] = "v1"
11 | subject.set :foo, :bar
12 |
13 | subject.settings[:foo].should == :bar
14 | end
15 |
16 | it "appends item when mode is :append" do
17 | subject.settings[:global][:list] = []
18 | subject.set :list, :item, :append
19 |
20 | subject.settings[:global][:list].should == [:item]
21 | end
22 |
23 | it "overrides item when mode is :override" do
24 | subject.settings[:global][:list] = []
25 | subject.set :list, [:item], :override
26 |
27 | subject.settings[:global][:list].should == [:item]
28 | end
29 |
30 | it "returns global value" do
31 | subject.set :name, "John Doe"
32 | subject.option(:name).should == "John Doe"
33 | end
34 |
35 | it "returns local value" do
36 | subject.settings[:version] = "v1"
37 | subject.set :name, "John Doe"
38 |
39 | subject.option(:name).should == "John Doe"
40 | end
41 |
42 | it "prefers local setting over global one" do
43 | subject.set :name, "Mary Doe"
44 |
45 | subject.settings[:version] = "v1"
46 | subject.set :name, "John Doe"
47 |
48 | subject.option(:name).should == "John Doe"
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/spec/rack-api/short_circuit_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe Rack::API, "Short circuit" do
4 | before do
5 | Rack::API.app do
6 | version :v1 do
7 | get("/") { error :status => 412, :headers => {"X-Awesome" => "UR NO Awesome"}, :message => "ZOMG! Nothing to see here!" }
8 | get("/custom") do
9 | error_message = Object.new
10 | def error_message.to_rack
11 | [412, {"X-Awesome" => "UR NO Awesome Indeed"}, ["Keep going!"]]
12 | end
13 |
14 | error(error_message)
15 | end
16 | end
17 | end
18 | end
19 |
20 | it "renders hash error" do
21 | get "/v1"
22 | last_response.status.should == 412
23 | last_response.headers["X-Awesome"].should == "UR NO Awesome"
24 | last_response.body.should == "ZOMG! Nothing to see here!"
25 | end
26 |
27 | it "renders object#to_rack method" do
28 | get "/v1/custom"
29 | last_response.status.should == 412
30 | last_response.headers["X-Awesome"].should == "UR NO Awesome Indeed"
31 | last_response.body.should == "Keep going!"
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/spec/rack-api/ssl_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe Rack::API::Middleware::SSL do
4 | let(:action) { proc {|env| [200, {}, ["success"]] } }
5 |
6 | it "denies http requests" do
7 | env = Rack::MockRequest.env_for("/v1", "rack.url_scheme" => "http")
8 | status, headers, response = Rack::API::Middleware::SSL.new(action).call(env)
9 |
10 | status.should == 400
11 | headers["Content-Type"].should == "text/plain"
12 | response.should include("Only HTTPS requests are supported by now.")
13 | end
14 |
15 | it "accepts https requests" do
16 | env = Rack::MockRequest.env_for("/v1", "rack.url_scheme" => "https")
17 | status, headers, response = Rack::API::Middleware::SSL.new(action).call(env)
18 |
19 | status.should == 200
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/spec/rack-api/url_for_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe Rack::API::Controller, "#url_for" do
4 | subject { Rack::API::Controller.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 "sets default url options hash" do
25 | subject = Rack::API::Controller.new(:version => "v1", :url_options => nil, :env => Rack::MockRequest.env_for("/v1"))
26 |
27 | expect {
28 | subject.url_for(:things, 1)
29 | }.to_not raise_error
30 | end
31 |
32 | it "uses a different host" do
33 | subject.url_options.merge!(:host => "mysite.com")
34 | subject.url_for.should == "http://mysite.com/v1"
35 | end
36 |
37 | it "uses a different protocol" do
38 | subject.url_options.merge!(:protocol => "https")
39 | subject.url_for.should == "https://example.org/v1"
40 | end
41 |
42 | it "uses a different port" do
43 | subject.url_options.merge!(:port => "2345")
44 | subject.url_for.should == "http://example.org:2345/v1"
45 | end
46 |
47 | it "uses #to_param when available" do
48 | subject.url_for("users", mock(:user, :to_param => "1-john-doe")).should == "http://example.org/v1/users/1-john-doe"
49 | end
50 |
51 | it "converts other data types" do
52 | subject.url_for(:users, 1).should == "http://example.org/v1/users/1"
53 | end
54 |
55 | it "adds query string" do
56 | actual = subject.url_for(:format => :json, :filters => [:name, :age])
57 | actual.should == "http://example.org/v1?filters%5B%5D=name&filters%5B%5D=age&format=json"
58 | end
59 |
60 | it "uses host from request" do
61 | env = Rack::MockRequest.env_for("/v1", "SERVER_NAME" => "mysite.com")
62 | subject = Rack::API::Controller.new(:version => "v1", :env => env)
63 | subject.url_for.should == "http://mysite.com/v1"
64 | end
65 |
66 | it "uses port from request" do
67 | env = Rack::MockRequest.env_for("/v1", "SERVER_PORT" => "2345")
68 | subject = Rack::API::Controller.new(:version => "v1", :env => env)
69 | subject.url_for.should == "http://example.org:2345/v1"
70 | end
71 | end
72 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require "bundler"
2 | Bundler.setup(:default, :development)
3 | Bundler.require
4 |
5 | require "rack/test"
6 | require "rspec"
7 | require "rack/api"
8 | require "base64"
9 | require "redis"
10 | require "ostruct"
11 |
12 | Dir[File.dirname(__FILE__) + "/support/**/*.rb"].each {|file| require file}
13 |
14 | RSpec.configure do |config|
15 | config.include Rack::Test::Methods
16 | config.include Helpers
17 |
18 | config.before { Rack::API.reset! }
19 | end
20 |
--------------------------------------------------------------------------------
/spec/support/awesome_middleware.rb:
--------------------------------------------------------------------------------
1 | class AwesomeMiddleware
2 | def initialize(app)
3 | @app = app
4 | end
5 |
6 | def call(env)
7 | status, headers, response = @app.call(env)
8 | [status, headers.merge("X-Awesome" => "U R Awesome"), response]
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/spec/support/core_ext.rb:
--------------------------------------------------------------------------------
1 | class Hash
2 | def to_awesome
3 | "U R Awesome"
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/support/helpers.rb:
--------------------------------------------------------------------------------
1 | module Helpers
2 | def app
3 | @app ||= Rack::API
4 | end
5 |
6 | def basic_auth(username, password)
7 | "Basic " + Base64.encode64("#{username}:#{password}")
8 | end
9 |
10 | def json(string)
11 | JSON.load(string)
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/spec/support/myapp.rb:
--------------------------------------------------------------------------------
1 | class MyApp < Rack::API
2 | version :v1 do
3 | get "/" do
4 | {:myapp => true}
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/support/mycontroller.rb:
--------------------------------------------------------------------------------
1 | class MyController < Rack::API::Controller
2 | def index
3 | {:name => params[:name]}
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/support/zomg_middleware.rb:
--------------------------------------------------------------------------------
1 | class ZOMGMiddleware
2 | def initialize(app)
3 | @app = app
4 | end
5 |
6 | def call(env)
7 | status, headers, response = @app.call(env)
8 | [status, headers.merge("X-ZOMG" => "ZOMG!"), response]
9 | end
10 | end
11 |
--------------------------------------------------------------------------------