sinatra-rabbit 0.9 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -5,7 +5,7 @@ Sinatra::Rabbit is a [Sinatra](http://www.sinatrarb.com) extensions that
5
5
  makes designing a [REST API](http://en.wikipedia.org/wiki/Representational_State_Transfer)
6
6
  much easier.
7
7
 
8
- Rabbit threat REST resource as 'collection'. Every collection then
8
+ Rabbit treats REST resource as 'collection'. Every collection then
9
9
  could have multiple operations. This simple DSL is then transformed into
10
10
  a proper modular [Sinatra::Base](http://www.sinatrarb.com/intro#Serving%20a%20Modular%20Application)
11
11
  application that can be launched within any [Rack](http://rack.rubyforge.org/) container (including Rails).
@@ -15,6 +15,129 @@ project. However this is an complete rewrite of the original Deltacloud Rabbit.
15
15
  The goal is to make Rabbit highly modular, so it can be used in other projects
16
16
  as well as in Deltacloud.
17
17
 
18
+ Full documentation: [rubydoc.info/github/mifo/sinatra-rabbit](http://rubydoc.info/github/mifo/sinatra-rabbit/master)
19
+
20
+ Features
21
+ -------
22
+
23
+ * Automatically generate proper path and use right HTTP method for all CRUD operations:
24
+ * `operation :create` is being mapped as 'POST /:collection'
25
+ * `operation :index` is being mapped as 'GET /:collection'
26
+ * `operation :show` is being mapped as 'GET /:collection/:id'
27
+ * `operation :update` is being mapped as 'PUT /:collection/:id'
28
+ * `operation :destroy` is being mapped as 'DELETE /:collection/:id'
29
+ * Use of Sinatra route conditions
30
+ <pre>
31
+ <code>
32
+ operation :stop, :if => driver.support_stop_operation? do
33
+ description "Stop virtual machine"
34
+ param :id, :string, :required, "Virtual Machine ID"
35
+ control do
36
+ # ...
37
+ end
38
+ end
39
+ </code>
40
+ </pre>
41
+ * Support operation specific features
42
+ * Support REST subcollections
43
+ <pre>
44
+ <code>
45
+ collection :buckets do
46
+ collection :blobs, :with_id => :bucket_id do
47
+ description "Blobs are binary objects stored in buckets"
48
+
49
+ # GET /buckets/:bucket_id/blobs/:id
50
+ operation :show do
51
+ description "Show content of blob"
52
+ param :id, :string, :required
53
+ control { #... }
54
+ end
55
+ end
56
+
57
+ # GET /buckets/:id
58
+ operation :show do
59
+ description "Show content of bucket"
60
+ param :id, :string, :required
61
+ control { #... }
62
+ end
63
+ end
64
+ </code>
65
+ </pre>
66
+ * Generate HEAD routes for all operations and collections
67
+ * Generate OPTIONS routes for all operations and collections
68
+ * `OPTIONS /:collection` will return list of all operations (using 'Allow' header)
69
+ * `OPTIONS /:collection/:operation` will return list of parameters defined for given operation (using 'Allow' header)
70
+ * Include bunch of tests to prove it work correctly
71
+
72
+ Configuration
73
+ -------
74
+
75
+ Rabbit behavior can be customized using `configure` method:
76
+
77
+ Sinatra::Rabbit.configure do
78
+ disable :head_routes # This will disable automatic generation of HEAD routes
79
+ disable :options_routes # This will disable automatic generation of OPTIONS routes
80
+ disable :documentation # This will disable automatic generation of documentation routes
81
+ end
82
+
83
+ Operation specific 'features'
84
+ --------
85
+
86
+ In some specific use-cases, you would like to have different set of parameters
87
+ for operation, based on some condition of your system.
88
+ To give you and example, in Deltacloud API project, we have support for
89
+ different drivers. The project aims to give user full cross-cloud abstraction
90
+ layer, however for some cloud providers the full abstraction is not possible and
91
+ in this case we need to handle this using 'features'. It mean that some
92
+ particular operations can have 'feature' assigned. If this 'feature' is
93
+ supported by underlaying driver, then we add new parameters to that operations.
94
+
95
+ In sinatra-rabbit, you can define features for every model in this way:
96
+
97
+ class Sample < Sinatra::Base
98
+ include Sinatra::Rabbit
99
+ include Sinatra::Rabbit::Features
100
+
101
+ features do
102
+ feature :user_name, :for => :instances do
103
+ operation :create do
104
+ param :user_name, :string, :optional, "Allow to define user-defined name"
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ You can also wrap features into 'if' block or put some conditions on top
111
+ of this block to make it more flexible.
112
+
113
+ Tests
114
+ ------
115
+
116
+ Project include set of minitest files located under 'tests/' directory. You can
117
+ launch them by typing:
118
+
119
+ $ rake test:all
120
+
121
+ If you want to run just DSL tests:
122
+
123
+ $ rake test:dsl
124
+
125
+ If you want to test if Sinatra::Base works on your system correctly with Rabbit:
126
+
127
+ $ rake test:app
128
+
129
+ If you want to see code coverage, you must install the *simplecov* gem:
130
+
131
+ $ gem install simplecov
132
+ $ rake coverage
133
+
134
+ NOTE: The coverage reports are supported only on Ruby 1.9
135
+
136
+ TODO
137
+ -------
138
+
139
+ * Add documentation generation and routes for Rabbit based applications
140
+
18
141
  Installation
19
142
  -------
20
143
 
@@ -32,7 +155,18 @@ Usage
32
155
  collection :images do
33
156
  description "Images description"
34
157
 
35
- operation :index do
158
+ collection :subcollection do
159
+ description "Images subcollection"
160
+
161
+ operation :index do
162
+ description "Hello world"
163
+ control do
164
+ status 200
165
+ end
166
+ end
167
+ end
168
+
169
+ operation :index, :if => lambda { 1 == 1} do
36
170
  description "Index operation description"
37
171
  control do
38
172
  status 200
@@ -50,6 +184,7 @@ Usage
50
184
  "Hey #{params[:id]}"
51
185
  end
52
186
  end
187
+ end
53
188
  end
54
189
 
55
190
  MySinatraApp.run!
@@ -22,5 +22,11 @@ unless "".respond_to? :camelize
22
22
  end
23
23
  end
24
24
 
25
+ class << Sinatra::Base
26
+ def options(path, opts={}, &bk)
27
+ route 'OPTIONS', path, opts, &bk
28
+ end
29
+ end
30
+
25
31
  $:.unshift File.join(File::dirname(__FILE__), '.')
26
32
  require 'rabbit/base'
@@ -14,25 +14,74 @@
14
14
  # License for the specific language governing permissions and limitations
15
15
  # under the License.
16
16
 
17
- require 'sinatra/rabbit/dsl'
18
- require 'sinatra/rabbit/param'
19
- require 'sinatra/rabbit/base_collection'
20
- require 'sinatra/rabbit/validator'
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
+ require_relative './dsl'
26
+ require_relative './param'
27
+ require_relative './base_collection'
28
+ require_relative './validator'
29
+ require_relative './features'
21
30
 
22
31
  module Sinatra
23
32
  module Rabbit
24
33
 
25
34
  STANDARD_OPERATIONS = {
26
- :create => { :member => false, :method => :post, :collection => true },
27
- :show => { :member => false, :method => :get },
28
- :destroy => { :member => false, :method => :delete },
29
- :index => { :member => false, :method => :get, :collection => true }
35
+ :create => {
36
+ :member => false,
37
+ :method => :post,
38
+ :collection => true
39
+ },
40
+ :show => {
41
+ :member => false,
42
+ :method => :get,
43
+ :required_params => [ :id ]
44
+ },
45
+ :destroy => {
46
+ :member => false,
47
+ :method => :delete,
48
+ :required_params => [ :id ]
49
+ },
50
+ :index => {
51
+ :member => false,
52
+ :method => :get,
53
+ :collection => true
54
+ }
30
55
  }
31
56
 
32
57
  def self.configure(&block)
33
58
  instance_eval(&block)
34
59
  end
35
60
 
61
+ def self.configuration
62
+ @configuration || {}
63
+ end
64
+
65
+ def self.enable(property)
66
+ @configuration[property] = true
67
+ end
68
+
69
+ def self.enabled?(property)
70
+ !@configuration[property].nil? and @configuration[property] == true
71
+ end
72
+
73
+ def self.disabled?(property)
74
+ !@configuration[property].nil? and @configuration[property] == false
75
+ end
76
+
77
+ def self.disable(property)
78
+ @configuration[property] = false
79
+ end
80
+
81
+ def self.set(property, value)
82
+ @configuration[property] = value
83
+ end
84
+
36
85
  # Automatically register the DSL to Sinatra::Base if this
37
86
  # module is included:
38
87
  #
@@ -43,30 +92,126 @@ module Sinatra
43
92
  # end
44
93
  #
45
94
  def self.included(base)
95
+ @configuration ||= {
96
+ :root_path => '/'
97
+ }
46
98
  base.register(DSL)
47
99
  end
48
100
 
49
101
  class Collection < BaseCollection
50
102
 
51
- def self.generate(name, &block)
52
- @name = name.to_sym
103
+ include DSL
104
+
105
+ def self.generate(name, parent_collection=nil, &block)
106
+ @collection_name = name.to_sym
107
+ @collections ||= []
108
+ @parent_collection = parent_collection
53
109
  class_eval(&block)
110
+ send(:head, full_path, {}) { status 200 } unless Rabbit.disabled? :head_routes
111
+ send(:options, full_path, {}) do
112
+ [200, { 'Allow' => operations.map { |o| o.operation_name }.join(',') }, '']
113
+ end unless Rabbit.disabled? :options_routes
54
114
  self
55
115
  end
56
116
 
57
- def self.path; @name; end
117
+ def self.collection(name, opts={}, &block)
118
+ unless block_given?
119
+ return @collections.find { |c| c.collection_name == name }
120
+ end
121
+ current_collection = BaseCollection.collection_class(name, self)
122
+ current_collection.set_base_class(self.base_class)
123
+ current_collection.with_id!(opts.delete(:with_id)) if opts.has_key? :with_id
124
+ current_collection.generate(name, self, &block)
125
+ @collections << current_collection
126
+ end
127
+
128
+ def self.parent_routes
129
+ return '' if @parent_collection.nil?
130
+ route = ["#{@parent_collection.collection_name}"]
131
+ current_parent = @parent_collection
132
+ while current_parent = current_parent.parent_collection
133
+ route << current_parent.collection_name
134
+ end
135
+ route.reverse.join('/')+'/'
136
+ end
137
+
138
+ def self.set_base_class(klass)
139
+ @klass = klass
140
+ end
141
+
142
+ def self.control(*args)
143
+ raise "The 'control' statement must be used only within context of Operation"
144
+ end
145
+
146
+ def self.features
147
+ return [] unless base_class.respond_to? :features
148
+ base_class.features.select { |f| f.collection == collection_name }
149
+ end
150
+
151
+ def self.with_id!(id)
152
+ @with_id = ":#{id}"
153
+ end
154
+
155
+ def self.path
156
+ with_id_param = @with_id.nil? ? '' : ':id/'
157
+ parent_routes + with_id_param + collection_name.to_s
158
+ end
58
159
 
59
- def self.description(text)
160
+ def self.base_class;@klass;end
161
+ def self.full_path;root_path + route_for(path);end
162
+ def self.collection_name; @collection_name; end
163
+ def self.parent_collection; @parent_collection; end
164
+ def self.collections; @collections; end
165
+
166
+ def self.description(text=nil)
167
+ return @description if text.nil?
60
168
  @description = text
61
169
  end
62
170
 
63
- def self.operation(operation_name, &block)
171
+ def self.documentation
172
+ Rabbit::Documentation.for_collection(self, operations)
173
+ end
174
+
175
+ def self.operation(operation_name, opts={}, &block)
64
176
  @operations ||= []
177
+ # Return operation when no block is given
178
+ return @operations.find { |o| o.operation_name == operation_name } unless block_given?
179
+
180
+ # Check if current operation is not already registred
65
181
  if operation_registred?(operation_name)
66
- raise "Operation #{operation_name} already register in #{self.name} collection"
182
+ raise "Operation #{operation_name} already registered in #{self.name} collection"
183
+ end
184
+
185
+ # Create operation class
186
+ @operations << (operation = operation_class(self, operation_name).generate(self, operation_name, &block))
187
+
188
+ # Generate HEAD routes
189
+ unless Rabbit.disabled? :head_routes
190
+ send(:head, root_path + route_for(path, operation_name, {})) { status 200 }
191
+ end
192
+
193
+ # Add route conditions if defined
194
+ if opts.has_key? :if
195
+ base_class.send(:set, :if_true) do |value|
196
+ condition do
197
+ (value.kind_of?(Proc) ? value.call : value) == true
198
+ end
199
+ end
200
+ opts[:if_true] = opts.delete(:if)
201
+ end
202
+
203
+ # Make the full_path method on operation return currect operation path
204
+ operation.set_route(root_path + route_for(path, operation_name, :id_name => @with_id || ':id'))
205
+
206
+ # Define Sinatra::Base route
207
+ base_class.send(http_method_for(operation_name), operation.full_path, opts, &operation.control)
208
+
209
+ # Generate OPTIONS routes
210
+ unless Rabbit.disabled? :options_routes
211
+ send(:options, root_path + route_for(path, operation_name, :member), {}) do
212
+ [200, { 'Allow' => operation.params.map { |p| p.to_s }.join(',') }, '']
213
+ end
67
214
  end
68
- operation = operation_class(self, operation_name).generate(self, operation_name, &block)
69
- send(http_method_for(operation_name), route_for(path, operation_name), {}, &operation.control)
70
215
  self
71
216
  end
72
217
 
@@ -74,13 +219,33 @@ module Sinatra
74
219
 
75
220
  class Operation
76
221
 
222
+ def self.set_route(path)
223
+ @operation_path = path
224
+ end
225
+
77
226
  def self.generate(collection, name, &block)
78
- @name, @params = name, []
227
+ @name, @params, @collection = name, [], collection
228
+ @collection.features.select { |f| f.operations.map { |o| o.name}.include?(@name) }.each do |feature|
229
+ feature.operations.each do |o|
230
+ instance_eval(&o.params)
231
+ end
232
+ end
233
+ if Sinatra::Rabbit::STANDARD_OPERATIONS.has_key? name
234
+ required_params = Sinatra::Rabbit::STANDARD_OPERATIONS[name][:required_params]
235
+ required_params.each do |p|
236
+ param p, :required, :string, "The #{p} parameter"
237
+ end unless required_params.nil?
238
+ end
79
239
  class_eval(&block)
240
+ description "#{name.to_s.capitalize} operation on #{@collection.name} collection" if description.nil?
80
241
  self
81
242
  end
82
243
 
83
- def self.description(text)
244
+ def self.full_path; @operation_path; end
245
+ def self.operation_name; @name; end
246
+
247
+ def self.description(text=nil)
248
+ return @description if text.nil?
84
249
  @description = text
85
250
  end
86
251
 
@@ -91,8 +256,7 @@ module Sinatra
91
256
  Rabbit::Validator.validate!(params, params_def)
92
257
  rescue => e
93
258
  if e.kind_of? Rabbit::Validator::ValidationError
94
- status e.http_status_code
95
- halt
259
+ halt e.http_status_code, e.message
96
260
  else
97
261
  raise e
98
262
  end
@@ -102,8 +266,11 @@ module Sinatra
102
266
  end
103
267
 
104
268
  def self.param(*args)
269
+ return @params.find { |p| p.name == args[0] } if args.size == 1
105
270
  @params << Rabbit::Param.new(*args)
106
271
  end
272
+
273
+ def self.params; @params; end
107
274
  end
108
275
 
109
276
  private
@@ -114,7 +281,11 @@ module Sinatra
114
281
 
115
282
  # Create an unique class name for all operations within Collection class
116
283
  def self.operation_class(collection_klass, operation_name)
117
- collection_klass.const_set(operation_name.to_s.camelize + 'Operation', Class.new(Operation))
284
+ begin
285
+ collection_klass.const_get(operation_name.to_s.camelize + 'Operation')
286
+ rescue NameError
287
+ collection_klass.const_set(operation_name.to_s.camelize + 'Operation', Class.new(Operation))
288
+ end
118
289
  end
119
290
 
120
291
  end
@@ -18,20 +18,26 @@ module Sinatra
18
18
  module Rabbit
19
19
 
20
20
  class BaseCollection < Sinatra::Base
21
- attr_accessor :name, :description
22
- attr_accessor :operations
23
-
24
21
  set :views, Proc.new { File.join(File::dirname(__FILE__), "..", "..", "views") }
25
22
  enable :method_overide
26
23
 
27
- def self.route_for(collection, operation_name=nil)
24
+ def self.route_for(collection, operation_name=nil, member=:member)
28
25
  if operation_name
29
26
  o = Sinatra::Rabbit::STANDARD_OPERATIONS[operation_name]
27
+ if o
28
+ o[:member] = false if member == :no_member
29
+ o[:collection] = true if member == :no_id
30
+ if member == :no_id_and_member
31
+ o[:collection] = true
32
+ o[:member] = true
33
+ end
34
+ end
30
35
  operation_path = (o && o[:member]) ? operation_name.to_s : nil
31
- id_param = (o && o[:collection]) ? nil : ":id"
36
+ operation_path = operation_name.to_s unless o
37
+ id_param = (o && o[:collection]) ? nil : (member.kind_of?(Hash) ? member[:id_name] : ':id')
32
38
  [route_for(collection), id_param, operation_path].compact.join('/')
33
39
  else
34
- "/#{collection}"
40
+ collection.to_s
35
41
  end
36
42
  end
37
43
 
@@ -40,8 +46,17 @@ module Sinatra
40
46
  (o && o[:method]) ? o[:method] : :get
41
47
  end
42
48
 
43
- def self.collection_class(name)
44
- Sinatra::Rabbit.const_set(name.to_s.camelize + 'Collection', Class.new(Collection))
49
+ def self.collection_class(name, parent_class=nil)
50
+ klass = parent_class || Sinatra::Rabbit
51
+ begin
52
+ klass.const_get(name.to_s.camelize + 'Collection')
53
+ rescue NameError
54
+ klass.const_set(name.to_s.camelize + 'Collection', Class.new(Collection))
55
+ end
56
+ end
57
+
58
+ def self.root_path
59
+ Sinatra::Rabbit.configuration[:root_path]
45
60
  end
46
61
 
47
62
  end
@@ -30,8 +30,13 @@ 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?
33
34
  @collections ||= []
34
- @collections << (current_collection = BaseCollection.collection_class(name).generate(name, &block))
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)
35
40
  use current_collection
36
41
  end
37
42
 
@@ -41,6 +46,11 @@ module Sinatra
41
46
  @collections
42
47
  end
43
48
 
49
+ def self.register_collection(c, &block)
50
+ @collections ||= []
51
+ @collections << c
52
+ end
53
+
44
54
  end
45
55
  end
46
56
  end
@@ -0,0 +1,69 @@
1
+ #
2
+ # Licensed to the Apache Software Foundation (ASF) under one or more
3
+ # contributor license agreements. See the NOTICE file distributed with
4
+ # this work for additional information regarding copyright ownership. The
5
+ # ASF licenses this file to you under the Apache License, Version 2.0 (the
6
+ # "License"); you may not use this file except in compliance with the
7
+ # License. You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14
+ # License for the specific language governing permissions and limitations
15
+ # under the License.
16
+
17
+ module Sinatra
18
+ module Rabbit
19
+ class Feature
20
+ attr_reader :name
21
+ attr_reader :collection
22
+ attr_reader :operations
23
+
24
+ def initialize(name, opts={}, &block)
25
+ @name = name
26
+ @operations = []
27
+ @collection = opts[:for]
28
+ raise "Each feature must define collection for which it will be valid using :for parameter" unless @collection
29
+ instance_eval(&block) if block_given?
30
+ end
31
+
32
+ def operation(name, &block)
33
+ @operations << Operation.new(name, &block) if block_given?
34
+ @operations.find { |o| o.name == name }
35
+ end
36
+
37
+ class Operation
38
+ attr_reader :name
39
+ attr_reader :params
40
+
41
+ def initialize(name, &block)
42
+ @name = name
43
+ @params = block
44
+ end
45
+
46
+ end
47
+ end
48
+
49
+ module Features
50
+
51
+ def features(&block)
52
+ @@features ||= []
53
+ instance_eval(&block) if block_given?
54
+ @@features
55
+ end
56
+
57
+ def feature(name, opts={}, &block)
58
+ @@features << Feature.new(name, opts, &block) if block_given?
59
+ @@features.find { |f| f.name == name }
60
+ end
61
+
62
+ def self.included(base)
63
+ base.register(Features)
64
+ end
65
+
66
+ end
67
+
68
+ end
69
+ end
@@ -24,22 +24,24 @@ module Sinatra
24
24
  @name, @klass = args.pop, args.pop
25
25
  raise "DSL: You need to specify the name and param type (#{@name})" unless @name or @klass
26
26
  parse_params!(args)
27
+ @description ||= "Description not available"
27
28
  end
28
29
 
29
30
  def required?; @required == true; end
30
31
  def optional?; !required?; end
31
32
  def enum?; !@values.nil?; end
32
- def number?; [:integer, :float].include?(@klass); end
33
+ def number?; [:integer, :float, :number].include?(@klass); end
34
+ def string?; @klass == :string; end
35
+
36
+ def to_s
37
+ "#{name}:#{klass}:#{required? ? 'required' : 'optional'}"
38
+ end
33
39
 
34
40
  private
35
41
 
36
42
  def parse_params!(args)
37
- if args.pop == :required
38
- @required = true
39
- else
40
- @required = false
41
- end
42
43
  @values = args.pop if args.last.kind_of? Array
44
+ @required = args.pop == :required if [:required, :optional].include? args.last
43
45
  @description = args.pop
44
46
  end
45
47
 
@@ -29,7 +29,7 @@ Gem::Specification.new do |s|
29
29
  a simple REST API using easy to undestand DSL.
30
30
  EOF
31
31
 
32
- s.version = '0.9'
32
+ s.version = '0.9.1'
33
33
  s.date = Time.now
34
34
  s.summary = %q{Sinatra REST API DSL}
35
35
  s.files = FileList[
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sinatra-rabbit
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.9'
4
+ version: 0.9.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-12-13 00:00:00.000000000 Z
12
+ date: 2012-03-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: sinatra
16
- requirement: &70240995322640 !ruby/object:Gem::Requirement
16
+ requirement: &70283050880100 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,7 +21,7 @@ dependencies:
21
21
  version: 1.3.0
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70240995322640
24
+ version_requirements: *70283050880100
25
25
  description: ! " Rabbit is a Sinatra extension which can help you writing\n a
26
26
  simple REST API using easy to undestand DSL.\n"
27
27
  email: dev@deltacloud.apache.org
@@ -34,6 +34,7 @@ files:
34
34
  - lib/sinatra/rabbit/base.rb
35
35
  - lib/sinatra/rabbit/base_collection.rb
36
36
  - lib/sinatra/rabbit/dsl.rb
37
+ - lib/sinatra/rabbit/features.rb
37
38
  - lib/sinatra/rabbit/param.rb
38
39
  - lib/sinatra/rabbit/validator.rb
39
40
  - LICENSE
@@ -59,7 +60,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
59
60
  version: '0'
60
61
  requirements: []
61
62
  rubyforge_project:
62
- rubygems_version: 1.8.10
63
+ rubygems_version: 1.8.15
63
64
  signing_key:
64
65
  specification_version: 3
65
66
  summary: Sinatra REST API DSL