sinatra-rabbit 1.0.5 → 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
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 treats REST resource as 'collection'. Every collection then
11
- could have multiple operations. This simple DSL is then transformed into
12
- a proper modular [Sinatra::Base](http://www.sinatrarb.com/intro#Serving%20a%20Modular%20Application)
13
- application that can be launched within any [Rack](http://rack.rubyforge.org/) container (including Rails).
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 'PUT /:collection/:id'
35
+ * `operation :update` is being mapped as 'PATCH /:collection/:id'
33
36
  * `operation :destroy` is being mapped as 'DELETE /:collection/:id'
34
- * Use of Sinatra route conditions
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
- operation :stop, :if => driver.support_stop_operation? do
38
- description "Stop virtual machine"
39
- param :id, :string, :required, "Virtual Machine ID"
40
- control do
41
- # ...
42
- end
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
- * Support operation specific features
47
- * Support REST subcollections
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
- description "Blobs are binary objects stored in buckets"
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
- * Include bunch of tests to prove it work correctly
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,8 @@
1
+ %html
2
+ %head
3
+ %body
4
+ %h1=self.class.name
5
+ %ul
6
+ - self.class.collections.each do |c|
7
+ %li
8
+ %a{:href => c.docs_url }=c.name
@@ -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
@@ -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
- send(:head, full_path, {}) { status 200 } unless Rabbit.disabled? :head_routes
114
- send(:options, full_path, {}) do
115
- [200, { 'Allow' => operations.map { |o| o.operation_name }.join(',') }, '']
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
- unless block_given?
122
- return @collections.find { |c| c.collection_name == name }
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
- current_collection = BaseCollection.collection_class(name, self)
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 = ["#{@parent_collection.collection_name}"]
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.set_base_class(klass)
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;@klass;end
173
- def self.full_path;root_path + route_for(path);end
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
- def self.collections; @collections; end
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.documentation
184
- Rabbit::Documentation.for_collection(self, operations)
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 @operations.find { |o| o.operation_name == operation_name } unless block_given?
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
- operation = operation_class(self, operation_name).generate(self, operation_name, opts, &block)
200
- @operations << operation
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
- operation.set_route(root_path + route_for(path, operation_name, :id_name => @with_id || ':id'))
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
- # Define Sinatra::Base route
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
- # Generate OPTIONS routes
231
- unless Rabbit.disabled? :options_routes
232
- send(:options, root_path + route_for(path, operation_name, :member), {}) do
233
- [200, { 'Allow' => operation.params.map { |p| p.to_s }.join(',') }, '']
234
- end
235
- end
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; @operations; end
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.set_route(path)
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(method=nil)
253
- @method ||= method || BaseCollection.http_method_for(@name)
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
- http_method(@options.delete(:http_method)) if @options.has_key? :http_method
260
- @collection.features.select { |f| f.operations.map { |o| o.name}.include?(@name) }.each do |feature|
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 @options.has_key?(:with_capability)
285
- @capability.call(@options[:with_capability])
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
- return @description if text.nil?
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
- @operations.any? { |o| o.name == name }
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
- if operation_name
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] : ':id')
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
@@ -30,20 +30,20 @@ module Sinatra
30
30
  # end
31
31
  # end
32
32
  def collection(name, &block)
33
- return @collections.find { |c| c.collection_name == name } unless block_given?
34
- @collections ||= []
35
- current_collection = BaseCollection.collection_class(name)
36
- current_collection.set_base_class(self)
37
- current_collection.generate(name, &block)
38
- @collections << current_collection
39
- Sinatra::Rabbit::DSL.register_collection(current_collection)
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
@@ -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
- $:.unshift File.join(File::dirname(__FILE__), '.')
32
- require 'rabbit/base'
39
+ require_relative './rabbit/base'
@@ -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.5'
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',