sinatra-rabbit 1.0.5 → 1.0.6
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/README.md +32 -24
- data/Rakefile +44 -0
- data/lib/sinatra/docs/api.haml +8 -0
- data/lib/sinatra/docs/collection.haml +24 -0
- data/lib/sinatra/docs/operation.haml +26 -0
- data/lib/sinatra/rabbit/base.rb +140 -64
- data/lib/sinatra/rabbit/base_collection.rb +8 -5
- data/lib/sinatra/rabbit/dsl.rb +16 -8
- data/lib/sinatra/rabbit.rb +9 -2
- data/sinatra-rabbit.gemspec +4 -1
- data/tests/app_test.rb +72 -0
- data/tests/docs_test.rb +75 -0
- data/tests/dsl_test.rb +262 -0
- data/tests/fixtures.rb +149 -0
- data/tests/params_test.rb +98 -0
- metadata +13 -4
data/README.md
CHANGED
@@ -5,12 +5,15 @@ Sinatra::Rabbit
|
|
5
5
|
|
6
6
|
Sinatra::Rabbit is a [Sinatra](http://www.sinatrarb.com) extensions that
|
7
7
|
makes designing a [REST API](http://en.wikipedia.org/wiki/Representational_State_Transfer)
|
8
|
-
much easier.
|
8
|
+
much easier and more fun.
|
9
9
|
|
10
|
-
Rabbit
|
11
|
-
|
12
|
-
|
13
|
-
|
10
|
+
Rabbit maps REST resources to 'collections'. Every collection then could define
|
11
|
+
CRUD and other operations to manipulate with resource. Rabbit will handle
|
12
|
+
parameter validation and capability checks for you, so you can focus on the
|
13
|
+
structure and design of your REST API.
|
14
|
+
|
15
|
+
Once your REST API is designed, Rabbit will make it available as modular Sinatra
|
16
|
+
application thus mountable to any Rack compatible container (Rails, Padrino...)
|
14
17
|
|
15
18
|
Original Rabbit idea has been taken from the [Deltacloud API](http://deltacloud.org)
|
16
19
|
project. However this is an complete rewrite of the original Deltacloud Rabbit.
|
@@ -25,54 +28,59 @@ An example application (modular Deltacloud API) can be found here:
|
|
25
28
|
Features
|
26
29
|
-------
|
27
30
|
|
28
|
-
* Automatically generate proper path and use right HTTP method for all CRUD operations:
|
31
|
+
* Automatically generate proper path and use the right HTTP method for all CRUD operations:
|
29
32
|
* `operation :create` is being mapped as 'POST /:collection'
|
30
33
|
* `operation :index` is being mapped as 'GET /:collection'
|
31
34
|
* `operation :show` is being mapped as 'GET /:collection/:id'
|
32
|
-
* `operation :update` is being mapped as '
|
35
|
+
* `operation :update` is being mapped as 'PATCH /:collection/:id'
|
33
36
|
* `operation :destroy` is being mapped as 'DELETE /:collection/:id'
|
34
|
-
*
|
37
|
+
* `action :reboot` is being mapped as 'POST /:collection/:id/reboot'
|
38
|
+
* Automatically set the :id parameter for routes like :show or :destroy
|
39
|
+
* Automatically validates the request parameter values
|
40
|
+
* Allow to change the HTTP method (`action :reboot, :http_method => :post`)
|
41
|
+
* Allow to check if operation is supported by the current resource
|
35
42
|
<pre>
|
36
43
|
<code>
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
44
|
+
check_capability :for => lambda { |m| driver.respond_to? m }
|
45
|
+
|
46
|
+
collection :vms do
|
47
|
+
action :stop, :with_capability => :stop do
|
48
|
+
control do
|
49
|
+
# ...
|
50
|
+
end
|
51
|
+
end
|
43
52
|
end
|
44
53
|
</code>
|
45
54
|
</pre>
|
46
|
-
*
|
47
|
-
|
55
|
+
* Allow to define groups of parameters as features and make them available if
|
56
|
+
the current resource support them
|
57
|
+
* Support the 'sub-collections' that might be defined on resouce
|
48
58
|
<pre>
|
49
59
|
<code>
|
50
60
|
collection :buckets do
|
51
61
|
collection :blobs, :with_id => :bucket_id do
|
52
|
-
|
53
|
-
|
62
|
+
|
54
63
|
# GET /buckets/:bucket_id/blobs/:id
|
55
64
|
operation :show do
|
56
|
-
description "Show content of blob"
|
57
|
-
param :id, :string, :required
|
58
65
|
control { #... }
|
59
66
|
end
|
60
67
|
end
|
61
68
|
|
62
69
|
# GET /buckets/:id
|
63
70
|
operation :show do
|
64
|
-
description "Show content of bucket"
|
65
|
-
param :id, :string, :required
|
66
71
|
control { #... }
|
67
72
|
end
|
68
73
|
end
|
69
74
|
</code>
|
70
75
|
</pre>
|
71
|
-
* Generate HEAD routes for all operations and collections
|
76
|
+
* Generate HEAD routes for all operations and collections to check if the route
|
77
|
+
is available
|
72
78
|
* Generate OPTIONS routes for all operations and collections
|
73
79
|
* `OPTIONS /:collection` will return list of all operations (using 'Allow' header)
|
74
80
|
* `OPTIONS /:collection/:operation` will return list of parameters defined for given operation (using 'Allow' header)
|
75
|
-
*
|
81
|
+
* Allow to manipulate with the REST structure programmatically, to generate
|
82
|
+
documentation or collect statistics
|
83
|
+
* Include bunch of tests to prove it all works correctly
|
76
84
|
|
77
85
|
Configuration
|
78
86
|
-------
|
data/Rakefile
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
|
3
|
+
namespace :test do
|
4
|
+
|
5
|
+
desc 'Run all tests suites'
|
6
|
+
task :all do
|
7
|
+
Rake::Task['test:app'].invoke
|
8
|
+
Rake::Task['test:dsl'].invoke
|
9
|
+
Rake::Task['test:docs'].invoke
|
10
|
+
Rake::Task['test:param'].invoke
|
11
|
+
end
|
12
|
+
|
13
|
+
Rake::TestTask.new(:app) do |t|
|
14
|
+
t.test_files = FileList['tests/app_test.rb']
|
15
|
+
t.verbose = false
|
16
|
+
t.options = "-v"
|
17
|
+
end
|
18
|
+
|
19
|
+
Rake::TestTask.new(:dsl) do |t|
|
20
|
+
t.test_files = FileList['tests/dsl_test.rb']
|
21
|
+
t.verbose = false
|
22
|
+
#t.options = "-v"
|
23
|
+
end
|
24
|
+
|
25
|
+
Rake::TestTask.new(:docs) do |t|
|
26
|
+
t.test_files = FileList['tests/docs_test.rb']
|
27
|
+
t.verbose = false
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
desc "Run RSpec with code coverage"
|
33
|
+
task :coverage do
|
34
|
+
ENV["COVERAGE"] = "true"
|
35
|
+
Rake::Task["test:all"].execute
|
36
|
+
end
|
37
|
+
|
38
|
+
desc "Reinstall gem"
|
39
|
+
task :reinstall do
|
40
|
+
puts %x{rm -rf sinatra-rabbit-*.gem}
|
41
|
+
puts %x{gem uninstall sinatra-rabbit --all -I -x}
|
42
|
+
puts %x{gem build sinatra-rabbit.gemspec}
|
43
|
+
puts %x{gem install sinatra-rabbit-*.gem --local}
|
44
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
%html
|
2
|
+
%head
|
3
|
+
%body
|
4
|
+
%h1=@collection.name.gsub('Sinatra::Rabbit::', '')
|
5
|
+
%blockquote=@collection.description
|
6
|
+
.url=@collection.full_path
|
7
|
+
.features
|
8
|
+
- @collection.features.each do |f|
|
9
|
+
%p.feature=f.name
|
10
|
+
%table.operations
|
11
|
+
%thead
|
12
|
+
%tr
|
13
|
+
%th Name
|
14
|
+
%th Type
|
15
|
+
%th Valid values
|
16
|
+
%th Description
|
17
|
+
%tbody
|
18
|
+
- @collection.operations.each do |o|
|
19
|
+
%tr
|
20
|
+
%td.name
|
21
|
+
%a{:href => o.docs_url }
|
22
|
+
%td.method=o.http_method.to_s.upcase
|
23
|
+
%td.url
|
24
|
+
%a{:href=>o.full_path}=o.full_path
|
@@ -0,0 +1,26 @@
|
|
1
|
+
%html
|
2
|
+
%head
|
3
|
+
%body
|
4
|
+
%h1=@operation.name.gsub('Sinatra::Rabbit::', '')
|
5
|
+
%blockquote=@operation.description
|
6
|
+
.url=@operation.full_path
|
7
|
+
- if @operation.required_capability
|
8
|
+
%p{:class => :requires}=@operation.required_capability
|
9
|
+
- @operation.features.each do |f|
|
10
|
+
.feature
|
11
|
+
.name=f.name
|
12
|
+
%blockquote=f.description
|
13
|
+
%table.parameters
|
14
|
+
%thead
|
15
|
+
%tr
|
16
|
+
%th Name
|
17
|
+
%th Type
|
18
|
+
%th Valid values
|
19
|
+
%th Description
|
20
|
+
%tbody
|
21
|
+
- (@operation.params + @operation.features_params).each do |p|
|
22
|
+
%tr
|
23
|
+
%td{ :class => p.required? && 'required'}=p.name
|
24
|
+
%td=p.klass
|
25
|
+
%td=p.values
|
26
|
+
%td=p.description
|
data/lib/sinatra/rabbit/base.rb
CHANGED
@@ -14,14 +14,6 @@
|
|
14
14
|
# License for the specific language governing permissions and limitations
|
15
15
|
# under the License.
|
16
16
|
|
17
|
-
unless Kernel.respond_to?(:require_relative)
|
18
|
-
module Kernel
|
19
|
-
def require_relative(path)
|
20
|
-
require File.join(File.dirname(caller[0]), path.to_str)
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
17
|
require_relative './dsl'
|
26
18
|
require_relative './param'
|
27
19
|
require_relative './base_collection'
|
@@ -98,7 +90,10 @@ module Sinatra
|
|
98
90
|
#
|
99
91
|
def self.included(base)
|
100
92
|
configuration[:root_path] = '/'
|
101
|
-
base.register(DSL)
|
93
|
+
base.register(DSL) if base.respond_to? :register
|
94
|
+
base.get '/docs' do
|
95
|
+
haml File.read(File.join(File.dirname(__FILE__), '..', 'docs', 'api.haml'))
|
96
|
+
end
|
102
97
|
end
|
103
98
|
|
104
99
|
class Collection < BaseCollection
|
@@ -107,31 +102,57 @@ module Sinatra
|
|
107
102
|
|
108
103
|
def self.generate(name, parent_collection=nil, &block)
|
109
104
|
@collection_name = name.to_sym
|
110
|
-
@collections ||= []
|
111
105
|
@parent_collection = parent_collection
|
112
106
|
class_eval(&block)
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
end unless Rabbit.disabled? :options_routes
|
107
|
+
generate_head_route unless Rabbit.disabled? :head_routes
|
108
|
+
generate_option_route unless Rabbit.disabled? :options_routes
|
109
|
+
generate_docs_route unless Rabbit.disabled? :docs
|
117
110
|
self
|
118
111
|
end
|
119
112
|
|
113
|
+
def self.generate_option_route
|
114
|
+
base_class.options '/docs' + full_path do
|
115
|
+
header 'Allow' => operations.map { |o| o.operation_name }.join(',')
|
116
|
+
status 200
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.generate_head_route
|
121
|
+
base_class.head full_path do
|
122
|
+
status 200
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.docs_url
|
127
|
+
(root_path || '/') + 'docs/' + route_for(path)
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.generate_docs_route
|
131
|
+
collection = self
|
132
|
+
base_class.get docs_url do
|
133
|
+
@collection = collection
|
134
|
+
haml File.read(File.join(File.dirname(__FILE__), '..', 'docs', 'collection.haml'))
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Define new collection using the name
|
139
|
+
#
|
140
|
+
# opts[:with_id] Define :id used if the collection is a subcollection
|
141
|
+
#
|
120
142
|
def self.collection(name, opts={}, &block)
|
121
|
-
|
122
|
-
|
143
|
+
return collections.find { |c| c.collection_name == name } if not block_given?
|
144
|
+
new_collection = BaseCollection.collection_class(name, self) do |c|
|
145
|
+
c.base_class = self.base_class
|
146
|
+
c.with_id!(opts.delete(:with_id)) if opts.has_key?(:with_id)
|
147
|
+
c.no_member! if opts.has_key?(:no_member)
|
148
|
+
c.generate(name, self, &block)
|
123
149
|
end
|
124
|
-
|
125
|
-
current_collection.set_base_class(self.base_class)
|
126
|
-
current_collection.with_id!(opts.delete(:with_id)) if opts.has_key? :with_id
|
127
|
-
current_collection.no_member! if opts.has_key? :no_member
|
128
|
-
current_collection.generate(name, self, &block)
|
129
|
-
@collections << current_collection
|
150
|
+
collections << new_collection
|
130
151
|
end
|
131
152
|
|
132
153
|
def self.parent_routes
|
133
154
|
return '' if @parent_collection.nil?
|
134
|
-
route = [
|
155
|
+
route = [ @parent_collection.collection_name.to_s ]
|
135
156
|
current_parent = @parent_collection
|
136
157
|
while current_parent = current_parent.parent_collection
|
137
158
|
route << current_parent.collection_name
|
@@ -139,7 +160,7 @@ module Sinatra
|
|
139
160
|
route.reverse.join('/')+'/'
|
140
161
|
end
|
141
162
|
|
142
|
-
def self.
|
163
|
+
def self.base_class=(klass)
|
143
164
|
@klass = klass
|
144
165
|
end
|
145
166
|
|
@@ -169,26 +190,37 @@ module Sinatra
|
|
169
190
|
parent_routes + with_id_param + ((@no_member) ? '' : collection_name.to_s)
|
170
191
|
end
|
171
192
|
|
172
|
-
def self.base_class
|
173
|
-
|
193
|
+
def self.base_class
|
194
|
+
@klass
|
195
|
+
end
|
196
|
+
|
174
197
|
def self.collection_name; @collection_name; end
|
175
198
|
def self.parent_collection; @parent_collection; end
|
176
|
-
|
199
|
+
|
200
|
+
def self.subcollection?
|
201
|
+
!parent_collection.nil?
|
202
|
+
end
|
203
|
+
|
204
|
+
def self.collections
|
205
|
+
@collections ||= []
|
206
|
+
end
|
207
|
+
|
208
|
+
def self.full_path
|
209
|
+
(root_path || '') + route_for(path)
|
210
|
+
end
|
177
211
|
|
178
212
|
def self.description(text=nil)
|
179
213
|
return @description if text.nil?
|
180
214
|
@description = text
|
181
215
|
end
|
182
216
|
|
183
|
-
def self.
|
184
|
-
|
217
|
+
def self.[](obj_id)
|
218
|
+
collections.find { |c| c.collection_name == obj_id } || operation(obj_id)
|
185
219
|
end
|
186
220
|
|
187
|
-
|
188
221
|
def self.operation(operation_name, opts={}, &block)
|
189
|
-
@operations ||= []
|
190
222
|
# Return operation when no block is given
|
191
|
-
return
|
223
|
+
return operations.find { |o| o.operation_name == operation_name } unless block_given?
|
192
224
|
|
193
225
|
# Check if current operation is not already registred
|
194
226
|
if operation_registred?(operation_name)
|
@@ -196,13 +228,8 @@ module Sinatra
|
|
196
228
|
end
|
197
229
|
|
198
230
|
# Create operation class
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
# Generate HEAD routes
|
203
|
-
unless Rabbit.disabled? :head_routes
|
204
|
-
send(:head, root_path + route_for(path, operation_name, {})) { status 200 }
|
205
|
-
end
|
231
|
+
new_operation = operation_class(self, operation_name).generate(self, operation_name, opts, &block)
|
232
|
+
operations << new_operation
|
206
233
|
|
207
234
|
# Add route conditions if defined
|
208
235
|
if opts.has_key? :if
|
@@ -215,24 +242,21 @@ module Sinatra
|
|
215
242
|
end
|
216
243
|
|
217
244
|
# Make the full_path method on operation return currect operation path
|
218
|
-
|
245
|
+
new_operation.route = root_path + route_for(path, operation_name, :id_name => @with_id || ':id')
|
219
246
|
|
220
247
|
# Change the HTTP method to POST automatically for 'action' operations
|
221
|
-
if opts[:http_method]
|
222
|
-
operation.http_method(opts.delete(:http_method))
|
223
|
-
end
|
248
|
+
new_operation.http_method = opts.delete(:http_method) if opts[:http_method]
|
224
249
|
|
225
|
-
#
|
250
|
+
# Remove :with_capability from Sinatra route options
|
226
251
|
route_options = opts.clone
|
227
252
|
route_options.delete :with_capability
|
228
|
-
base_class.send(operation.http_method || http_method_for(operation_name), operation.full_path, route_options, &operation.control)
|
229
253
|
|
230
|
-
#
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
254
|
+
# Define Sinatra route and generate OPTIONS route if enabled
|
255
|
+
base_class.send(new_operation.http_method || http_method_for(operation_name), new_operation.full_path, route_options, &new_operation.control)
|
256
|
+
|
257
|
+
new_operation.generate_option_route(root_path + route_for(path, operation_name, :no_id_and_member)) unless Rabbit.disabled?(:options_routes)
|
258
|
+
new_operation.generate_head_route(root_path + route_for(path, operation_name, :member)) unless Rabbit.disabled?(:head_routes)
|
259
|
+
new_operation.generate_docs_route(new_operation.docs_url) unless Rabbit.disabled?(:doc_routes)
|
236
260
|
self
|
237
261
|
end
|
238
262
|
|
@@ -241,23 +265,71 @@ module Sinatra
|
|
241
265
|
operation(action_name, opts, &block)
|
242
266
|
end
|
243
267
|
|
244
|
-
def self.operations
|
268
|
+
def self.operations
|
269
|
+
@operations ||= []
|
270
|
+
end
|
271
|
+
|
272
|
+
def self.features_for(operation_name)
|
273
|
+
features.select { |f| f.operations.map { |o| o.name}.include?(operation_name) }
|
274
|
+
end
|
245
275
|
|
246
|
-
class Operation
|
276
|
+
class Operation < BaseCollection
|
247
277
|
|
248
|
-
def self.
|
278
|
+
def self.docs_url
|
279
|
+
@collection.root_path + ['docs', @collection.collection_name, operation_name].join('/')
|
280
|
+
end
|
281
|
+
|
282
|
+
def self.route=(path)
|
249
283
|
@operation_path = path
|
250
284
|
end
|
251
285
|
|
252
|
-
def self.http_method
|
253
|
-
@method ||=
|
286
|
+
def self.http_method
|
287
|
+
@method ||= BaseCollection.http_method_for(@name)
|
288
|
+
end
|
289
|
+
|
290
|
+
def self.http_method=(method)
|
291
|
+
@method = method
|
292
|
+
end
|
293
|
+
|
294
|
+
def self.generate_head_route(path)
|
295
|
+
@collection.base_class.head path do
|
296
|
+
status 200
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
def self.generate_docs_route(path)
|
301
|
+
operation = self
|
302
|
+
@collection.base_class.get path do
|
303
|
+
@operation = operation
|
304
|
+
haml File.read(File.join(File.dirname(__FILE__), '..', 'docs', 'operation.haml'))
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
def self.generate_option_route(path)
|
309
|
+
operation_params = params.map { |p| p.to_s }.join(',')
|
310
|
+
@collection.base_class.options path do
|
311
|
+
headers 'Allow' => operation_params
|
312
|
+
status 200
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
def self.features
|
317
|
+
@collection.features_for(operation_name)
|
318
|
+
end
|
319
|
+
|
320
|
+
def self.features_params
|
321
|
+
features.map { |f| f.operations.map { |o| o.params_array } }.flatten
|
254
322
|
end
|
255
323
|
|
256
324
|
def self.generate(collection, name, opts={}, &block)
|
257
325
|
@name, @params, @collection = name, [], collection
|
258
326
|
@options = opts
|
259
|
-
|
260
|
-
@
|
327
|
+
|
328
|
+
if @options.has_key?(:http_method)
|
329
|
+
@method = @options.delete(:http_method)
|
330
|
+
end
|
331
|
+
|
332
|
+
features.each do |feature|
|
261
333
|
if Sinatra::Rabbit.configuration[:check_features]
|
262
334
|
next unless Sinatra::Rabbit.configuration[:check_features].call(collection.collection_name, feature.name)
|
263
335
|
end
|
@@ -265,6 +337,7 @@ module Sinatra
|
|
265
337
|
instance_eval(&o.params)
|
266
338
|
end
|
267
339
|
end
|
340
|
+
|
268
341
|
if Sinatra::Rabbit::STANDARD_OPERATIONS.has_key? name
|
269
342
|
required_params = Sinatra::Rabbit::STANDARD_OPERATIONS[name][:required_params]
|
270
343
|
required_params.each do |p|
|
@@ -279,24 +352,27 @@ module Sinatra
|
|
279
352
|
def self.full_path; @operation_path; end
|
280
353
|
def self.operation_name; @name; end
|
281
354
|
|
355
|
+
def self.required_capability
|
356
|
+
@options[:with_capability]
|
357
|
+
end
|
358
|
+
|
282
359
|
def self.has_capability?
|
283
360
|
@capability ||= Sinatra::Rabbit.configuration[:check_capability]
|
284
|
-
if @capability and
|
285
|
-
@capability.call(
|
361
|
+
if @capability and required_capability
|
362
|
+
@capability.call(required_capability)
|
286
363
|
else
|
287
364
|
true
|
288
365
|
end
|
289
366
|
end
|
290
367
|
|
291
368
|
def self.description(text=nil)
|
292
|
-
|
293
|
-
@description = text
|
369
|
+
@description ||= text
|
294
370
|
end
|
295
371
|
|
296
372
|
def self.control(&block)
|
297
373
|
params_def = @params
|
298
374
|
if not has_capability?
|
299
|
-
@control = Proc.new { [412, {}, "The required capability to execute this operation is missing"] }
|
375
|
+
@control = Proc.new { [412, { 'Expect' => @options[:with_capability]}, "The required capability to execute this operation is missing"] }
|
300
376
|
else
|
301
377
|
@control ||= Proc.new do
|
302
378
|
begin
|
@@ -324,7 +400,7 @@ module Sinatra
|
|
324
400
|
private
|
325
401
|
|
326
402
|
def self.operation_registred?(name)
|
327
|
-
|
403
|
+
operations.any? { |o| o.name == name }
|
328
404
|
end
|
329
405
|
|
330
406
|
# Create an unique class name for all operations within Collection class
|
@@ -22,19 +22,21 @@ module Sinatra
|
|
22
22
|
enable :method_overide
|
23
23
|
|
24
24
|
def self.route_for(collection, operation_name=nil, member=:member)
|
25
|
-
|
25
|
+
unless operation_name.nil?
|
26
26
|
o = Sinatra::Rabbit::STANDARD_OPERATIONS[operation_name]
|
27
27
|
if o
|
28
|
+
o = o.clone
|
28
29
|
o[:member] = false if member == :no_member
|
29
30
|
o[:collection] = true if member == :no_id
|
30
|
-
if member == :no_id_and_member
|
31
|
+
if member == :no_id_and_member or member == :docs
|
31
32
|
o[:collection] = true
|
32
33
|
o[:member] = true
|
33
34
|
end
|
34
35
|
end
|
35
36
|
operation_path = (o && o[:member]) ? operation_name.to_s : nil
|
36
37
|
operation_path = operation_name.to_s unless o
|
37
|
-
id_param = (o && o[:collection]) ? nil : (member.kind_of?(Hash) ? member[:id_name] :
|
38
|
+
id_param = (o && o[:collection]) ? nil : (member.kind_of?(Hash) ? member[:id_name] : ":id")
|
39
|
+
id_param.gsub!(':', '') if id_param and member == :docs
|
38
40
|
[route_for(collection), id_param, operation_path].compact.join('/')
|
39
41
|
else
|
40
42
|
collection.to_s
|
@@ -49,10 +51,11 @@ module Sinatra
|
|
49
51
|
def self.collection_class(name, parent_class=nil)
|
50
52
|
klass = parent_class || Sinatra::Rabbit
|
51
53
|
begin
|
52
|
-
klass.const_get(name.to_s.camelize + 'Collection')
|
54
|
+
yield k = klass.const_get(name.to_s.camelize + 'Collection')
|
53
55
|
rescue NameError
|
54
|
-
klass.const_set(name.to_s.camelize + 'Collection', Class.new(Collection))
|
56
|
+
yield k = klass.const_set(name.to_s.camelize + 'Collection', Class.new(Collection))
|
55
57
|
end
|
58
|
+
return k
|
56
59
|
end
|
57
60
|
|
58
61
|
def self.root_path
|
data/lib/sinatra/rabbit/dsl.rb
CHANGED
@@ -30,20 +30,20 @@ module Sinatra
|
|
30
30
|
# end
|
31
31
|
# end
|
32
32
|
def collection(name, &block)
|
33
|
-
return
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
33
|
+
return self[name] unless block_given?
|
34
|
+
current_collection = BaseCollection.collection_class(name) do |c|
|
35
|
+
c.base_class = self
|
36
|
+
c.generate(name, &block)
|
37
|
+
end
|
38
|
+
collections << current_collection
|
39
|
+
DSL << current_collection
|
40
40
|
use current_collection
|
41
41
|
end
|
42
42
|
|
43
43
|
# Return all defined collections
|
44
44
|
#
|
45
45
|
def collections
|
46
|
-
@collections
|
46
|
+
@collections ||= []
|
47
47
|
end
|
48
48
|
|
49
49
|
def self.register_collection(c, &block)
|
@@ -51,6 +51,14 @@ module Sinatra
|
|
51
51
|
@collections << c
|
52
52
|
end
|
53
53
|
|
54
|
+
def self.<<(c)
|
55
|
+
register_collection(c)
|
56
|
+
end
|
57
|
+
|
58
|
+
def [](collection)
|
59
|
+
collections.find { |c| c.collection_name == collection }
|
60
|
+
end
|
61
|
+
|
54
62
|
end
|
55
63
|
end
|
56
64
|
end
|
data/lib/sinatra/rabbit.rb
CHANGED
@@ -22,11 +22,18 @@ unless "".respond_to? :camelize
|
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
25
|
+
unless Kernel.respond_to?(:require_relative)
|
26
|
+
module Kernel
|
27
|
+
def require_relative(path)
|
28
|
+
require File.join(File.dirname(caller[0]), path.to_str)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
25
33
|
class << Sinatra::Base
|
26
34
|
def options(path, opts={}, &bk)
|
27
35
|
route 'OPTIONS', path, opts, &bk
|
28
36
|
end
|
29
37
|
end
|
30
38
|
|
31
|
-
|
32
|
-
require 'rabbit/base'
|
39
|
+
require_relative './rabbit/base'
|
data/sinatra-rabbit.gemspec
CHANGED
@@ -29,12 +29,15 @@ Gem::Specification.new do |s|
|
|
29
29
|
a simple REST API using easy to undestand DSL.
|
30
30
|
EOF
|
31
31
|
|
32
|
-
s.version = '1.0.
|
32
|
+
s.version = '1.0.6'
|
33
33
|
s.date = Time.now
|
34
34
|
s.summary = %q{Sinatra REST API DSL}
|
35
35
|
s.files = FileList[
|
36
36
|
'lib/sinatra/*.rb',
|
37
37
|
'lib/sinatra/rabbit/*.rb',
|
38
|
+
'lib/sinatra/docs/*.haml',
|
39
|
+
'tests/*.rb',
|
40
|
+
'Rakefile',
|
38
41
|
'LICENSE',
|
39
42
|
'README.md',
|
40
43
|
'sinatra-rabbit.gemspec',
|