restfulness 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.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +342 -0
- data/Rakefile +1 -0
- data/example/Gemfile +5 -0
- data/example/README.md +42 -0
- data/example/app.rb +42 -0
- data/example/config.ru +5 -0
- data/lib/restfulness.rb +36 -0
- data/lib/restfulness/application.rb +83 -0
- data/lib/restfulness/dispatcher.rb +14 -0
- data/lib/restfulness/dispatchers/rack.rb +91 -0
- data/lib/restfulness/exceptions.rb +17 -0
- data/lib/restfulness/log_formatters/quiet_formatter.rb +7 -0
- data/lib/restfulness/log_formatters/verbose_formatter.rb +16 -0
- data/lib/restfulness/path.rb +46 -0
- data/lib/restfulness/request.rb +81 -0
- data/lib/restfulness/resource.rb +111 -0
- data/lib/restfulness/response.rb +57 -0
- data/lib/restfulness/route.rb +48 -0
- data/lib/restfulness/router.rb +27 -0
- data/lib/restfulness/statuses.rb +71 -0
- data/lib/restfulness/version.rb +3 -0
- data/restfulness.gemspec +29 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/unit/application_spec.rb +91 -0
- data/spec/unit/dispatcher_spec.rb +14 -0
- data/spec/unit/dispatchers/rack_spec.rb +34 -0
- data/spec/unit/exceptions_spec.rb +21 -0
- data/spec/unit/path_spec.rb +91 -0
- data/spec/unit/request_spec.rb +129 -0
- data/spec/unit/resource_spec.rb +245 -0
- data/spec/unit/response_spec.rb +65 -0
- data/spec/unit/route_spec.rb +160 -0
- data/spec/unit/router_spec.rb +103 -0
- metadata +189 -0
@@ -0,0 +1,34 @@
|
|
1
|
+
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe Restfulness::Dispatchers::Rack do
|
5
|
+
|
6
|
+
let :klass do
|
7
|
+
Restfulness::Dispatchers::Rack
|
8
|
+
end
|
9
|
+
|
10
|
+
let :app do
|
11
|
+
Class.new(Restfulness::Application).new
|
12
|
+
end
|
13
|
+
|
14
|
+
let :obj do
|
15
|
+
klass.new(app)
|
16
|
+
end
|
17
|
+
|
18
|
+
let :env do
|
19
|
+
{
|
20
|
+
'REQUEST_METHOD' => 'GET',
|
21
|
+
'SCRIPT_NAME' => 'projects',
|
22
|
+
'PATH_INFO' => 'projects',
|
23
|
+
'QUERY_STRING' => '',
|
24
|
+
'SERVER_NAME' => 'localhost',
|
25
|
+
'SERVER_PORT' => '3000',
|
26
|
+
'HTTP_CONTENT_TYPE' => 'application/json'
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "#" do
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Restfulness::HTTPException do
|
4
|
+
|
5
|
+
describe "#initialize" do
|
6
|
+
it "should assign variables" do
|
7
|
+
obj = Restfulness::HTTPException.new(200, "payload", :message => 'foo', :headers => {})
|
8
|
+
obj.code.should eql(200)
|
9
|
+
obj.payload.should eql("payload")
|
10
|
+
obj.message.should eql('foo')
|
11
|
+
obj.headers.should eql({})
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should use status code for message if none provided" do
|
15
|
+
obj = Restfulness::HTTPException.new(200, "payload")
|
16
|
+
obj.message.should eql('OK')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Restfulness::Path do
|
4
|
+
|
5
|
+
class PathResource < Restfulness::Resource
|
6
|
+
end
|
7
|
+
|
8
|
+
let :klass do
|
9
|
+
Restfulness::Path
|
10
|
+
end
|
11
|
+
let :route_class do
|
12
|
+
Restfulness::Route
|
13
|
+
end
|
14
|
+
let :resource_class do
|
15
|
+
PathResource
|
16
|
+
end
|
17
|
+
let :simple_route do
|
18
|
+
route_class.new('project', resource_class)
|
19
|
+
end
|
20
|
+
let :complex_route do
|
21
|
+
route_class.new('project', :project_id, 'status', resource_class)
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "#initialize" do
|
25
|
+
|
26
|
+
it "should assign route" do
|
27
|
+
obj = klass.new(simple_route, '/project')
|
28
|
+
obj.route.should eql(simple_route)
|
29
|
+
end
|
30
|
+
|
31
|
+
context "simple paths" do
|
32
|
+
it "should prepare basic path" do
|
33
|
+
obj = klass.new(simple_route, '/project')
|
34
|
+
obj.components.should eql(['project'])
|
35
|
+
obj.params[:id].should be_nil
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should prepare irregular path components" do
|
39
|
+
obj = klass.new(simple_route, '/project/')
|
40
|
+
obj.components.should eql(['project'])
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should include id" do
|
44
|
+
obj = klass.new(simple_route, '/project/12345')
|
45
|
+
obj.components.should eql(['project', '12345'])
|
46
|
+
obj.params[:id].should eql('12345')
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
context "complex paths" do
|
52
|
+
it "should prepare path" do
|
53
|
+
obj = klass.new(complex_route, '/project/12345/status')
|
54
|
+
obj.components.should eql(['project', '12345', 'status'])
|
55
|
+
obj.params[:project_id].should eql('12345')
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should handle end id" do
|
59
|
+
obj = klass.new(complex_route, '/project/12345/status/23456')
|
60
|
+
obj.components.should eql(['project', '12345', 'status', '23456'])
|
61
|
+
obj.params[:project_id].should eql('12345')
|
62
|
+
obj.params[:id].should eql('23456')
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe "#to_s" do
|
68
|
+
it "should provide simple string" do
|
69
|
+
obj = klass.new(complex_route, '/project/12345/status/23456')
|
70
|
+
obj.to_s.should eql('/project/12345/status/23456')
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "#[]" do
|
75
|
+
let :obj do
|
76
|
+
obj = klass.new(complex_route, '/project/12345/status/23456')
|
77
|
+
end
|
78
|
+
it "should grant access to components by index" do
|
79
|
+
obj[0].should eql('project')
|
80
|
+
obj[1].should eql('12345')
|
81
|
+
obj[2].should eql('status')
|
82
|
+
obj[3].should eql('23456')
|
83
|
+
obj[4].should be_nil
|
84
|
+
end
|
85
|
+
it "should grant access to path parameters by symbol" do
|
86
|
+
obj[:project_id].should eql('12345')
|
87
|
+
obj[:id].should eql('23456')
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Restfulness::Request do
|
4
|
+
|
5
|
+
let :app do
|
6
|
+
Class.new(Restfulness::Application) do
|
7
|
+
routes do
|
8
|
+
# nothing
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
let :klass do
|
14
|
+
Restfulness::Request
|
15
|
+
end
|
16
|
+
|
17
|
+
let :obj do
|
18
|
+
klass.new(app)
|
19
|
+
end
|
20
|
+
|
21
|
+
class RequestResource < Restfulness::Resource
|
22
|
+
end
|
23
|
+
let :resource do
|
24
|
+
RequestResource
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
describe "#initialize" do
|
29
|
+
it "should prepare basic objects" do
|
30
|
+
obj.action.should be_nil
|
31
|
+
obj.headers.should eql({})
|
32
|
+
obj.body.should be_nil
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "#uri=" do
|
37
|
+
it "should convert URL into URI" do
|
38
|
+
obj.uri = "https://example.com/project/12345"
|
39
|
+
obj.uri.path.should eql("/project/12345")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "#path" do
|
44
|
+
it "should be nil if there is no route" do
|
45
|
+
obj.stub(:route).and_return(nil)
|
46
|
+
obj.path.should be_nil
|
47
|
+
end
|
48
|
+
|
49
|
+
context "with route" do
|
50
|
+
let :path do
|
51
|
+
obj.uri = "https://example.com/project/12345"
|
52
|
+
route = Restfulness::Route.new('project', resource)
|
53
|
+
obj.stub(:route).and_return(route)
|
54
|
+
obj.path
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should build path" do
|
58
|
+
path.to_s.should eql('/project/12345')
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should re-use same path object" do
|
62
|
+
path.object_id.should eql(obj.path.object_id)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe "#route" do
|
68
|
+
it "should ask the router for a route" do
|
69
|
+
obj.uri = "https://example.com/project/12345"
|
70
|
+
route = double(:Route)
|
71
|
+
app.router.should_receive(:route_for).with(obj.uri.path).and_return(route)
|
72
|
+
obj.route.should eql(route)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe "#query" do
|
77
|
+
it "should parse uri with query" do
|
78
|
+
obj.uri = "https://example.com/project/12345?foo=bar&test=23"
|
79
|
+
obj.query.should be_a(HashWithIndifferentAccess)
|
80
|
+
obj.query[:foo].should eql('bar')
|
81
|
+
obj.query[:test].to_i.should eql(23)
|
82
|
+
end
|
83
|
+
|
84
|
+
it "should handle uri with empty query" do
|
85
|
+
obj.uri = "https://example.com/project/12345"
|
86
|
+
obj.query.should be_empty
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should handle complex query items" do
|
90
|
+
obj.uri = "https://example.com/project/12345?foo[]=bar&foo[]=bar2&hash[a]=b&hash[b]=c"
|
91
|
+
obj.query[:foo].should eql(['bar', 'bar2'])
|
92
|
+
obj.query[:hash].should be_a(HashWithIndifferentAccess)
|
93
|
+
obj.query[:hash][:a].should eql('b')
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe "#params" do
|
98
|
+
it "should not return anything for empty body" do
|
99
|
+
obj.stub(:body).and_return(nil)
|
100
|
+
obj.params.should be_nil
|
101
|
+
end
|
102
|
+
it "should raise 406 error if no content type" do
|
103
|
+
obj.headers[:content_type] = nil
|
104
|
+
obj.body = "{\"foo\":\"bar\"}"
|
105
|
+
expect {
|
106
|
+
obj.params
|
107
|
+
}.to raise_error(Restfulness::HTTPException, "Not Acceptable")
|
108
|
+
end
|
109
|
+
it "should decode a JSON body" do
|
110
|
+
obj.headers[:content_type] = "application/json"
|
111
|
+
obj.body = "{\"foo\":\"bar\"}"
|
112
|
+
obj.params['foo'].should eql('bar')
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
describe "method helpers" do
|
117
|
+
it "should respond to method questions" do
|
118
|
+
[:get?, :post?, :put?, :delete?, :head?, :options?].each do |q|
|
119
|
+
obj.should respond_to(q)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
it "should suggest type of method" do
|
123
|
+
obj.action = :get
|
124
|
+
obj.get?.should be_true
|
125
|
+
obj.post?.should be_false
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
@@ -0,0 +1,245 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Restfulness::Resource do
|
4
|
+
|
5
|
+
class GetResource < Restfulness::Resource
|
6
|
+
def get
|
7
|
+
'result'
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class GetPostResource < Restfulness::Resource
|
12
|
+
def get
|
13
|
+
'result'
|
14
|
+
end
|
15
|
+
def post
|
16
|
+
'post'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
let :app do
|
21
|
+
Class.new(Restfulness::Application) do
|
22
|
+
routes do
|
23
|
+
# empty
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
let :request do
|
28
|
+
Restfulness::Request.new(app)
|
29
|
+
end
|
30
|
+
let :response do
|
31
|
+
Restfulness::Response.new(request)
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "#initialize" do
|
35
|
+
let :resource do
|
36
|
+
GetResource
|
37
|
+
end
|
38
|
+
it "should assign request and response" do
|
39
|
+
obj = resource.new(request, response)
|
40
|
+
obj.request.should eql(request)
|
41
|
+
obj.response.should eql(response)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "#options" do
|
46
|
+
let :resource do
|
47
|
+
GetPostResource
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should return list of supported methods" do
|
51
|
+
obj = resource.new(request, response)
|
52
|
+
obj.options.should be_nil
|
53
|
+
response.headers['Allow'].should eql('GET, POST, OPTIONS')
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "#call" do
|
58
|
+
let :resource do
|
59
|
+
GetResource
|
60
|
+
end
|
61
|
+
it "should perform action" do
|
62
|
+
request.action = :get
|
63
|
+
obj = resource.new(request, response)
|
64
|
+
obj.should_receive(:get).and_return('res')
|
65
|
+
obj.call.should eql('res')
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
describe "#method_allowed?" do
|
71
|
+
let :resource do
|
72
|
+
GetPostResource
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should be true on valid method" do
|
76
|
+
request.action = :get
|
77
|
+
obj = resource.new(request, response)
|
78
|
+
obj.method_allowed?.should be_true
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should be false on invalid method" do
|
82
|
+
request.action = :put
|
83
|
+
obj = resource.new(request, response)
|
84
|
+
obj.method_allowed?.should be_false
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
describe "basic callback responses" do
|
89
|
+
let :resource do
|
90
|
+
GetPostResource
|
91
|
+
end
|
92
|
+
|
93
|
+
let :obj do
|
94
|
+
request.action = :get
|
95
|
+
obj = resource.new(request, response)
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should all be true for questions" do
|
99
|
+
obj.exists?.should be_true
|
100
|
+
obj.authorized?.should be_true
|
101
|
+
obj.allowed?.should be_true
|
102
|
+
end
|
103
|
+
|
104
|
+
it "should be nil for values" do
|
105
|
+
obj.last_modified.should be_nil
|
106
|
+
obj.etag.should be_nil
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
describe "#check_callbacks" do
|
111
|
+
let :resource do
|
112
|
+
Class.new(GetPostResource) do
|
113
|
+
def head; nil; end
|
114
|
+
def put; nil; end
|
115
|
+
def delete; nil; end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
let :obj do
|
120
|
+
request.action = :get
|
121
|
+
obj = resource.new(request, response)
|
122
|
+
end
|
123
|
+
|
124
|
+
it "should all be good by default" do
|
125
|
+
expect {
|
126
|
+
obj.check_callbacks
|
127
|
+
}.to_not raise_error
|
128
|
+
end
|
129
|
+
|
130
|
+
it "should raise error on invalid method" do
|
131
|
+
obj.stub(:method_allowed?).and_return(false)
|
132
|
+
expect {
|
133
|
+
obj.check_callbacks
|
134
|
+
}.to raise_error(Restfulness::HTTPException, "Method Not Allowed")
|
135
|
+
end
|
136
|
+
|
137
|
+
[:head, :get, :put, :delete].each do |action|
|
138
|
+
it "should raise error when not exists for #{action.to_s.upcase}" do
|
139
|
+
request.action = action
|
140
|
+
obj.stub(:exists?).and_return(false)
|
141
|
+
expect {
|
142
|
+
obj.check_callbacks
|
143
|
+
}.to raise_error(Restfulness::HTTPException, "Resource Not Found")
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
[:post].each do |action|
|
148
|
+
it "should not check exists? for #{action.to_s.upcase}" do
|
149
|
+
obj.request.action = action
|
150
|
+
obj.should_not_receive(:exists?)
|
151
|
+
obj.check_callbacks
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
it "should raise error when not authorized" do
|
156
|
+
obj.stub(:authorized?).and_return(false)
|
157
|
+
expect {
|
158
|
+
obj.check_callbacks
|
159
|
+
}.to raise_error(Restfulness::HTTPException, "Unauthorized")
|
160
|
+
end
|
161
|
+
|
162
|
+
it "should raise error when not allowed" do
|
163
|
+
obj.stub(:allowed?).and_return(false)
|
164
|
+
expect {
|
165
|
+
obj.check_callbacks
|
166
|
+
}.to raise_error(Restfulness::HTTPException, "Forbidden")
|
167
|
+
end
|
168
|
+
|
169
|
+
describe "with etag" do
|
170
|
+
it "should raise error when equal" do
|
171
|
+
obj.stub(:etag).and_return('sometag')
|
172
|
+
request.headers[:if_none_match] = 'sometag'
|
173
|
+
expect {
|
174
|
+
obj.check_callbacks
|
175
|
+
}.to raise_error(Restfulness::HTTPException, "Not Modified")
|
176
|
+
end
|
177
|
+
|
178
|
+
it "should continue if not equal" do
|
179
|
+
obj.stub(:etag).and_return('sometag')
|
180
|
+
request.headers[:if_none_match] = 'someoldtag'
|
181
|
+
expect {
|
182
|
+
obj.check_callbacks
|
183
|
+
}.to_not raise_error
|
184
|
+
end
|
185
|
+
|
186
|
+
it "should not be called unless action is :get or :head" do
|
187
|
+
obj.should_not_receive(:etag)
|
188
|
+
request.headers[:if_none_match] = 'sometag'
|
189
|
+
[:post, :put, :delete].each do |action|
|
190
|
+
request.action = action
|
191
|
+
obj.check_callbacks
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
describe "with if modified" do
|
197
|
+
it "should raise error when equal" do
|
198
|
+
time = Time.now
|
199
|
+
obj.stub(:last_modified).and_return(time)
|
200
|
+
request.headers[:if_modified_since] = time.to_s
|
201
|
+
expect {
|
202
|
+
obj.check_callbacks
|
203
|
+
}.to raise_error(Restfulness::HTTPException, "Not Modified")
|
204
|
+
end
|
205
|
+
|
206
|
+
it "should continue if not equal" do
|
207
|
+
time = Time.now
|
208
|
+
obj.stub(:last_modified).and_return(time)
|
209
|
+
request.headers[:if_modified_since] = (time - 60).to_s
|
210
|
+
expect {
|
211
|
+
obj.check_callbacks
|
212
|
+
}.to_not raise_error
|
213
|
+
end
|
214
|
+
|
215
|
+
it "should not be called unless action is :get or :head" do
|
216
|
+
obj.should_not_receive(:last_modified)
|
217
|
+
request.headers[:if_modified_since] = 'somedate'
|
218
|
+
[:post, :put, :delete].each do |action|
|
219
|
+
request.action = action
|
220
|
+
obj.check_callbacks
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
describe "#error" do
|
228
|
+
|
229
|
+
class Get418Resource < Restfulness::Resource
|
230
|
+
def get
|
231
|
+
error(418, {})
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
it "should raise a new exception" do
|
236
|
+
klass = Get418Resource
|
237
|
+
obj = klass.new(request, response)
|
238
|
+
expect {
|
239
|
+
obj.get
|
240
|
+
}.to raise_error(Restfulness::HTTPException, "I'm A Teapot")
|
241
|
+
end
|
242
|
+
|
243
|
+
end
|
244
|
+
|
245
|
+
end
|