rack-api 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ .DS_Store
2
+ pkg
3
+ tmp
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color --format documentation
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,49 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ rack-api (0.1.0)
5
+ activesupport (~> 3.0.6)
6
+ rack (~> 1.2.1)
7
+ rack-mount (~> 0.6.14)
8
+
9
+ GEM
10
+ remote: http://rubygems.org/
11
+ specs:
12
+ activesupport (3.0.6)
13
+ archive-tar-minitar (0.5.2)
14
+ columnize (0.3.2)
15
+ diff-lcs (1.1.2)
16
+ linecache19 (0.5.12)
17
+ ruby_core_source (>= 0.1.4)
18
+ rack (1.2.2)
19
+ rack-mount (0.6.14)
20
+ rack (>= 1.0.0)
21
+ rack-test (0.5.7)
22
+ rack (>= 1.0)
23
+ rspec (2.5.0)
24
+ rspec-core (~> 2.5.0)
25
+ rspec-expectations (~> 2.5.0)
26
+ rspec-mocks (~> 2.5.0)
27
+ rspec-core (2.5.1)
28
+ rspec-expectations (2.5.0)
29
+ diff-lcs (~> 1.1.2)
30
+ rspec-mocks (2.5.0)
31
+ ruby-debug-base19 (0.11.25)
32
+ columnize (>= 0.3.1)
33
+ linecache19 (>= 0.5.11)
34
+ ruby_core_source (>= 0.1.4)
35
+ ruby-debug19 (0.11.6)
36
+ columnize (>= 0.3.1)
37
+ linecache19 (>= 0.5.11)
38
+ ruby-debug-base19 (>= 0.11.19)
39
+ ruby_core_source (0.1.5)
40
+ archive-tar-minitar (>= 0.5.2)
41
+
42
+ PLATFORMS
43
+ ruby
44
+
45
+ DEPENDENCIES
46
+ rack-api!
47
+ rack-test (~> 0.5.7)
48
+ rspec (~> 2.5.0)
49
+ ruby-debug19
data/README.rdoc ADDED
@@ -0,0 +1,54 @@
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
+ For additional examples, see https://github.com/fnando/rack-api/tree/master/examples.
28
+
29
+ == Maintainer
30
+
31
+ * Nando Vieira (http://nandovieira.com.br)
32
+
33
+ == License
34
+
35
+ (The MIT License)
36
+
37
+ Permission is hereby granted, free of charge, to any person obtaining
38
+ a copy of this software and associated documentation files (the
39
+ 'Software'), to deal in the Software without restriction, including
40
+ without limitation the rights to use, copy, modify, merge, publish,
41
+ distribute, sublicense, and/or sell copies of the Software, and to
42
+ permit persons to whom the Software is furnished to do so, subject to
43
+ the following conditions:
44
+
45
+ The above copyright notice and this permission notice shall be
46
+ included in all copies or substantial portions of the Software.
47
+
48
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
49
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
50
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
51
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
52
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
53
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
54
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require "bundler"
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require "rspec/core/rake_task"
5
+ RSpec::Core::RakeTask.new
@@ -0,0 +1,22 @@
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
@@ -0,0 +1,28 @@
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
@@ -0,0 +1,19 @@
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
@@ -0,0 +1,34 @@
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
@@ -0,0 +1,24 @@
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
@@ -0,0 +1,18 @@
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
@@ -0,0 +1,18 @@
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
@@ -0,0 +1,162 @@
1
+ module Rack
2
+ class API
3
+ class App
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::App::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::App::MIME_TYPES.
23
+ #
24
+ DEFAULT_MIME_TYPE = "application/octet-stream"
25
+
26
+ attr_reader :block
27
+ attr_reader :env
28
+
29
+ # Hold block that will be executed in case the
30
+ # route is recognized.
31
+ #
32
+ def initialize(options)
33
+ options.each do |name, value|
34
+ instance_variable_set("@#{name}", value)
35
+ end
36
+ end
37
+
38
+ # Always log to the standard output.
39
+ #
40
+ def logger
41
+ @logger ||= Logger.new(STDOUT)
42
+ end
43
+
44
+ # Hold headers that will be sent on the response.
45
+ #
46
+ def headers
47
+ @headers ||= {}
48
+ end
49
+
50
+ # Merge all params into one single hash.
51
+ #
52
+ def params
53
+ @params ||= HashWithIndifferentAccess.new(request.params.merge(env["rack.routing_args"]))
54
+ end
55
+
56
+ # Return a request object.
57
+ #
58
+ def request
59
+ @request ||= Rack::Request.new(env)
60
+ end
61
+
62
+ # Return the requested format. Defaults to JSON.
63
+ #
64
+ def format
65
+ params.fetch(:format, "json")
66
+ end
67
+
68
+ # Stop processing by rendering the provided information.
69
+ #
70
+ # Rack::API.app do
71
+ # version :v1 do
72
+ # get "/" do
73
+ # error(:status => 403, :message => "Not here!")
74
+ # end
75
+ # end
76
+ # end
77
+ #
78
+ # Valid options are:
79
+ #
80
+ # # <tt>:status</tt>: a HTTP status code. Defaults to 403.
81
+ # # <tt>:message</tt>: a message that will be rendered as the response body. Defaults to "Forbidden".
82
+ # # <tt>:headers</tt>: the response headers. Defaults to <tt>{"Content-Type" => "text/plain"}</tt>.
83
+ #
84
+ # You can also provide a object that responds to <tt>to_rack</tt>. In this case, this
85
+ # method must return a valid Rack response (a 3-item array).
86
+ #
87
+ # class MyError
88
+ # def self.to_rack
89
+ # [500, {"Content-Type" => "text/plain"}, ["Internal Server Error"]]
90
+ # end
91
+ # end
92
+ #
93
+ # Rack::API.app do
94
+ # version :v1 do
95
+ # get "/" do
96
+ # error(MyError)
97
+ # end
98
+ # end
99
+ # end
100
+ #
101
+ def error(options = {})
102
+ throw :error, Response.new(options)
103
+ end
104
+
105
+ # Set response status code.
106
+ #
107
+ def status(*args)
108
+ @status = args.first unless args.empty?
109
+ @status || 200
110
+ end
111
+
112
+ # Reset environment between requests.
113
+ #
114
+ def reset! # :nodoc:
115
+ @params = nil
116
+ @request = nil
117
+ @headers = nil
118
+ end
119
+
120
+ # Render the result of block.
121
+ #
122
+ def call(env) # :nodoc:
123
+ reset!
124
+ @env = env
125
+
126
+ response = catch(:error) do
127
+ render instance_eval(&block)
128
+ end
129
+
130
+ response.respond_to?(:to_rack) ? response.to_rack : response
131
+ end
132
+
133
+ # Return response content type based on extension.
134
+ # If you're using an unknown extension that wasn't registered on
135
+ # Rack::API::App::MIME_TYPES, it will return Rack::API::App::DEFAULT_MIME_TYPE,
136
+ # which defaults to <tt>application/octet-stream</tt>.
137
+ #
138
+ def content_type
139
+ mime = MIME_TYPES.fetch(format, DEFAULT_MIME_TYPE)
140
+ headers.fetch("Content-Type", mime)
141
+ end
142
+
143
+ private
144
+ def render(response) # :nodoc:
145
+ [status, headers.merge("Content-Type" => content_type), [format_response(response)]]
146
+ end
147
+
148
+ def format_response(response) # :nodoc:
149
+ formatter_name = format.split("_").collect {|word| word[0,1].upcase + word[1,word.size].downcase}.join("")
150
+
151
+ if Rack::API::Formatter.const_defined?(formatter_name)
152
+ formatter = Rack::API::Formatter.const_get(formatter_name).new(response, params)
153
+ formatter.to_format
154
+ elsif response.respond_to?("to_#{format}")
155
+ response.__send__("to_#{format}")
156
+ else
157
+ throw :error, Response.new(:status => 406, :message => "Unknown format")
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,20 @@
1
+ module Rack
2
+ class API
3
+ module Formatter
4
+ class Base
5
+ attr_accessor :object
6
+ attr_accessor :params
7
+
8
+ class AbstractMethodError < StandardError; end
9
+
10
+ def initialize(object, params)
11
+ @object, @params = object, params
12
+ end
13
+
14
+ def to_format
15
+ raise AbstractMethodError
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,11 @@
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
@@ -0,0 +1,8 @@
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
@@ -0,0 +1,21 @@
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
@@ -0,0 +1,118 @@
1
+ module Rack
2
+ class API
3
+ class Runner
4
+ HTTP_METHODS = %w[get post put delete head]
5
+
6
+ attr_accessor :settings
7
+
8
+ def initialize
9
+ @settings = {
10
+ :prefix => "/",
11
+ :formats => %w[json jsonp],
12
+ :middlewares => []
13
+ }
14
+ end
15
+
16
+ # Add a middleware to the execution stack.
17
+ #
18
+ def use(middleware, *args)
19
+ settings[:middlewares] << [middleware, *args]
20
+ end
21
+
22
+ # Set an additional url prefix.
23
+ #
24
+ def prefix(name)
25
+ settings[:prefix] = name
26
+ end
27
+
28
+ # Create a new API version.
29
+ #
30
+ def version(name, &block)
31
+ raise ArgumentError, "you need to pass a block" unless block_given?
32
+ settings[:version] = name.to_s
33
+ instance_eval(&block)
34
+ settings.delete(:version)
35
+ end
36
+
37
+ # Run all routes.
38
+ #
39
+ def call(env) # :nodoc:
40
+ route_set.freeze.call(env)
41
+ end
42
+
43
+ # Require basic authentication before processing other requests.
44
+ # The authentication reques must be defined before routes.
45
+ #
46
+ # Rack::API.app do
47
+ # basic_auth "Protected Area" do |user, pass|
48
+ # User.authenticate(user, pass)
49
+ # end
50
+ # end
51
+ def basic_auth(realm = "Restricted Area", &block)
52
+ settings[:auth] = [realm, block]
53
+ end
54
+
55
+ # Define the formats that this app implements.
56
+ # Respond only to <tt>:json</tt> by default.
57
+ #
58
+ # When setting a format you have some alternatives on how this object
59
+ # will be formated.
60
+ #
61
+ # First, Rack::API will look for a formatter defined on Rack::API::Formatter
62
+ # namespace. If there's no formatter, it will look for a method <tt>to_<format></tt>.
63
+ # It will raise an exception if no formatter method has been defined.
64
+ #
65
+ # See Rack::API::Formatter::Jsonp for an example on how to create additional
66
+ # formatters.
67
+ #
68
+ def respond_to(*formats)
69
+ settings[:formats] = formats
70
+ end
71
+
72
+ # Hold all routes.
73
+ #
74
+ def route_set # :nodoc:
75
+ @route_set ||= Rack::Mount::RouteSet.new
76
+ end
77
+
78
+ # Define a new routing that will be triggered when both request method and
79
+ # path are recognized.
80
+ #
81
+ # You're better off using all verb shortcut methods. Implemented verbs are
82
+ # +get+, +post+, +put+, +delete+ and +head+.
83
+ #
84
+ # class MyAPI < Rack::API
85
+ # version "v1" do
86
+ # get "users(.:format)" do
87
+ # # do something
88
+ # end
89
+ # end
90
+ # end
91
+ #
92
+ def route(method, path, requirements = {}, &block)
93
+ path = Rack::Mount::Strexp.compile mount_path(path), requirements, %w[ / . ? ]
94
+ route_set.add_route(build_app(block), :path_info => path, :request_method => method)
95
+ end
96
+
97
+ HTTP_METHODS.each do |method|
98
+ class_eval <<-RUBY, __FILE__, __LINE__
99
+ def #{method}(*args, &block) # def get(*args, &block)
100
+ route("#{method.upcase}", *args, &block) # route("GET", *args, &block)
101
+ end # end
102
+ RUBY
103
+ end
104
+
105
+ def mount_path(path) # :nodoc:
106
+ Rack::Mount::Utils.normalize_path([settings[:prefix], settings[:version], path].join("/"))
107
+ end
108
+
109
+ def build_app(block) # :nodoc:
110
+ builder = Rack::Builder.new
111
+ builder.use Rack::Auth::Basic, settings[:auth][0], &settings[:auth][1] if settings[:auth]
112
+ settings[:middlewares].each {|middleware| builder.use(*middleware)}
113
+ builder.run App.new(:block => block)
114
+ builder.to_app
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,10 @@
1
+ module Rack
2
+ class API
3
+ module Version
4
+ MAJOR = 0
5
+ MINOR = 1
6
+ PATCH = 0
7
+ STRING = "#{MAJOR}.#{MINOR}.#{PATCH}"
8
+ end
9
+ end
10
+ end