restfulness 0.3.2 → 0.3.3
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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +6 -5
- data/README.md +98 -80
- data/lib/restfulness.rb +3 -0
- data/lib/restfulness/dispatchers/rack.rb +1 -0
- data/lib/restfulness/headers/accept.rb +66 -0
- data/lib/restfulness/headers/media_type.rb +127 -0
- data/lib/restfulness/request.rb +43 -15
- data/lib/restfulness/version.rb +1 -1
- data/restfulness.gemspec +1 -1
- data/spec/spec_helper.rb +1 -1
- data/spec/unit/application_spec.rb +14 -14
- data/spec/unit/dispatcher_spec.rb +1 -1
- data/spec/unit/dispatchers/rack_spec.rb +21 -20
- data/spec/unit/exceptions_spec.rb +5 -5
- data/spec/unit/headers/accept_spec.rb +70 -0
- data/spec/unit/headers/media_type_spec.rb +262 -0
- data/spec/unit/http_authentication/basic_spec.rb +7 -7
- data/spec/unit/path_spec.rb +19 -19
- data/spec/unit/request_spec.rb +96 -44
- data/spec/unit/requests/authorization_header_spec.rb +8 -8
- data/spec/unit/requests/authorization_spec.rb +3 -3
- data/spec/unit/resource_spec.rb +28 -28
- data/spec/unit/resources/authentication_spec.rb +2 -2
- data/spec/unit/resources/events_spec.rb +1 -1
- data/spec/unit/response_spec.rb +53 -53
- data/spec/unit/route_spec.rb +24 -24
- data/spec/unit/router_spec.rb +29 -29
- data/spec/unit/sanitizer_spec.rb +9 -9
- metadata +13 -7
@@ -0,0 +1,127 @@
|
|
1
|
+
module Restfulness
|
2
|
+
module Headers
|
3
|
+
|
4
|
+
# Generic media type handling according to the RFC2616 HTTP/1.1 header fields
|
5
|
+
# specification.
|
6
|
+
#
|
7
|
+
# If instantiated with a string, the MediaType object will attempt to parse and
|
8
|
+
# set the objects attributes.
|
9
|
+
#
|
10
|
+
# If an empty or no string is provided, the media-type can be prepared by setting
|
11
|
+
# the type, subtype and optional parameters values. Calling the #to_s method will
|
12
|
+
# provide the compiled version.
|
13
|
+
#
|
14
|
+
# Accessor names and parsing is based on details from https://en.wikipedia.org/wiki/Media_type.
|
15
|
+
#
|
16
|
+
class MediaType
|
17
|
+
|
18
|
+
# First part of the mime-type, typically "application", "text", or similar.
|
19
|
+
# Vendor types are not supported.
|
20
|
+
attr_accessor :type
|
21
|
+
|
22
|
+
# Always last part of definition. For example:
|
23
|
+
#
|
24
|
+
# * "json" from "application/json"
|
25
|
+
# * "user" from "application/vnd.example.user+json;version=1"
|
26
|
+
#
|
27
|
+
attr_accessor :subtype
|
28
|
+
|
29
|
+
# Refers to the vendor part of type string, for example:
|
30
|
+
#
|
31
|
+
# * "example" from "application/vnd.example.user+json"
|
32
|
+
#
|
33
|
+
attr_accessor :vendor
|
34
|
+
|
35
|
+
# When using vendor content types, a suffix may be provided:
|
36
|
+
#
|
37
|
+
# * "json" from "application/vnd.example.user+json"
|
38
|
+
#
|
39
|
+
attr_accessor :suffix
|
40
|
+
|
41
|
+
# Hash of parameters using symbols as keys
|
42
|
+
attr_accessor :parameters
|
43
|
+
|
44
|
+
def initialize(str = "")
|
45
|
+
# Defaults
|
46
|
+
self.type = "*"
|
47
|
+
self.subtype = "*"
|
48
|
+
self.vendor = ""
|
49
|
+
self.suffix = ""
|
50
|
+
self.parameters = {}
|
51
|
+
|
52
|
+
# Attempt to parse string if provided
|
53
|
+
parse(str) unless str.empty?
|
54
|
+
end
|
55
|
+
|
56
|
+
def parse(str)
|
57
|
+
# Split between base and parameters
|
58
|
+
parts = str.split(';').map{|p| p.strip}
|
59
|
+
t = parts.shift.split('/', 2)
|
60
|
+
self.type = t[0] if t[0]
|
61
|
+
|
62
|
+
# Handle subtype, and more complex vendor + suffix
|
63
|
+
if t[1]
|
64
|
+
(v, s) = t[1].split('+',2)
|
65
|
+
self.suffix = s if s
|
66
|
+
s = v.split('.')
|
67
|
+
s.shift if s[0] == 'vnd'
|
68
|
+
self.subtype = s.pop
|
69
|
+
self.vendor = s.join('.') unless s.empty?
|
70
|
+
end
|
71
|
+
|
72
|
+
# Finally, with remaining parts, handle parameters
|
73
|
+
self.parameters = Hash[parts.map{|p| (k,v) = p.split('=', 2); [k.to_sym, v]}]
|
74
|
+
end
|
75
|
+
|
76
|
+
def to_s
|
77
|
+
base = "#{type}/"
|
78
|
+
if !vendor.empty?
|
79
|
+
base << ["vnd", vendor, subtype].join('.')
|
80
|
+
else
|
81
|
+
base << subtype
|
82
|
+
end
|
83
|
+
base << "+#{suffix}" unless suffix.empty?
|
84
|
+
base << ";" + parameters.map{|k,v| "#{k}=#{v}"}.join(';') unless parameters.empty?
|
85
|
+
base
|
86
|
+
end
|
87
|
+
|
88
|
+
def ==(value)
|
89
|
+
if value.is_a?(String)
|
90
|
+
value = self.class.new(value)
|
91
|
+
end
|
92
|
+
raise "Invalid type comparison!" unless value.is_a?(MediaType)
|
93
|
+
type == value.type &&
|
94
|
+
subtype == value.subtype &&
|
95
|
+
vendor == value.vendor &&
|
96
|
+
suffix == value.suffix &&
|
97
|
+
parameters == value.parameters
|
98
|
+
end
|
99
|
+
|
100
|
+
def charset
|
101
|
+
parameters[:charset]
|
102
|
+
end
|
103
|
+
|
104
|
+
def version
|
105
|
+
parameters[:version]
|
106
|
+
end
|
107
|
+
|
108
|
+
def json?
|
109
|
+
type == "application" && (subtype == "json" || suffix == "json")
|
110
|
+
end
|
111
|
+
|
112
|
+
def xml?
|
113
|
+
type == "application" && (subtype == "xml" || suffix == "xml")
|
114
|
+
end
|
115
|
+
|
116
|
+
def text?
|
117
|
+
type == "text" && subtype == "plain"
|
118
|
+
end
|
119
|
+
|
120
|
+
def form?
|
121
|
+
type == "application" && subtype == "x-www-form-urlencoded"
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
end
|
data/lib/restfulness/request.rb
CHANGED
@@ -1,12 +1,15 @@
|
|
1
1
|
module Restfulness
|
2
2
|
|
3
3
|
# Simple, indpendent, request interface for dealing with the incoming information
|
4
|
-
# in a request.
|
4
|
+
# in a request.
|
5
5
|
#
|
6
6
|
# Currently wraps around the information provided in a Rack Request object.
|
7
7
|
class Request
|
8
8
|
include Requests::Authorization
|
9
9
|
|
10
|
+
# Expose rack env to interact with rack middleware
|
11
|
+
attr_accessor :env
|
12
|
+
|
10
13
|
# Who does this request belong to?
|
11
14
|
attr_reader :app
|
12
15
|
|
@@ -55,19 +58,32 @@ module Restfulness
|
|
55
58
|
@sanitized_query ||= uri.query ? Sanitizer.sanitize_query_string(uri.query) : ''
|
56
59
|
end
|
57
60
|
|
61
|
+
def accept
|
62
|
+
if headers[:accept]
|
63
|
+
@accept ||= Headers::Accept.new(headers[:accept])
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def content_type
|
68
|
+
if headers[:content_type]
|
69
|
+
@content_type ||= Headers::MediaType.new(headers[:content_type])
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
58
73
|
def params
|
59
74
|
@params ||= begin
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
when /application\/x\-www\-form\-urlencoded/
|
67
|
-
@params = params_from_form(body)
|
75
|
+
data = body_to_string || ""
|
76
|
+
if data.length > 0
|
77
|
+
if content_type && content_type.json?
|
78
|
+
params_from_json(data)
|
79
|
+
elsif content_type && content_type.form?
|
80
|
+
params_from_form(data)
|
68
81
|
else
|
82
|
+
# Body provided with no or invalid content type
|
69
83
|
raise HTTPException.new(406)
|
70
84
|
end
|
85
|
+
else
|
86
|
+
{}
|
71
87
|
end
|
72
88
|
end
|
73
89
|
end
|
@@ -90,15 +106,27 @@ module Restfulness
|
|
90
106
|
|
91
107
|
protected
|
92
108
|
|
93
|
-
def
|
94
|
-
|
109
|
+
def body_to_string
|
110
|
+
unless body.nil?
|
111
|
+
# Sometimes the body can be a StringIO, Tempfile, or some other freakish IO.
|
112
|
+
if body.respond_to?(:read)
|
113
|
+
body.read
|
114
|
+
else
|
115
|
+
body
|
116
|
+
end
|
117
|
+
else
|
118
|
+
""
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def params_from_json(data)
|
123
|
+
MultiJson.decode(data)
|
95
124
|
rescue MultiJson::LoadError
|
96
|
-
raise HTTPException.new(400)
|
125
|
+
raise HTTPException.new(400, "Invalid JSON in request body")
|
97
126
|
end
|
98
127
|
|
99
|
-
def params_from_form(
|
100
|
-
|
101
|
-
Rack::Utils.parse_query(body.is_a?(StringIO) ? body.read : body)
|
128
|
+
def params_from_form(data)
|
129
|
+
Rack::Utils.parse_query(data)
|
102
130
|
end
|
103
131
|
|
104
132
|
end
|
data/lib/restfulness/version.rb
CHANGED
data/restfulness.gemspec
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -14,7 +14,7 @@ describe Restfulness::Application do
|
|
14
14
|
describe "#router" do
|
15
15
|
it "should access class's router" do
|
16
16
|
obj = klass.new
|
17
|
-
obj.router.
|
17
|
+
expect(obj.router).to eql(klass.router)
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
@@ -23,8 +23,8 @@ describe Restfulness::Application do
|
|
23
23
|
env = {}
|
24
24
|
obj = klass.new
|
25
25
|
app = double(:app)
|
26
|
-
app.
|
27
|
-
obj.
|
26
|
+
expect(app).to receive(:call).with(env)
|
27
|
+
expect(obj).to receive(:build_rack_app).and_return(app)
|
28
28
|
obj.call(env)
|
29
29
|
end
|
30
30
|
end
|
@@ -34,10 +34,10 @@ describe Restfulness::Application do
|
|
34
34
|
obj = klass.new
|
35
35
|
obj.class.middlewares << Rack::ShowExceptions
|
36
36
|
app = obj.send(:build_rack_app)
|
37
|
-
app.
|
37
|
+
expect(app).to be_a(Rack::Builder)
|
38
38
|
# Note, this might brake if Rack changes!
|
39
|
-
app.instance_variable_get(:@use).first.call.
|
40
|
-
app.instance_variable_get(:@run).
|
39
|
+
expect(app.instance_variable_get(:@use).first.call).to be_a(klass.middlewares.first)
|
40
|
+
expect(app.instance_variable_get(:@run)).to be_a(Restfulness::Dispatchers::Rack)
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
@@ -45,18 +45,18 @@ describe Restfulness::Application do
|
|
45
45
|
|
46
46
|
context "basic usage" do
|
47
47
|
it "should build a new router with block" do
|
48
|
-
klass.router.
|
49
|
-
klass.router.
|
48
|
+
expect(klass.router).not_to be_nil
|
49
|
+
expect(klass.router).to be_a(Restfulness::Router)
|
50
50
|
end
|
51
51
|
|
52
52
|
it "should be accessable from instance" do
|
53
53
|
obj = klass.new
|
54
|
-
obj.router.
|
54
|
+
expect(obj.router).to eql(klass.router)
|
55
55
|
end
|
56
56
|
|
57
57
|
it "should pass block to Router instance" do
|
58
58
|
block = lambda { }
|
59
|
-
Restfulness::Router.
|
59
|
+
expect(Restfulness::Router).to receive(:new).with(no_args, &block)
|
60
60
|
Class.new(Restfulness::Application) do
|
61
61
|
routes &block
|
62
62
|
end
|
@@ -67,14 +67,14 @@ describe Restfulness::Application do
|
|
67
67
|
|
68
68
|
describe ".middlewares" do
|
69
69
|
it "should provide empty array of middlewares" do
|
70
|
-
klass.middlewares.
|
71
|
-
klass.middlewares.
|
70
|
+
expect(klass.middlewares).to be_a(Array)
|
71
|
+
expect(klass.middlewares).to be_empty
|
72
72
|
end
|
73
73
|
end
|
74
74
|
|
75
75
|
describe ".logger" do
|
76
76
|
it "should return main logger" do
|
77
|
-
klass.logger.
|
77
|
+
expect(klass.logger).to eql(Restfulness.logger)
|
78
78
|
end
|
79
79
|
end
|
80
80
|
|
@@ -83,7 +83,7 @@ describe Restfulness::Application do
|
|
83
83
|
orig = Restfulness.logger
|
84
84
|
logger = double(:Logger)
|
85
85
|
klass.logger = logger
|
86
|
-
Restfulness.logger.
|
86
|
+
expect(Restfulness.logger).to eql(logger)
|
87
87
|
Restfulness.logger = orig
|
88
88
|
end
|
89
89
|
end
|
@@ -16,7 +16,7 @@ describe Restfulness::Dispatchers::Rack do
|
|
16
16
|
let :app do
|
17
17
|
Class.new(Restfulness::Application) {
|
18
18
|
routes do
|
19
|
-
add 'projects', RackExampleResource
|
19
|
+
add 'projects', RackExampleResource
|
20
20
|
end
|
21
21
|
}.new
|
22
22
|
end
|
@@ -46,9 +46,9 @@ describe Restfulness::Dispatchers::Rack do
|
|
46
46
|
|
47
47
|
it "should handle basic call and return response" do
|
48
48
|
res = obj.call(env)
|
49
|
-
res[0].
|
50
|
-
res[1].
|
51
|
-
res[2].first.
|
49
|
+
expect(res[0]).to eql(200)
|
50
|
+
expect(res[1]).to be_a(Hash)
|
51
|
+
expect(res[2].first).to eql('rack_example_result')
|
52
52
|
end
|
53
53
|
|
54
54
|
|
@@ -60,7 +60,7 @@ describe Restfulness::Dispatchers::Rack do
|
|
60
60
|
actions = ['DELETE', 'GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'OPTIONS']
|
61
61
|
actions.each do |action|
|
62
62
|
val = obj.send(:parse_action, env, action)
|
63
|
-
val.
|
63
|
+
expect(val).to eql(action.downcase.to_sym)
|
64
64
|
end
|
65
65
|
end
|
66
66
|
|
@@ -72,12 +72,12 @@ describe Restfulness::Dispatchers::Rack do
|
|
72
72
|
|
73
73
|
it "should override the action if the override header is present" do
|
74
74
|
env['HTTP_X_HTTP_METHOD_OVERRIDE'] = 'PATCH'
|
75
|
-
obj.send(:parse_action, env, 'POST').
|
75
|
+
expect(obj.send(:parse_action, env, 'POST')).to eql(:patch)
|
76
76
|
end
|
77
77
|
|
78
78
|
it "should handle junk in action override header" do
|
79
79
|
env['HTTP_X_HTTP_METHOD_OVERRIDE'] = ' PatCH '
|
80
|
-
obj.send(:parse_action, env, 'POST').
|
80
|
+
expect(obj.send(:parse_action, env, 'POST')).to eql(:patch)
|
81
81
|
end
|
82
82
|
|
83
83
|
end
|
@@ -86,8 +86,8 @@ describe Restfulness::Dispatchers::Rack do
|
|
86
86
|
|
87
87
|
it "should parse headers from environment" do
|
88
88
|
res = obj.send(:prepare_headers, env)
|
89
|
-
res[:content_type].
|
90
|
-
res[:x_auth_token].
|
89
|
+
expect(res[:content_type]).to eql('application/json')
|
90
|
+
expect(res[:x_auth_token]).to eql('foobartoken')
|
91
91
|
end
|
92
92
|
end
|
93
93
|
|
@@ -96,24 +96,25 @@ describe Restfulness::Dispatchers::Rack do
|
|
96
96
|
it "should prepare request object with main fields" do
|
97
97
|
req = obj.send(:prepare_request, env)
|
98
98
|
|
99
|
-
req.uri.
|
100
|
-
req.action.
|
101
|
-
req.body.
|
102
|
-
req.headers.keys.
|
103
|
-
req.remote_ip.
|
104
|
-
req.user_agent.
|
99
|
+
expect(req.uri).to be_a(URI)
|
100
|
+
expect(req.action).to eql(:get)
|
101
|
+
expect(req.body).to be_nil
|
102
|
+
expect(req.headers.keys).to include(:x_auth_token)
|
103
|
+
expect(req.remote_ip).to eql('192.168.1.23')
|
104
|
+
expect(req.user_agent).to eql('Some Navigator')
|
105
|
+
expect(req.env).to be env
|
105
106
|
|
106
|
-
req.query.
|
107
|
-
req.query[:query].
|
107
|
+
expect(req.query).not_to be_empty
|
108
|
+
expect(req.query[:query]).to eql('test')
|
108
109
|
|
109
|
-
req.headers[:content_type].
|
110
|
+
expect(req.headers[:content_type]).to eql('application/json')
|
110
111
|
end
|
111
112
|
|
112
113
|
it "should handle the body stringio" do
|
113
114
|
env['rack.input'] = StringIO.new("Some String")
|
114
115
|
|
115
116
|
req = obj.send(:prepare_request, env)
|
116
|
-
req.body.read.
|
117
|
+
expect(req.body.read).to eql('Some String')
|
117
118
|
end
|
118
119
|
|
119
120
|
it "should rewind the body stringio" do
|
@@ -121,7 +122,7 @@ describe Restfulness::Dispatchers::Rack do
|
|
121
122
|
env['rack.input'].read
|
122
123
|
|
123
124
|
req = obj.send(:prepare_request, env)
|
124
|
-
req.body.read.
|
125
|
+
expect(req.body.read).to eql('Some String')
|
125
126
|
end
|
126
127
|
|
127
128
|
|
@@ -5,15 +5,15 @@ describe Restfulness::HTTPException do
|
|
5
5
|
describe "#initialize" do
|
6
6
|
it "should assign variables" do
|
7
7
|
obj = Restfulness::HTTPException.new(200, "payload", :message => 'foo', :headers => {})
|
8
|
-
obj.status.
|
9
|
-
obj.payload.
|
10
|
-
obj.message.
|
11
|
-
obj.headers.
|
8
|
+
expect(obj.status).to eql(200)
|
9
|
+
expect(obj.payload).to eql("payload")
|
10
|
+
expect(obj.message).to eql('foo')
|
11
|
+
expect(obj.headers).to eql({})
|
12
12
|
end
|
13
13
|
|
14
14
|
it "should use status status for message if none provided" do
|
15
15
|
obj = Restfulness::HTTPException.new(200, "payload")
|
16
|
-
obj.message.
|
16
|
+
expect(obj.message).to eql('OK')
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|