rack-api 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.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