├── .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 | --------------------------------------------------------------------------------