sinatra-rest-addons 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/LICENSE +20 -0
- data/README.rdoc +8 -0
- data/Rakefile +54 -0
- data/VERSION +1 -0
- data/lib/sinatra/rest/helpers.rb +16 -0
- data/lib/sinatra/rest/routes.rb +116 -0
- data/sinatra-rest-addons.gemspec +56 -0
- data/test/helper.rb +38 -0
- data/test/test_routes.rb +240 -0
- metadata +84 -0
data/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) 2009 Cyril Rohr
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
data/Rakefile
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'rake'
|
|
3
|
+
|
|
4
|
+
begin
|
|
5
|
+
require 'jeweler'
|
|
6
|
+
Jeweler::Tasks.new do |gem|
|
|
7
|
+
gem.name = "sinatra-rest-addons"
|
|
8
|
+
gem.summary = %Q{A set of helpers and extensions for sinatra apps that expose REST resources.}
|
|
9
|
+
gem.description = %Q{A set of helpers and extensions for sinatra apps that expose REST resources.}
|
|
10
|
+
gem.email = "cyril.rohr@gmail.com"
|
|
11
|
+
gem.homepage = "http://github.com/crohr/sinatra-rest-addons"
|
|
12
|
+
gem.authors = ["Cyril Rohr"]
|
|
13
|
+
gem.add_development_dependency "contest", ">= 0"
|
|
14
|
+
gem.add_dependency "sinatra", ">= 0.9.0"
|
|
15
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
|
16
|
+
end
|
|
17
|
+
Jeweler::GemcutterTasks.new
|
|
18
|
+
rescue LoadError
|
|
19
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
require 'rake/testtask'
|
|
23
|
+
Rake::TestTask.new(:test) do |test|
|
|
24
|
+
test.libs << 'lib' << 'test'
|
|
25
|
+
test.pattern = 'test/**/test_*.rb'
|
|
26
|
+
test.verbose = true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
begin
|
|
30
|
+
require 'rcov/rcovtask'
|
|
31
|
+
Rcov::RcovTask.new do |test|
|
|
32
|
+
test.libs << 'test'
|
|
33
|
+
test.pattern = 'test/**/test_*.rb'
|
|
34
|
+
test.verbose = true
|
|
35
|
+
end
|
|
36
|
+
rescue LoadError
|
|
37
|
+
task :rcov do
|
|
38
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
task :test => :check_dependencies
|
|
43
|
+
|
|
44
|
+
task :default => :test
|
|
45
|
+
|
|
46
|
+
require 'rake/rdoctask'
|
|
47
|
+
Rake::RDocTask.new do |rdoc|
|
|
48
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
|
49
|
+
|
|
50
|
+
rdoc.rdoc_dir = 'rdoc'
|
|
51
|
+
rdoc.title = "sinatra-rest-addons #{version}"
|
|
52
|
+
rdoc.rdoc_files.include('README*')
|
|
53
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
54
|
+
end
|
data/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.0
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require 'digest/sha1'
|
|
2
|
+
|
|
3
|
+
module Sinatra
|
|
4
|
+
module REST
|
|
5
|
+
# Include it with:
|
|
6
|
+
# class App < Sinatra::Base
|
|
7
|
+
# helpers Sinatra::REST::Helpers
|
|
8
|
+
# end
|
|
9
|
+
module Helpers
|
|
10
|
+
def compute_etag(*args)
|
|
11
|
+
raise ArgumentError, "You must provide at least one parameter for the ETag computation" if args.empty?
|
|
12
|
+
Digest::SHA1.hexdigest(args.join("."))
|
|
13
|
+
end
|
|
14
|
+
end # module Helpers
|
|
15
|
+
end # module REST
|
|
16
|
+
end # module Sinatra
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
require 'sinatra/base'
|
|
2
|
+
|
|
3
|
+
module Sinatra
|
|
4
|
+
module REST
|
|
5
|
+
# Include it with:
|
|
6
|
+
# class App < Sinatra::Base
|
|
7
|
+
# register Sinatra::REST::Routes
|
|
8
|
+
# end
|
|
9
|
+
module Routes
|
|
10
|
+
INFINITY = 1/0.0
|
|
11
|
+
|
|
12
|
+
# Allow the definition of OPTIONS routes
|
|
13
|
+
def options(path, opts={}, &bk); route 'OPTIONS', path, opts, &bk end
|
|
14
|
+
|
|
15
|
+
# e.g.
|
|
16
|
+
# get '/', :provides => [:xml, "text/html;level=5"] { "hello" }
|
|
17
|
+
# will accept requests with Accept header =
|
|
18
|
+
# * application/xml
|
|
19
|
+
# * application/*
|
|
20
|
+
# * text/html
|
|
21
|
+
# * text/html;level=5
|
|
22
|
+
# * text/html;level=6
|
|
23
|
+
def provides(*formats)
|
|
24
|
+
generate_type_hash = Proc.new{ |header|
|
|
25
|
+
type, *params = header.split(/;\s*/)
|
|
26
|
+
Hash[*params.map{|p| p.split(/\s*=\s*/)}.flatten].merge("type" => type)
|
|
27
|
+
}
|
|
28
|
+
condition {
|
|
29
|
+
supported_formats = formats.map do |f|
|
|
30
|
+
# selects the correct mime type if a symbol is given
|
|
31
|
+
f.is_a?(Symbol) ? ::Rack::Mime::MIME_TYPES[".#{f.to_s}"] : f
|
|
32
|
+
end.compact.map do |f|
|
|
33
|
+
generate_type_hash.call(f)
|
|
34
|
+
end
|
|
35
|
+
# request.accept is an Array
|
|
36
|
+
accepted_formats = request.accept.map do |f|
|
|
37
|
+
generate_type_hash.call(f)
|
|
38
|
+
end
|
|
39
|
+
selected_format = supported_formats.detect{ |supported_format|
|
|
40
|
+
!accepted_formats.detect{ |accepted_format|
|
|
41
|
+
Regexp.new(Regexp.escape(accepted_format["type"]).gsub("\\*", ".*?"), Regexp::IGNORECASE) =~ supported_format["type"] &&
|
|
42
|
+
(accepted_format["level"] || INFINITY).to_f >= (supported_format["level"] || 0).to_f
|
|
43
|
+
}.nil?
|
|
44
|
+
}
|
|
45
|
+
if selected_format.nil?
|
|
46
|
+
content_type :txt
|
|
47
|
+
halt 406, supported_formats.map{|f|
|
|
48
|
+
output = f["type"]
|
|
49
|
+
output.concat(";level=#{f["level"]}") if f.has_key?("level")
|
|
50
|
+
output
|
|
51
|
+
}.join(",")
|
|
52
|
+
else
|
|
53
|
+
response.headers['Content-Type'] = "#{selected_format["type"]}#{selected_format["level"].nil? ? "" : ";level=#{selected_format["level"]}"}"
|
|
54
|
+
end
|
|
55
|
+
}
|
|
56
|
+
end # def provides
|
|
57
|
+
|
|
58
|
+
# Allow access to the route based on the result of the given proc, whose argument is a <tt>credentials</tt> object.
|
|
59
|
+
# You MUST declare a helper function named <tt>credentials</tt> that will return an object (of your choice) containing the client's credentials,
|
|
60
|
+
# that will be passed as an argument to the given Proc.
|
|
61
|
+
# Halts the response process with a 403 status code if the given proc returns false (you may then process the error with a <tt>error 403 {}</tt> block).
|
|
62
|
+
# e.g.
|
|
63
|
+
# helpers do
|
|
64
|
+
# def credentials; [params["user"], params["password"]]; end
|
|
65
|
+
# end
|
|
66
|
+
# get '/', :allow => Proc.new{ |credentials| credentials.first == "someone" && credentials.last == "password" } do
|
|
67
|
+
# "allowed"
|
|
68
|
+
# end
|
|
69
|
+
def allow(proc)
|
|
70
|
+
raise ArgumentError, "You must provide a Proc that returns true or false when given the result of a call to the 'credentials' helper" unless proc.kind_of?(Proc)
|
|
71
|
+
condition {
|
|
72
|
+
unless proc.call(credentials)
|
|
73
|
+
halt 403, "You cannot access this resource"
|
|
74
|
+
end
|
|
75
|
+
}
|
|
76
|
+
end # def allow
|
|
77
|
+
|
|
78
|
+
# Automatically decode the input data (coming from a POST or PUT request) based on the request's content-type.
|
|
79
|
+
# First argument must be a parsing procedure that will be used to parse the input data according to its content type.
|
|
80
|
+
# The decoded input will be available in request.env['sinatra.decoded_input']
|
|
81
|
+
# e.g.
|
|
82
|
+
# post '/',
|
|
83
|
+
# :decode => Proc.new{ |content_type, content|
|
|
84
|
+
# content_type =~ /^application\/.*json$/i ? JSON.parse(content) : throw(:halt, [400, "Don't know how to parse '#{content_type}' content."])
|
|
85
|
+
# } do
|
|
86
|
+
# "#{request.env['sinatra.decoded_input']}"
|
|
87
|
+
# end
|
|
88
|
+
def decode(*args)
|
|
89
|
+
args = [args] unless args.kind_of? Array
|
|
90
|
+
parsing_proc = args.shift
|
|
91
|
+
raise ArgumentError, "You must provide a proc to parse the input data" unless parsing_proc.kind_of?(Proc)
|
|
92
|
+
size_range = args.shift || (1..(1024**3))
|
|
93
|
+
condition {
|
|
94
|
+
begin
|
|
95
|
+
case (mime_type = request.env['CONTENT_TYPE'])
|
|
96
|
+
when /^application\/x-www-form-urlencoded/i
|
|
97
|
+
request.env['sinatra.decoded_input'] = request.env['rack.request.form_hash']
|
|
98
|
+
else
|
|
99
|
+
if not size_range.include?(request.env['rack.input'].size)
|
|
100
|
+
content_type :txt
|
|
101
|
+
halt 400, "Input data size must be between #{size_range.begin} and #{size_range.end} bytes."
|
|
102
|
+
else
|
|
103
|
+
content = request.env['rack.input'].read
|
|
104
|
+
request.env['sinatra.decoded_input'] = parsing_proc.call(mime_type, content)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
rescue StandardError => e
|
|
108
|
+
content_type :txt
|
|
109
|
+
halt 400, "#{e.class.name}: #{e.message}"
|
|
110
|
+
end
|
|
111
|
+
}
|
|
112
|
+
end # def decode
|
|
113
|
+
|
|
114
|
+
end # module Routes
|
|
115
|
+
end # module REST
|
|
116
|
+
end # module Sinatra
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Generated by jeweler
|
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
|
4
|
+
# -*- encoding: utf-8 -*-
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |s|
|
|
7
|
+
s.name = %q{sinatra-rest-addons}
|
|
8
|
+
s.version = "0.1.0"
|
|
9
|
+
|
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
|
11
|
+
s.authors = ["Cyril Rohr"]
|
|
12
|
+
s.date = %q{2009-11-13}
|
|
13
|
+
s.description = %q{A set of helpers and extensions for sinatra apps that expose REST resources.}
|
|
14
|
+
s.email = %q{cyril.rohr@gmail.com}
|
|
15
|
+
s.extra_rdoc_files = [
|
|
16
|
+
"LICENSE",
|
|
17
|
+
"README.rdoc"
|
|
18
|
+
]
|
|
19
|
+
s.files = [
|
|
20
|
+
"LICENSE",
|
|
21
|
+
"README.rdoc",
|
|
22
|
+
"Rakefile",
|
|
23
|
+
"VERSION",
|
|
24
|
+
"lib/sinatra/rest/helpers.rb",
|
|
25
|
+
"lib/sinatra/rest/routes.rb",
|
|
26
|
+
"sinatra-rest-addons.gemspec",
|
|
27
|
+
"test/helper.rb",
|
|
28
|
+
"test/test_routes.rb"
|
|
29
|
+
]
|
|
30
|
+
s.homepage = %q{http://github.com/crohr/sinatra-rest-addons}
|
|
31
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
|
32
|
+
s.require_paths = ["lib"]
|
|
33
|
+
s.rubygems_version = %q{1.3.5}
|
|
34
|
+
s.summary = %q{A set of helpers and extensions for sinatra apps that expose REST resources.}
|
|
35
|
+
s.test_files = [
|
|
36
|
+
"test/helper.rb",
|
|
37
|
+
"test/test_routes.rb"
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
if s.respond_to? :specification_version then
|
|
41
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
|
42
|
+
s.specification_version = 3
|
|
43
|
+
|
|
44
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
|
45
|
+
s.add_development_dependency(%q<contest>, [">= 0"])
|
|
46
|
+
s.add_runtime_dependency(%q<sinatra>, [">= 0.9.0"])
|
|
47
|
+
else
|
|
48
|
+
s.add_dependency(%q<contest>, [">= 0"])
|
|
49
|
+
s.add_dependency(%q<sinatra>, [">= 0.9.0"])
|
|
50
|
+
end
|
|
51
|
+
else
|
|
52
|
+
s.add_dependency(%q<contest>, [">= 0"])
|
|
53
|
+
s.add_dependency(%q<sinatra>, [">= 0.9.0"])
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
data/test/helper.rb
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Adapted from the Sinatra test suite
|
|
2
|
+
ENV['RACK_ENV'] = 'test'
|
|
3
|
+
|
|
4
|
+
begin
|
|
5
|
+
require 'rack'
|
|
6
|
+
rescue LoadError
|
|
7
|
+
require 'rubygems'
|
|
8
|
+
require 'rack'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
libdir = File.dirname(File.dirname(__FILE__)) + '/lib'
|
|
12
|
+
$LOAD_PATH.unshift libdir unless $LOAD_PATH.include?(libdir)
|
|
13
|
+
|
|
14
|
+
require 'contest'
|
|
15
|
+
require 'rack/test'
|
|
16
|
+
require 'sinatra/base'
|
|
17
|
+
|
|
18
|
+
class Sinatra::Base
|
|
19
|
+
# Allow assertions in request context
|
|
20
|
+
include Test::Unit::Assertions
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
Sinatra::Base.set :environment, :test
|
|
24
|
+
|
|
25
|
+
class Test::Unit::TestCase
|
|
26
|
+
include Rack::Test::Methods
|
|
27
|
+
|
|
28
|
+
# Sets up a Sinatra::Base subclass defined with the block
|
|
29
|
+
# given. Used in setup or individual spec methods to establish
|
|
30
|
+
# the application.
|
|
31
|
+
def mock_app(base=Sinatra::Base, &block)
|
|
32
|
+
@app = Sinatra.new(base, &block)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def app
|
|
36
|
+
Rack::Lint.new(@app)
|
|
37
|
+
end
|
|
38
|
+
end
|
data/test/test_routes.rb
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
require File.dirname(__FILE__) + '/helper'
|
|
2
|
+
require 'sinatra/rest/routes'
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
class RoutesTest < Test::Unit::TestCase
|
|
6
|
+
|
|
7
|
+
test "defines OPTIONS request handlers with options" do
|
|
8
|
+
mock_app {
|
|
9
|
+
register Sinatra::REST::Routes
|
|
10
|
+
options '/hello' do
|
|
11
|
+
response['X-Hello'] = 'World!'
|
|
12
|
+
'remove me'
|
|
13
|
+
end
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
request = Rack::MockRequest.new(@app)
|
|
17
|
+
response = request.request("OPTIONS", '/hello', {})
|
|
18
|
+
assert response.ok?
|
|
19
|
+
assert_equal 'World!', response.headers['X-Hello']
|
|
20
|
+
assert_equal 'remove me', response.body
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
context "route option :provides" do
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
test "should return 406 and the list of supported types, if the server does not support the types accepted by the client [simple matching]" do
|
|
27
|
+
mock_app {
|
|
28
|
+
register Sinatra::REST::Routes
|
|
29
|
+
get '/', :provides => ["application/xml", "application/vnd.x.y.z+xml"] do
|
|
30
|
+
request.env['HTTP_ACCEPT']
|
|
31
|
+
end
|
|
32
|
+
}
|
|
33
|
+
get '/', {}, { 'HTTP_ACCEPT' => 'application/json' }
|
|
34
|
+
assert_equal 406, last_response.status
|
|
35
|
+
assert_equal 'application/xml,application/vnd.x.y.z+xml', last_response.body
|
|
36
|
+
assert_equal 'text/plain', last_response.headers['Content-Type']
|
|
37
|
+
end
|
|
38
|
+
test "should return 406 if the accepted type has a level lower than what is supported" do
|
|
39
|
+
mock_app {
|
|
40
|
+
register Sinatra::REST::Routes
|
|
41
|
+
get '/', :provides => ["application/xml;level=5"] do
|
|
42
|
+
request.env['HTTP_ACCEPT']
|
|
43
|
+
end
|
|
44
|
+
}
|
|
45
|
+
get '/', {}, { 'HTTP_ACCEPT' => 'application/xml;level=4' }
|
|
46
|
+
assert_equal 406, last_response.status
|
|
47
|
+
assert_equal 'application/xml;level=5', last_response.body
|
|
48
|
+
assert_equal 'text/plain', last_response.headers['Content-Type']
|
|
49
|
+
end
|
|
50
|
+
test "should return the first matching type if the accepted type contains a *" do
|
|
51
|
+
mock_app {
|
|
52
|
+
register Sinatra::REST::Routes
|
|
53
|
+
get '/', :provides => ["application/xml", "application/vnd.x.y.z+xml"] do
|
|
54
|
+
request.env['HTTP_ACCEPT']
|
|
55
|
+
end
|
|
56
|
+
}
|
|
57
|
+
get '/', {}, { 'HTTP_ACCEPT' => 'application/*' }
|
|
58
|
+
assert_equal 200, last_response.status
|
|
59
|
+
assert_equal 'application/*', last_response.body
|
|
60
|
+
assert_equal 'application/xml', last_response.headers['Content-Type']
|
|
61
|
+
end
|
|
62
|
+
test "should be successful if the accepted type does not require a specific level" do
|
|
63
|
+
mock_app {
|
|
64
|
+
register Sinatra::REST::Routes
|
|
65
|
+
get '/', :provides => ["application/xml;level=5"] do
|
|
66
|
+
request.env['HTTP_ACCEPT']
|
|
67
|
+
end
|
|
68
|
+
}
|
|
69
|
+
get '/', {}, { 'HTTP_ACCEPT' => 'application/xml' }
|
|
70
|
+
assert last_response.ok?
|
|
71
|
+
assert_equal 'application/xml', last_response.body
|
|
72
|
+
assert_equal 'application/xml;level=5', last_response.headers['Content-Type']
|
|
73
|
+
end
|
|
74
|
+
test "should be successful if the accepted type level is greater than what is supported" do
|
|
75
|
+
mock_app {
|
|
76
|
+
register Sinatra::REST::Routes
|
|
77
|
+
get '/', :provides => ["application/xml;level=5"] do
|
|
78
|
+
request.env['HTTP_ACCEPT']
|
|
79
|
+
end
|
|
80
|
+
}
|
|
81
|
+
get '/', {}, { 'HTTP_ACCEPT' => 'application/xml;level=6' }
|
|
82
|
+
assert last_response.ok?
|
|
83
|
+
assert_equal 'application/xml;level=6', last_response.body
|
|
84
|
+
assert_equal 'application/xml;level=5', last_response.headers['Content-Type']
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
context "route option :decode" do
|
|
89
|
+
setup do
|
|
90
|
+
PARSING_PROC = Proc.new{|content_type, content| content_type =~ /^application\/.*json$/i ? JSON.parse(content) : throw(:halt, [400, "Cannot parse"])} unless defined? PARSING_PROC
|
|
91
|
+
end
|
|
92
|
+
test "should return 400 if the input content is empty" do
|
|
93
|
+
mock_app {
|
|
94
|
+
register Sinatra::REST::Routes
|
|
95
|
+
post '/', :decode => PARSING_PROC do
|
|
96
|
+
request.env['sinatra.decoded_input'].inspect
|
|
97
|
+
end
|
|
98
|
+
}
|
|
99
|
+
post '/', "", {'CONTENT_TYPE' => "application/json"}
|
|
100
|
+
assert_equal 400, last_response.status
|
|
101
|
+
assert_equal 'Input data size must be between 1 and 1073741824 bytes.', last_response.body
|
|
102
|
+
assert_equal 'text/plain', last_response.headers['Content-Type']
|
|
103
|
+
end
|
|
104
|
+
test "should return 400 if the input content is too large" do
|
|
105
|
+
mock_app {
|
|
106
|
+
register Sinatra::REST::Routes
|
|
107
|
+
post '/', :decode => [PARSING_PROC, 2...30] do
|
|
108
|
+
request.env['sinatra.decoded_input'].inspect
|
|
109
|
+
end
|
|
110
|
+
}
|
|
111
|
+
post '/', '{"key1": ["value1", "value2"]}', {'CONTENT_TYPE' => "application/json"}
|
|
112
|
+
assert_equal 400, last_response.status
|
|
113
|
+
assert_equal 'Input data size must be between 2 and 30 bytes.', last_response.body
|
|
114
|
+
assert_equal 'text/plain', last_response.headers['Content-Type']
|
|
115
|
+
end
|
|
116
|
+
test "should return 400 if the input content can be parsed but is malformed" do
|
|
117
|
+
mock_app {
|
|
118
|
+
register Sinatra::REST::Routes
|
|
119
|
+
post '/', :decode => [PARSING_PROC, 2..30] do
|
|
120
|
+
request.env['sinatra.decoded_input'].inspect
|
|
121
|
+
end
|
|
122
|
+
}
|
|
123
|
+
post '/', '{"key1": ["value1", "value2"]', {'CONTENT_TYPE' => "application/json"}
|
|
124
|
+
assert_equal 400, last_response.status
|
|
125
|
+
assert_equal "JSON::ParserError: 618: unexpected token at '{\"key1\": [\"value1\", \"value2\"]'", last_response.body
|
|
126
|
+
assert_equal 'text/plain', last_response.headers['Content-Type']
|
|
127
|
+
end
|
|
128
|
+
test "should allow the parsing proc to throw an exception if needed (e.g. no parser can be found for the requested content type)" do
|
|
129
|
+
mock_app {
|
|
130
|
+
register Sinatra::REST::Routes
|
|
131
|
+
post '/', :decode => [PARSING_PROC, 2..30] do
|
|
132
|
+
request.env['sinatra.decoded_input'].inspect
|
|
133
|
+
end
|
|
134
|
+
}
|
|
135
|
+
post '/', '<item></item>', {'CONTENT_TYPE' => "application/xml"}
|
|
136
|
+
assert_equal 400, last_response.status
|
|
137
|
+
assert_equal "Cannot parse", last_response.body
|
|
138
|
+
assert_equal 'text/html', last_response.headers['Content-Type']
|
|
139
|
+
end
|
|
140
|
+
test "should correctly parse the input content if a parser can be found for the specified content type, and the decoded input must be made available in env['sinatra.decoded_input']" do
|
|
141
|
+
input = {"key1" => ["value1", "value2"]}
|
|
142
|
+
mock_app {
|
|
143
|
+
register Sinatra::REST::Routes
|
|
144
|
+
post '/', :decode => [PARSING_PROC, 2...30] do
|
|
145
|
+
content_type 'application/json'
|
|
146
|
+
JSON.dump request.env['sinatra.decoded_input']
|
|
147
|
+
end
|
|
148
|
+
}
|
|
149
|
+
post '/', JSON.dump(input), {'CONTENT_TYPE' => "application/json"}
|
|
150
|
+
assert_equal 200, last_response.status
|
|
151
|
+
assert_equal JSON.dump(input), last_response.body
|
|
152
|
+
assert_equal 'application/json', last_response.headers['Content-Type']
|
|
153
|
+
end
|
|
154
|
+
test "should set the decoded input to the params hash already decoded by Sinatra, if the request's content type is application/x-www-form-urlencoded" do
|
|
155
|
+
input = {"key1" => ["value1", "value2"]}
|
|
156
|
+
mock_app {
|
|
157
|
+
register Sinatra::REST::Routes
|
|
158
|
+
post '/', :decode => [PARSING_PROC, 2...30] do
|
|
159
|
+
content_type 'application/json'
|
|
160
|
+
JSON.dump request.env['sinatra.decoded_input']
|
|
161
|
+
end
|
|
162
|
+
}
|
|
163
|
+
post '/', input
|
|
164
|
+
assert_equal 200, last_response.status
|
|
165
|
+
assert_equal JSON.dump(input), last_response.body
|
|
166
|
+
assert_equal 'application/json', last_response.headers['Content-Type']
|
|
167
|
+
end
|
|
168
|
+
test "should return 400 if the parsing proc raises a StandardError" do
|
|
169
|
+
mock_app {
|
|
170
|
+
register Sinatra::REST::Routes
|
|
171
|
+
post '/', :decode => Proc.new{|content_type, content| raise(StandardError, "error message") } do
|
|
172
|
+
request.env['sinatra.decoded_input'].inspect
|
|
173
|
+
end
|
|
174
|
+
}
|
|
175
|
+
post '/', 'whatever content', {'CONTENT_TYPE' => "whatever type"}
|
|
176
|
+
assert_equal 400, last_response.status
|
|
177
|
+
assert_equal "StandardError: error message", last_response.body
|
|
178
|
+
assert_equal 'text/plain', last_response.headers['Content-Type']
|
|
179
|
+
end
|
|
180
|
+
test "should raise an error if the first argument is not a proc" do
|
|
181
|
+
assert_raise(ArgumentError){ mock_app {
|
|
182
|
+
register Sinatra::REST::Routes
|
|
183
|
+
disable :raise_errors, :show_exceptions
|
|
184
|
+
post '/', :decode => :whatever do
|
|
185
|
+
JSON.dump request.env['sinatra.decoded_input']
|
|
186
|
+
end
|
|
187
|
+
} }
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
context "route option :allow" do
|
|
192
|
+
test "should return 403 if the proc returns false" do
|
|
193
|
+
mock_app {
|
|
194
|
+
register Sinatra::REST::Routes
|
|
195
|
+
helpers do
|
|
196
|
+
def credentials; ["nobody"]; end
|
|
197
|
+
end
|
|
198
|
+
get '/', :allow => lambda{|credentials| credentials.first == "crohr" } do
|
|
199
|
+
"allowed"
|
|
200
|
+
end
|
|
201
|
+
}
|
|
202
|
+
get '/'
|
|
203
|
+
assert_equal 403, last_response.status
|
|
204
|
+
assert_equal "You cannot access this resource", last_response.body
|
|
205
|
+
end
|
|
206
|
+
test "should return 200 if the proc returns true" do
|
|
207
|
+
mock_app {
|
|
208
|
+
register Sinatra::REST::Routes
|
|
209
|
+
helpers do
|
|
210
|
+
def credentials; ["crohr", "1234x"]; end
|
|
211
|
+
end
|
|
212
|
+
get '/', :allow => lambda{|credentials| credentials.first == "crohr" && credentials.last == "1234x" } do
|
|
213
|
+
"allowed"
|
|
214
|
+
end
|
|
215
|
+
}
|
|
216
|
+
get '/'
|
|
217
|
+
assert_equal 200, last_response.status
|
|
218
|
+
assert_equal "allowed", last_response.body
|
|
219
|
+
end
|
|
220
|
+
test "should raise an error if the argument is not a proc" do
|
|
221
|
+
assert_raise(ArgumentError) {
|
|
222
|
+
mock_app {
|
|
223
|
+
register Sinatra::REST::Routes
|
|
224
|
+
get '/', :allow => :whatever do
|
|
225
|
+
"allowed"
|
|
226
|
+
end
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
end
|
|
230
|
+
test "should raise a NoMethodError if 'credentials' helpers are not defined" do
|
|
231
|
+
mock_app {
|
|
232
|
+
register Sinatra::REST::Routes
|
|
233
|
+
get '/', :allow => lambda{} do
|
|
234
|
+
"allowed"
|
|
235
|
+
end
|
|
236
|
+
}
|
|
237
|
+
assert_raise(NameError) { get '/' }
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: sinatra-rest-addons
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Cyril Rohr
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
|
|
12
|
+
date: 2009-11-13 00:00:00 +01:00
|
|
13
|
+
default_executable:
|
|
14
|
+
dependencies:
|
|
15
|
+
- !ruby/object:Gem::Dependency
|
|
16
|
+
name: contest
|
|
17
|
+
type: :development
|
|
18
|
+
version_requirement:
|
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
20
|
+
requirements:
|
|
21
|
+
- - ">="
|
|
22
|
+
- !ruby/object:Gem::Version
|
|
23
|
+
version: "0"
|
|
24
|
+
version:
|
|
25
|
+
- !ruby/object:Gem::Dependency
|
|
26
|
+
name: sinatra
|
|
27
|
+
type: :runtime
|
|
28
|
+
version_requirement:
|
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: 0.9.0
|
|
34
|
+
version:
|
|
35
|
+
description: A set of helpers and extensions for sinatra apps that expose REST resources.
|
|
36
|
+
email: cyril.rohr@gmail.com
|
|
37
|
+
executables: []
|
|
38
|
+
|
|
39
|
+
extensions: []
|
|
40
|
+
|
|
41
|
+
extra_rdoc_files:
|
|
42
|
+
- LICENSE
|
|
43
|
+
- README.rdoc
|
|
44
|
+
files:
|
|
45
|
+
- LICENSE
|
|
46
|
+
- README.rdoc
|
|
47
|
+
- Rakefile
|
|
48
|
+
- VERSION
|
|
49
|
+
- lib/sinatra/rest/helpers.rb
|
|
50
|
+
- lib/sinatra/rest/routes.rb
|
|
51
|
+
- sinatra-rest-addons.gemspec
|
|
52
|
+
- test/helper.rb
|
|
53
|
+
- test/test_routes.rb
|
|
54
|
+
has_rdoc: true
|
|
55
|
+
homepage: http://github.com/crohr/sinatra-rest-addons
|
|
56
|
+
licenses: []
|
|
57
|
+
|
|
58
|
+
post_install_message:
|
|
59
|
+
rdoc_options:
|
|
60
|
+
- --charset=UTF-8
|
|
61
|
+
require_paths:
|
|
62
|
+
- lib
|
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: "0"
|
|
68
|
+
version:
|
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: "0"
|
|
74
|
+
version:
|
|
75
|
+
requirements: []
|
|
76
|
+
|
|
77
|
+
rubyforge_project:
|
|
78
|
+
rubygems_version: 1.3.5
|
|
79
|
+
signing_key:
|
|
80
|
+
specification_version: 3
|
|
81
|
+
summary: A set of helpers and extensions for sinatra apps that expose REST resources.
|
|
82
|
+
test_files:
|
|
83
|
+
- test/helper.rb
|
|
84
|
+
- test/test_routes.rb
|