sinatras-hat 0.0.2
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/lib/core_ext/array.rb +17 -0
- data/lib/core_ext/hash.rb +24 -0
- data/lib/core_ext/object.rb +49 -0
- data/lib/sinatras-hat.rb +23 -0
- data/lib/sinatras-hat/actions.rb +67 -0
- data/lib/sinatras-hat/auth.rb +59 -0
- data/lib/sinatras-hat/maker.rb +228 -0
- data/lib/sinatras-hat/responses.rb +62 -0
- metadata +91 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
class Array
|
2
|
+
def extract_options!
|
3
|
+
last.is_a?(Hash) ? pop : { }
|
4
|
+
end
|
5
|
+
|
6
|
+
def move_to_front(*entries)
|
7
|
+
entries.each do |entry|
|
8
|
+
unshift(entry) if delete(entry)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def move_to_back(*entries)
|
13
|
+
entries.each do |entry|
|
14
|
+
push(entry) if delete(entry)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Hash
|
2
|
+
def make_indifferent!
|
3
|
+
keys_values = self.dup
|
4
|
+
indifferent = Hash.new { |h,k| h[k.to_s] if Symbol === k }
|
5
|
+
replace(indifferent)
|
6
|
+
merge!(keys_values)
|
7
|
+
end
|
8
|
+
|
9
|
+
def nest!
|
10
|
+
new_params = Hash.new.make_indifferent!
|
11
|
+
each_pair do |full_key, value|
|
12
|
+
this_param = new_params
|
13
|
+
split_keys = full_key.split(/\]\[|\]|\[/)
|
14
|
+
split_keys.each_index do |index|
|
15
|
+
break if split_keys.length == index + 1
|
16
|
+
this_param[split_keys[index]] ||= Hash.new.make_indifferent!
|
17
|
+
this_param = this_param[split_keys[index]]
|
18
|
+
end
|
19
|
+
this_param[split_keys.last] = value
|
20
|
+
end
|
21
|
+
clear
|
22
|
+
replace(new_params)
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'metaid'
|
2
|
+
|
3
|
+
class Object
|
4
|
+
def tap
|
5
|
+
yield self
|
6
|
+
self
|
7
|
+
end
|
8
|
+
|
9
|
+
def try(m, *a, &b)
|
10
|
+
respond_to?(m) ? send(m, *a, &b) : nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def with(hash)
|
14
|
+
hash.each do |key, value|
|
15
|
+
meta_def(key) { hash[key] } unless respond_to?(key)
|
16
|
+
meta_def("#{key}=") { |v| hash[key] = v } unless respond_to?("#{key}=")
|
17
|
+
end
|
18
|
+
|
19
|
+
return unless block_given?
|
20
|
+
|
21
|
+
result = yield
|
22
|
+
|
23
|
+
hash.each do |key, value|
|
24
|
+
meta_eval { remove_method(key) }
|
25
|
+
meta_eval { remove_method("#{key}=") }
|
26
|
+
end
|
27
|
+
|
28
|
+
result
|
29
|
+
end
|
30
|
+
|
31
|
+
module InstanceExecHelper; end
|
32
|
+
include InstanceExecHelper
|
33
|
+
def instance_exec(*args, &block)
|
34
|
+
begin
|
35
|
+
old_critical, Thread.critical = Thread.critical, true
|
36
|
+
n = 0
|
37
|
+
n += 1 while respond_to?(mname="__instance_exec#{n}")
|
38
|
+
InstanceExecHelper.module_eval{ define_method(mname, &block) }
|
39
|
+
ensure
|
40
|
+
Thread.critical = old_critical
|
41
|
+
end
|
42
|
+
begin
|
43
|
+
ret = send(mname, *args)
|
44
|
+
ensure
|
45
|
+
InstanceExecHelper.module_eval{ remove_method(mname) } rescue nil
|
46
|
+
end
|
47
|
+
ret
|
48
|
+
end
|
49
|
+
end
|
data/lib/sinatras-hat.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
$LOAD_PATH << File.join(File.dirname(__FILE__), 'core_ext')
|
2
|
+
$LOAD_PATH << File.join(File.dirname(__FILE__), 'sinatras-hat')
|
3
|
+
|
4
|
+
require 'erb'
|
5
|
+
require 'extlib'
|
6
|
+
require 'dm-core'
|
7
|
+
require 'dm-serializer'
|
8
|
+
require 'array'
|
9
|
+
require 'hash'
|
10
|
+
require 'object'
|
11
|
+
require 'action'
|
12
|
+
require 'actions'
|
13
|
+
require 'responses'
|
14
|
+
require 'maker'
|
15
|
+
|
16
|
+
load 'auth.rb'
|
17
|
+
|
18
|
+
Rack::File::MIME_TYPES['json'] = 'text/x-json'
|
19
|
+
Rack::File::MIME_TYPES['yaml'] = 'text/x-yaml'
|
20
|
+
|
21
|
+
def mount(model, opts={}, &block)
|
22
|
+
Sinatra::Hat::Maker.new(model).define(self, opts, &block)
|
23
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module Hat
|
3
|
+
module Actions
|
4
|
+
def generate_actions!
|
5
|
+
only.each { |action| send("#{action}!") }
|
6
|
+
children.each do |resource|
|
7
|
+
mount(resource)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def index!
|
12
|
+
map :index, '/' do |params|
|
13
|
+
call(:finder, params)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def new!
|
18
|
+
map :new, '/new' do |params|
|
19
|
+
proxy(params).new
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def edit!
|
24
|
+
map :edit, '/:id/edit' do |params|
|
25
|
+
call(:record, params)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def show!
|
30
|
+
map :show, '/:id' do |params|
|
31
|
+
call(:record, params)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def create!
|
36
|
+
map :create, '/', :verb => :post do |params|
|
37
|
+
create[proxy(params), parse_for_attributes(params)]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def update!
|
42
|
+
map :update, '/:id', :verb => :put do |params|
|
43
|
+
update[call(:record, params), parse_for_attributes(params)]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def destroy!
|
48
|
+
map :destroy, '/:id', :verb => :delete do |params|
|
49
|
+
destroy[call(:record, params), parse_for_attributes(params)]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def parse_for_attributes(params, name=model.name.downcase)
|
56
|
+
if handler = accepts[params[:format].try(:to_sym)]
|
57
|
+
params.merge(name => handler.call(params[name]))
|
58
|
+
else
|
59
|
+
params.nest!
|
60
|
+
params[name] ||= { }
|
61
|
+
params[name][parent.model_id] = params[parent.model_id] if parent
|
62
|
+
params
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# from http://www.gittr.com/index.php/archive/sinatra-basic-authentication-selectively-applied
|
2
|
+
# adapted by pat nakajima for sinatra's hat
|
3
|
+
module Sinatra
|
4
|
+
module Authorization
|
5
|
+
class ProtectedAction
|
6
|
+
attr_reader :credentials, :context, :block
|
7
|
+
|
8
|
+
def initialize(context, credentials={}, &block)
|
9
|
+
@credentials, @context, @block = credentials, context, block
|
10
|
+
end
|
11
|
+
|
12
|
+
def check!
|
13
|
+
unauthorized! unless auth.provided?
|
14
|
+
bad_request! unless auth.basic?
|
15
|
+
unauthorized! unless authorize(*auth.credentials)
|
16
|
+
end
|
17
|
+
|
18
|
+
def remote_user
|
19
|
+
auth.username
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def authorize(username, password)
|
25
|
+
block.call(username, password)
|
26
|
+
end
|
27
|
+
|
28
|
+
def unauthorized!
|
29
|
+
context.header 'WWW-Authenticate' => %(Basic realm="#{credentials[:realm]}")
|
30
|
+
throw :halt, [ 401, 'Authorization Required' ]
|
31
|
+
end
|
32
|
+
|
33
|
+
def bad_request!
|
34
|
+
throw :halt, [ 400, 'Bad Request' ]
|
35
|
+
end
|
36
|
+
|
37
|
+
def auth
|
38
|
+
@auth ||= Rack::Auth::Basic::Request.new(context.request.env)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
module Helpers
|
43
|
+
def protect!(credentials, &block)
|
44
|
+
return if authorized?
|
45
|
+
guard = ProtectedAction.new(self, credentials, &block)
|
46
|
+
guard.check!
|
47
|
+
request.env['REMOTE_USER'] = guard.remote_user
|
48
|
+
end
|
49
|
+
|
50
|
+
def authorized?
|
51
|
+
request.env['REMOTE_USER']
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
helpers do
|
58
|
+
include Sinatra::Authorization::Helpers
|
59
|
+
end
|
@@ -0,0 +1,228 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module Hat
|
3
|
+
class Maker
|
4
|
+
attr_accessor :parent
|
5
|
+
attr_reader :model, :context, :options
|
6
|
+
|
7
|
+
include Actions, Responses
|
8
|
+
|
9
|
+
def initialize(model)
|
10
|
+
@model = model
|
11
|
+
with(options)
|
12
|
+
end
|
13
|
+
|
14
|
+
def define(context, opts={}, &block)
|
15
|
+
@context = context
|
16
|
+
@options.merge!(opts)
|
17
|
+
instance_eval(&block) if block_given?
|
18
|
+
generate_actions!
|
19
|
+
end
|
20
|
+
|
21
|
+
def mount(klass, opts={}, &block)
|
22
|
+
child = Maker.new(klass)
|
23
|
+
child.parent = self
|
24
|
+
child.define(context, opts, &block)
|
25
|
+
child
|
26
|
+
end
|
27
|
+
|
28
|
+
def resource_path(suffix)
|
29
|
+
resources = parents + [self]
|
30
|
+
path = resources.inject("") do |memo, maker|
|
31
|
+
memo += eql?(maker) ?
|
32
|
+
"/#{maker.prefix}" :
|
33
|
+
"/#{maker.prefix}/:#{maker.model.name}_id"
|
34
|
+
end
|
35
|
+
(path + suffix).tap do |s|
|
36
|
+
s.downcase!
|
37
|
+
s.gsub!(%r(/$), '')
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def parents
|
42
|
+
@parents ||= parent ? Array(parent) + parent.parents : []
|
43
|
+
end
|
44
|
+
|
45
|
+
def protect(*args)
|
46
|
+
opts = args.extract_options!
|
47
|
+
credentials.update(opts)
|
48
|
+
actions = get_or_set_option(:protect, args) do
|
49
|
+
self.protect += args
|
50
|
+
self.protect.uniq!
|
51
|
+
end
|
52
|
+
|
53
|
+
[actions].flatten
|
54
|
+
end
|
55
|
+
|
56
|
+
def only(*args)
|
57
|
+
result = get_or_set_option(:only, args) do
|
58
|
+
self.only = args
|
59
|
+
self.only.uniq!
|
60
|
+
end
|
61
|
+
|
62
|
+
result = Array(result)
|
63
|
+
|
64
|
+
result.move_to_back(:index)
|
65
|
+
result.move_to_front(:new, :edit)
|
66
|
+
|
67
|
+
result
|
68
|
+
end
|
69
|
+
|
70
|
+
def children(*args)
|
71
|
+
result = get_or_set_option(:children, args) do
|
72
|
+
self.children = args
|
73
|
+
self.children.uniq!
|
74
|
+
end
|
75
|
+
|
76
|
+
[result].flatten
|
77
|
+
end
|
78
|
+
|
79
|
+
def authenticator(&block)
|
80
|
+
if block_given?
|
81
|
+
@authenticator = block
|
82
|
+
else
|
83
|
+
@authenticator ||= proc { |username, password|
|
84
|
+
credentials[:username] == username and credentials[:password] == password
|
85
|
+
}
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def finder(&block)
|
90
|
+
if block_given?
|
91
|
+
@finder = block
|
92
|
+
else
|
93
|
+
@finder ||= proc { |params| all }
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def record(&block)
|
98
|
+
if block_given?
|
99
|
+
@record = block
|
100
|
+
else
|
101
|
+
@record ||= proc { |params| first(:id => params[:id]) }
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def create(&block)
|
106
|
+
if block_given?
|
107
|
+
@create = block
|
108
|
+
else
|
109
|
+
@create ||= proc do |model, params|
|
110
|
+
result = model.new
|
111
|
+
result.attributes = params[model_name]
|
112
|
+
result.save ? result : nil
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def update(&block)
|
118
|
+
if block_given?
|
119
|
+
@update = block
|
120
|
+
else
|
121
|
+
@update ||= proc do |record, params|
|
122
|
+
record.attributes = params[model_name]
|
123
|
+
record.save ? record : false
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def destroy(&block)
|
129
|
+
if block_given?
|
130
|
+
@destroy = block
|
131
|
+
else
|
132
|
+
@destroy ||= proc do |record, params|
|
133
|
+
record.destroy
|
134
|
+
:destroyed
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def map(name, path, opts={}, &block)
|
140
|
+
opts[:verb] ||= :get
|
141
|
+
klass = self
|
142
|
+
actions[name] = Action.new(self, name, block)
|
143
|
+
|
144
|
+
context.send(opts[:verb], resource_path(path)) do
|
145
|
+
begin
|
146
|
+
klass.templated(self, name, opts)
|
147
|
+
rescue Errno::ENOENT => e
|
148
|
+
klass.rescue_template_error(e)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
context.send(opts[:verb], "#{resource_path(path)}.:format") do
|
153
|
+
begin
|
154
|
+
klass.serialized(self, name, opts)
|
155
|
+
rescue UnsupportedFormat => e
|
156
|
+
klass.rescue_format_error(e)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def rescue_format_error(e)
|
162
|
+
throw :halt, [
|
163
|
+
406, [
|
164
|
+
"The `#{e.format}` format is not supported.\n",
|
165
|
+
"Valid Formats: #{accepts.keys.join(', ')}\n",
|
166
|
+
].join("\n")
|
167
|
+
]
|
168
|
+
end
|
169
|
+
|
170
|
+
# TODO Create a real template for this error.
|
171
|
+
def rescue_template_error(e)
|
172
|
+
msg = "<pre>There was a problem with your view template:\n\n "
|
173
|
+
msg << e.message
|
174
|
+
msg << "\n</pre>"
|
175
|
+
end
|
176
|
+
|
177
|
+
def call(method, params)
|
178
|
+
proxy(params).instance_exec(params, &send(method))
|
179
|
+
end
|
180
|
+
|
181
|
+
def proxy(params={})
|
182
|
+
return model if parent.nil?
|
183
|
+
fake_params = params.dup
|
184
|
+
fake_params.merge!("id" => params[parent.model_id])
|
185
|
+
fake_params.make_indifferent!
|
186
|
+
parent.call(:record, fake_params).try(prefix) || model
|
187
|
+
end
|
188
|
+
|
189
|
+
def model_id
|
190
|
+
"#{model_name}_id".to_sym
|
191
|
+
end
|
192
|
+
|
193
|
+
def model_name
|
194
|
+
model.name.downcase
|
195
|
+
end
|
196
|
+
|
197
|
+
def options
|
198
|
+
@options ||= {
|
199
|
+
:only => [:new, :edit, :show, :create, :update, :destroy, :index],
|
200
|
+
:prefix => Extlib::Inflection.tableize(model.name),
|
201
|
+
:actions => { },
|
202
|
+
:protect => [ ],
|
203
|
+
:formats => { },
|
204
|
+
:renderer => :erb,
|
205
|
+
:children => [],
|
206
|
+
:to_param => :id,
|
207
|
+
:nest_params => true,
|
208
|
+
:credentials => {
|
209
|
+
:username => 'admin',
|
210
|
+
:password => 'password',
|
211
|
+
:realm => 'TheApp.com'
|
212
|
+
},
|
213
|
+
:accepts => {
|
214
|
+
:yaml => proc { |string| YAML.load(string) },
|
215
|
+
:json => proc { |string| JSON.parse(string) },
|
216
|
+
:xml => proc { |string| Hash.from_xml(string)['hash'] }
|
217
|
+
}
|
218
|
+
}
|
219
|
+
end
|
220
|
+
|
221
|
+
private
|
222
|
+
|
223
|
+
def get_or_set_option(name, args, opts={})
|
224
|
+
args.length > 0 ? yield : options[name]
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module Hat
|
3
|
+
# TODO: Move these to Action class?
|
4
|
+
module Responses
|
5
|
+
class UnsupportedFormat < StandardError
|
6
|
+
attr_reader :format
|
7
|
+
|
8
|
+
def initialize(format)
|
9
|
+
@format = format
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def templated(event, name, opts={})
|
14
|
+
event.protect!(:realm => credentials[:realm], &authenticator) if protecting?(name)
|
15
|
+
|
16
|
+
root = File.join(Sinatra.application.options.views, prefix)
|
17
|
+
result = actions[name].handle(event)
|
18
|
+
event.instance_variable_set ivar_name(result), result
|
19
|
+
return opts[:verb] == :get ?
|
20
|
+
event.render(renderer, name, :views_directory => root) :
|
21
|
+
event.redirect(redirection_path(result))
|
22
|
+
end
|
23
|
+
|
24
|
+
def serialized(event, name, opts={})
|
25
|
+
format = event.params[:format].to_sym
|
26
|
+
|
27
|
+
event.protect!(:realm => credentials[:realm], &authenticator) if protecting?(name)
|
28
|
+
|
29
|
+
if accepts[format] or opts[:verb].eql?(:get)
|
30
|
+
event.content_type(format) rescue nil
|
31
|
+
object = actions[name].handle(event)
|
32
|
+
result = serializer_for(format).call(object)
|
33
|
+
return result unless result.nil?
|
34
|
+
end
|
35
|
+
|
36
|
+
raise UnsupportedFormat.new(format)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def serializer_for(format)
|
42
|
+
formats[format.to_sym] ||= proc do |object|
|
43
|
+
object.try("to_#{format}")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def protecting?(name)
|
48
|
+
protect.include?(:all) or protect.include?(name)
|
49
|
+
end
|
50
|
+
|
51
|
+
def redirection_path(result)
|
52
|
+
result.is_a?(Symbol) ?
|
53
|
+
"/#{prefix}" :
|
54
|
+
"/#{prefix}/#{result.send(to_param)}"
|
55
|
+
end
|
56
|
+
|
57
|
+
def ivar_name(result)
|
58
|
+
"@" + (result.respond_to?(:each) ? prefix : model.name.downcase)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
metadata
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sinatras-hat
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Pat Nakajima
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-10-16 00:00:00 -04:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: extlib
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: sinatra
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "0"
|
34
|
+
version:
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: metaid
|
37
|
+
type: :runtime
|
38
|
+
version_requirement:
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: "0"
|
44
|
+
version:
|
45
|
+
description:
|
46
|
+
email: patnakajima@gmail.com
|
47
|
+
executables: []
|
48
|
+
|
49
|
+
extensions: []
|
50
|
+
|
51
|
+
extra_rdoc_files: []
|
52
|
+
|
53
|
+
files:
|
54
|
+
- lib/core_ext
|
55
|
+
- lib/core_ext/hash.rb
|
56
|
+
- lib/core_ext/array.rb
|
57
|
+
- lib/core_ext/object.rb
|
58
|
+
- lib/sinatras-hat
|
59
|
+
- lib/sinatras-hat/maker.rb
|
60
|
+
- lib/sinatras-hat/actions.rb
|
61
|
+
- lib/sinatras-hat/auth.rb
|
62
|
+
- lib/sinatras-hat/responses.rb
|
63
|
+
- lib/sinatras-hat.rb
|
64
|
+
has_rdoc: false
|
65
|
+
homepage:
|
66
|
+
post_install_message:
|
67
|
+
rdoc_options: []
|
68
|
+
|
69
|
+
require_paths:
|
70
|
+
- lib
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: "0"
|
76
|
+
version:
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: "0"
|
82
|
+
version:
|
83
|
+
requirements: []
|
84
|
+
|
85
|
+
rubyforge_project:
|
86
|
+
rubygems_version: 1.3.1
|
87
|
+
signing_key:
|
88
|
+
specification_version: 2
|
89
|
+
summary: Simple REST-ful resources with Sinatra.
|
90
|
+
test_files: []
|
91
|
+
|