sinatra-api-helpers 1.0.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.
- data/.rspec +3 -0
- data/.yardopts +1 -0
- data/LICENSE +18 -0
- data/README.md +7 -0
- data/lib/sinatra-api-helpers.rb +32 -0
- data/lib/sinatra/api/ext/hash.rb +24 -0
- data/lib/sinatra/api/helpers.rb +256 -0
- data/lib/sinatra/api/version.rb +26 -0
- data/sinatra-api-helpers.gemspec +24 -0
- data/spec/helpers/router.rb +60 -0
- data/spec/spec_helper.rb +40 -0
- data/spec/unit/helpers_spec.rb +123 -0
- metadata +138 -0
data/.rspec
ADDED
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--protected -m markdown -r README.md
|
data/LICENSE
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Copyright (c) 2013 Algol Labs, LLC. <dev@algollabs.com>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
5
|
+
the Software without restriction, including without limitation the rights to use,
|
6
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
7
|
+
Software, and to permit persons to whom the Software is furnished to do so,
|
8
|
+
subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
11
|
+
copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
15
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
16
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
18
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# Copyright (c) 2013 Algol Labs, LLC. <dev@algollabs.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a
|
4
|
+
# copy of this software and associated documentation files (the "Software"),
|
5
|
+
# to deal in the Software without restriction, including without limitation
|
6
|
+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
7
|
+
# and/or sell copies of the Software, and to permit persons to whom the
|
8
|
+
# Software is furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
16
|
+
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
19
|
+
# SOFTWARE.
|
20
|
+
#
|
21
|
+
|
22
|
+
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__)))
|
23
|
+
|
24
|
+
require 'json'
|
25
|
+
require 'sinatra/base'
|
26
|
+
require 'sinatra/api/ext/hash'
|
27
|
+
require 'sinatra/api/version'
|
28
|
+
require 'sinatra/api/helpers'
|
29
|
+
|
30
|
+
module Sinatra
|
31
|
+
register API
|
32
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Hash
|
2
|
+
unless Hash.instance_methods.include?(:deep_merge)
|
3
|
+
# Merges self with another hash, recursively.
|
4
|
+
#
|
5
|
+
# This code was lovingly stolen from some random gem:
|
6
|
+
# http://gemjack.com/gems/tartan-0.1.1/classes/Hash.html
|
7
|
+
#
|
8
|
+
# Thanks to whoever made it.
|
9
|
+
def deep_merge(hash)
|
10
|
+
target = dup
|
11
|
+
|
12
|
+
hash.keys.each do |key|
|
13
|
+
if hash[key].is_a? Hash and self[key].is_a? Hash
|
14
|
+
target[key] = target[key].deep_merge(hash[key])
|
15
|
+
next
|
16
|
+
end
|
17
|
+
|
18
|
+
target[key] = hash[key]
|
19
|
+
end
|
20
|
+
|
21
|
+
target
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,256 @@
|
|
1
|
+
# Copyright (c) 2013 Algol Labs, LLC. <dev@algollabs.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a
|
4
|
+
# copy of this software and associated documentation files (the "Software"),
|
5
|
+
# to deal in the Software without restriction, including without limitation
|
6
|
+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
7
|
+
# and/or sell copies of the Software, and to permit persons to whom the
|
8
|
+
# Software is furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
16
|
+
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
19
|
+
# SOFTWARE.
|
20
|
+
#
|
21
|
+
|
22
|
+
module Sinatra
|
23
|
+
# TODO: accept nested parameters
|
24
|
+
module API
|
25
|
+
module Helpers
|
26
|
+
def api_call?
|
27
|
+
(request.accept || '').to_s.include?('json') ||
|
28
|
+
(request.content_type||'').to_s.include?('json')
|
29
|
+
end
|
30
|
+
|
31
|
+
# Define the required API arguments map. Any item defined
|
32
|
+
# not found in the supplied parameters of the API call will
|
33
|
+
# result in a 400 RC with a proper message marking the missing
|
34
|
+
# field.
|
35
|
+
#
|
36
|
+
# The map is a Hash of parameter keys and optional validator blocks.
|
37
|
+
#
|
38
|
+
# @example A map of required API call arguments
|
39
|
+
# api_required!({ title: nil, user_id: nil })
|
40
|
+
#
|
41
|
+
# Each entry can be optionally mapped to a validation proc that will
|
42
|
+
# be invoked *if* the field was supplied. The proc will be passed
|
43
|
+
# the value of the field.
|
44
|
+
#
|
45
|
+
# If the value is invalid and you need to suspend the request, you
|
46
|
+
# must return a String object with an appropriate error message.
|
47
|
+
#
|
48
|
+
# @example Rejecting a title if it's rude
|
49
|
+
# api_required!({
|
50
|
+
# :title => lambda { |t| return "Don't be rude" if t && t =~ /rude/ }
|
51
|
+
# })
|
52
|
+
#
|
53
|
+
# @note
|
54
|
+
# The supplied value passed to validation blocks is not pre-processed,
|
55
|
+
# so you must make sure that you check for nils or bad values in validator blocks!
|
56
|
+
def api_required!(args, h = params)
|
57
|
+
args.each_pair { |name, cnd|
|
58
|
+
if cnd.is_a?(Hash)
|
59
|
+
api_required!(cnd, h[name])
|
60
|
+
next
|
61
|
+
end
|
62
|
+
|
63
|
+
parse_api_argument(h, name, cnd, :required)
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
# Same as #api_required! except that fields defined in this map
|
68
|
+
# are optional and will be used only if they're supplied.
|
69
|
+
#
|
70
|
+
# @see #api_required!
|
71
|
+
def api_optional!(args, h = params)
|
72
|
+
args.each_pair { |name, cnd|
|
73
|
+
if cnd.is_a?(Hash)
|
74
|
+
api_optional!(cnd, h[name])
|
75
|
+
next
|
76
|
+
end
|
77
|
+
|
78
|
+
parse_api_argument(h, name, cnd, :optional)
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
# Consumes supplied parameters with the given keys from the API
|
83
|
+
# parameter map, and yields the consumed values for processing by
|
84
|
+
# the supplied block (if any).
|
85
|
+
#
|
86
|
+
# This is useful if:
|
87
|
+
# 1. a certain parameter does not correspond to a model attribute
|
88
|
+
# and needs to be renamed, or is used in a validation context
|
89
|
+
# 2. the data needs special treatment
|
90
|
+
# 3. the data needs to be (re)formatted
|
91
|
+
#
|
92
|
+
def api_consume!(keys)
|
93
|
+
out = nil
|
94
|
+
|
95
|
+
keys = [ keys ] unless keys.is_a?(Array)
|
96
|
+
keys.each do |k|
|
97
|
+
if val = @api[:required].delete(k.to_sym)
|
98
|
+
out = val
|
99
|
+
out = yield(val) if block_given?
|
100
|
+
end
|
101
|
+
|
102
|
+
if val = @api[:optional].delete(k.to_sym)
|
103
|
+
out = val
|
104
|
+
out = yield(val) if block_given?
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
out
|
109
|
+
end
|
110
|
+
|
111
|
+
def api_transform!(key, &handler)
|
112
|
+
if val = @api[:required][key.to_sym]
|
113
|
+
@api[:required][key.to_sym] = yield(val) if block_given?
|
114
|
+
end
|
115
|
+
|
116
|
+
if val = @api[:optional][key.to_sym]
|
117
|
+
@api[:optional][key.to_sym] = yield(val) if block_given?
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def api_has_param?(key)
|
122
|
+
@api[:optional].has_key?(key)
|
123
|
+
end
|
124
|
+
|
125
|
+
def api_param(key)
|
126
|
+
@api[:optional][key.to_sym] || @api[:required][key.to_sym]
|
127
|
+
end
|
128
|
+
|
129
|
+
# Returns a Hash of the *supplied* request parameters. Rejects
|
130
|
+
# any parameter that was not defined in the REQUIRED or OPTIONAL
|
131
|
+
# maps (or was consumed).
|
132
|
+
#
|
133
|
+
# @param Hash q A Hash of attributes to merge with the parameters,
|
134
|
+
# useful for defining defaults
|
135
|
+
def api_params(q = {})
|
136
|
+
@api[:optional].deep_merge(@api[:required]).deep_merge(q)
|
137
|
+
end
|
138
|
+
|
139
|
+
def api_clear!()
|
140
|
+
@api = { required: {}, optional: {} }
|
141
|
+
end
|
142
|
+
|
143
|
+
alias_method :api_reset!, :api_clear!
|
144
|
+
|
145
|
+
# Attempt to locate a resource based on an ID supplied in a request parameter.
|
146
|
+
#
|
147
|
+
# If the param map contains a resource id (ie, :folder_id),
|
148
|
+
# we attempt to locate and expose it to the route.
|
149
|
+
#
|
150
|
+
# A 404 is raised if:
|
151
|
+
# 1. the scope is missing (@space for folder, @space or @folder for page)
|
152
|
+
# 2. the resource couldn't be identified in its scope (@space or @folder)
|
153
|
+
#
|
154
|
+
# If the resources were located, they're accessible using @folder or @page.
|
155
|
+
#
|
156
|
+
# The route can be halted using the :requires => [] condition when it expects
|
157
|
+
# a resource.
|
158
|
+
#
|
159
|
+
# @example using :requires to reject a request with an invalid @page
|
160
|
+
# get '/folders/:folder_id/pages/:page_id', :requires => [ :page ] do
|
161
|
+
# @page.show # page is good
|
162
|
+
# @folder.show # so is its folder
|
163
|
+
# end
|
164
|
+
#
|
165
|
+
def __api_locate_resource(r, container = nil)
|
166
|
+
|
167
|
+
resource_id = params[r + '_id'].to_i
|
168
|
+
rklass = r.capitalize
|
169
|
+
|
170
|
+
collection = case
|
171
|
+
when container.nil?; eval "#{rklass}"
|
172
|
+
else; container.send("#{r.to_plural}")
|
173
|
+
end
|
174
|
+
|
175
|
+
puts "locating resource #{r} with id #{resource_id} from #{collection} [#{container}]"
|
176
|
+
|
177
|
+
resource = collection.get(resource_id)
|
178
|
+
|
179
|
+
if !resource
|
180
|
+
m = "No such resource: #{rklass}##{resource_id}"
|
181
|
+
if container
|
182
|
+
m << " in #{container.class.name.to_s}##{container.id}"
|
183
|
+
end
|
184
|
+
|
185
|
+
halt 404, m
|
186
|
+
end
|
187
|
+
|
188
|
+
if respond_to?(:can?)
|
189
|
+
unless can? :access, resource
|
190
|
+
halt 403, "You do not have access to this #{rklass} resource."
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
instance_variable_set('@'+r, resource)
|
195
|
+
|
196
|
+
resource
|
197
|
+
end
|
198
|
+
|
199
|
+
private
|
200
|
+
|
201
|
+
def parse_api_argument(h = params, name, cnd, type)
|
202
|
+
cnd ||= lambda { |*_| true }
|
203
|
+
name = name.to_s
|
204
|
+
|
205
|
+
unless [:required, :optional].include?(type)
|
206
|
+
raise ArgumentError, 'API Argument type must be either :required or :optional'
|
207
|
+
end
|
208
|
+
|
209
|
+
if !h.has_key?(name)
|
210
|
+
if type == :required
|
211
|
+
halt 400, "Missing required parameter :#{name}"
|
212
|
+
end
|
213
|
+
else
|
214
|
+
if cnd.respond_to?(:call)
|
215
|
+
errmsg = cnd.call(h[name])
|
216
|
+
halt 400, { :"#{name}" => errmsg } if errmsg && errmsg.is_a?(String)
|
217
|
+
end
|
218
|
+
|
219
|
+
@api[type][name.to_sym] = h[name]
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def self.registered(app)
|
225
|
+
app.helpers Helpers
|
226
|
+
app.before do
|
227
|
+
@api = { required: {}, optional: {} }
|
228
|
+
@parent_resource = nil
|
229
|
+
|
230
|
+
if api_call?
|
231
|
+
request.body.rewind
|
232
|
+
body = request.body.read.to_s || ''
|
233
|
+
unless body.empty?
|
234
|
+
begin;
|
235
|
+
params.merge!(::JSON.parse(body))
|
236
|
+
# puts params.inspect
|
237
|
+
# puts request.path
|
238
|
+
rescue ::JSON::ParserError => e
|
239
|
+
puts e.message
|
240
|
+
puts e.backtrace
|
241
|
+
halt 400, "Malformed JSON content"
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
app.set(:requires) do |*resources|
|
248
|
+
condition do
|
249
|
+
@required = resources.collect { |r| r.to_s }
|
250
|
+
@required.each { |r| @parent_resource = __api_locate_resource(r, @parent_resource) }
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# Copyright (c) 2013 Algol Labs, LLC. <dev@algollabs.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a
|
4
|
+
# copy of this software and associated documentation files (the "Software"),
|
5
|
+
# to deal in the Software without restriction, including without limitation
|
6
|
+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
7
|
+
# and/or sell copies of the Software, and to permit persons to whom the
|
8
|
+
# Software is furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
16
|
+
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
19
|
+
# SOFTWARE.
|
20
|
+
#
|
21
|
+
|
22
|
+
module Sinatra
|
23
|
+
module API
|
24
|
+
VERSION = "1.0.0"
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require File.join(%W[#{File.dirname(__FILE__)} lib sinatra api version])
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = 'sinatra-api-helpers'
|
5
|
+
s.summary = 'Handy helpers for writing RESTful APIs in Sinatra.'
|
6
|
+
s.version = Sinatra::API::VERSION
|
7
|
+
s.date = Time.now.strftime('%Y-%m-%d')
|
8
|
+
s.authors = [ 'Ahmad Amireh' ]
|
9
|
+
s.email = 'ahmad@algollabs.com'
|
10
|
+
s.homepage = 'https://github.com/amireh/sinatra-api-helpers'
|
11
|
+
s.files = Dir.glob("{lib,spec}/**/*.rb") +
|
12
|
+
[ 'LICENSE', 'README.md', '.rspec', '.yardopts', __FILE__ ]
|
13
|
+
s.has_rdoc = 'yard'
|
14
|
+
s.license = 'MIT'
|
15
|
+
|
16
|
+
s.required_ruby_version = '>= 1.9.3'
|
17
|
+
|
18
|
+
s.add_dependency 'json'
|
19
|
+
s.add_dependency 'sinatra'
|
20
|
+
|
21
|
+
s.add_development_dependency 'rspec'
|
22
|
+
s.add_development_dependency 'rack-test'
|
23
|
+
s.add_development_dependency 'yard', '>= 0.8.0'
|
24
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Router
|
2
|
+
class << self
|
3
|
+
def puts(*args)
|
4
|
+
super(*args) if $VERBOSE
|
5
|
+
end
|
6
|
+
|
7
|
+
# Locates routes defined for any verb containing the provided token.
|
8
|
+
#
|
9
|
+
# @param [String] token the token the route should contain
|
10
|
+
# @return [Array<Hash, Fixnum>] a map of all the verb routes, and the count of located routes
|
11
|
+
def routes_for(token)
|
12
|
+
all_routes = {}
|
13
|
+
count = 0
|
14
|
+
Sinatra::Application.routes.each do |verb_routes|
|
15
|
+
verb, routes = verb_routes[0], verb_routes[1]
|
16
|
+
all_routes[verb] ||= []
|
17
|
+
routes.each_with_index do |route, i|
|
18
|
+
route_regex = route.first.source
|
19
|
+
if route_regex.to_s.include?(token)
|
20
|
+
all_routes[verb] << route
|
21
|
+
count += 1
|
22
|
+
|
23
|
+
puts "Route located: #{verb} -> #{route_regex.to_s}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
all_routes[verb].uniq!
|
27
|
+
end
|
28
|
+
[ all_routes, count ]
|
29
|
+
end
|
30
|
+
|
31
|
+
def purge(token)
|
32
|
+
routes, nr_routes = *routes_for(token)
|
33
|
+
|
34
|
+
# puts "cleaning up #{nr_routes} routes"
|
35
|
+
|
36
|
+
routes.each_pair do |verb, vroutes|
|
37
|
+
vroutes.each do |r| delete_route(verb, r) end
|
38
|
+
end
|
39
|
+
|
40
|
+
yield(nr_routes) if block_given?
|
41
|
+
|
42
|
+
nr_routes
|
43
|
+
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
|
47
|
+
def delete_route(verb, r)
|
48
|
+
verb_routes = Sinatra::Application.routes.select { |v| v == verb }.first
|
49
|
+
|
50
|
+
unless verb_routes
|
51
|
+
raise "Couldn't find routes for verb #{verb}, that's impossible"
|
52
|
+
end
|
53
|
+
|
54
|
+
unless verb_routes[1].delete(r)
|
55
|
+
raise "Route '#{r}' not found for verb #{verb}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
$LOAD_PATH << File.join(File.dirname(__FILE__), '..')
|
2
|
+
|
3
|
+
ENV['RACK_ENV'] = 'test'
|
4
|
+
|
5
|
+
|
6
|
+
require 'lib/sinatra-api-helpers'
|
7
|
+
require 'rspec'
|
8
|
+
require 'rack/test'
|
9
|
+
|
10
|
+
class SinatraAPITestApp < Sinatra::Base
|
11
|
+
register Sinatra::API
|
12
|
+
end
|
13
|
+
|
14
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
15
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
16
|
+
# Require this file using `require "spec_helper"` to ensure that it is only
|
17
|
+
# loaded once.
|
18
|
+
#
|
19
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
20
|
+
RSpec.configure do |config|
|
21
|
+
Thread.abort_on_exception = true
|
22
|
+
|
23
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
24
|
+
config.run_all_when_everything_filtered = true
|
25
|
+
config.filter_run :focus => true
|
26
|
+
|
27
|
+
# Run specs in random order to surface order dependencies. If you find an
|
28
|
+
# order dependency and want to debug it, you can fix the order by providing
|
29
|
+
# the seed, which is printed after each run.
|
30
|
+
# --seed 1234
|
31
|
+
config.order = 'random'
|
32
|
+
|
33
|
+
include Rack::Test::Methods
|
34
|
+
|
35
|
+
def app
|
36
|
+
Sinatra::Application
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
Dir["./spec/helpers/**/*.rb"].sort.each { |f| require f }
|
@@ -0,0 +1,123 @@
|
|
1
|
+
describe "Helpers" do
|
2
|
+
before :each do
|
3
|
+
Router.purge('/')
|
4
|
+
end
|
5
|
+
|
6
|
+
class ModelAdapter
|
7
|
+
end
|
8
|
+
|
9
|
+
class CollectionAdapter
|
10
|
+
def initialize(model)
|
11
|
+
@model = model
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.get(key)
|
15
|
+
return @model.new
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Item
|
20
|
+
def self.get(id)
|
21
|
+
return {} if id == 1
|
22
|
+
end
|
23
|
+
|
24
|
+
def sub_items
|
25
|
+
CollectionAdapter.new(SubItem)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class SubItem < ModelAdapter
|
30
|
+
def item
|
31
|
+
Item.new
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should reject a request missing a required parameter" do
|
36
|
+
app.get '/' do
|
37
|
+
api_required!({
|
38
|
+
id: nil
|
39
|
+
})
|
40
|
+
end
|
41
|
+
|
42
|
+
get '/'
|
43
|
+
last_response.status.should == 400
|
44
|
+
last_response.body.should match(/Missing required parameter :id/)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should accept a request satisfying required parameters" do
|
48
|
+
app.get '/' do
|
49
|
+
api_required!({
|
50
|
+
id: nil
|
51
|
+
})
|
52
|
+
end
|
53
|
+
|
54
|
+
get '/', { id: 5 }
|
55
|
+
last_response.status.should == 200
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should accept a request not satisfying optional parameters" do
|
59
|
+
app.get '/' do
|
60
|
+
api_required!({
|
61
|
+
id: nil
|
62
|
+
})
|
63
|
+
api_optional!({
|
64
|
+
name: nil
|
65
|
+
})
|
66
|
+
end
|
67
|
+
|
68
|
+
get '/', { id: 5 }
|
69
|
+
last_response.status.should == 200
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should apply parameter conditions" do
|
73
|
+
app.get '/' do
|
74
|
+
api_optional!({
|
75
|
+
name: lambda { |v|
|
76
|
+
unless (v || '').match /ahmad/
|
77
|
+
"Unexpected name."
|
78
|
+
end
|
79
|
+
}
|
80
|
+
})
|
81
|
+
end
|
82
|
+
|
83
|
+
get '/', { name: 'foobar' }
|
84
|
+
last_response.status.should == 400
|
85
|
+
last_response.body.should match(/Unexpected name/)
|
86
|
+
|
87
|
+
get '/', { name: 'ahmad' }
|
88
|
+
last_response.status.should == 200
|
89
|
+
end
|
90
|
+
|
91
|
+
it "should pick parameters" do
|
92
|
+
app.get '/' do
|
93
|
+
api_optional!({
|
94
|
+
name: nil
|
95
|
+
})
|
96
|
+
|
97
|
+
api_params.to_json
|
98
|
+
end
|
99
|
+
|
100
|
+
get '/', {
|
101
|
+
name: 'foobar',
|
102
|
+
some: 'thing'
|
103
|
+
}
|
104
|
+
|
105
|
+
last_response.body.should == {
|
106
|
+
name: 'foobar'
|
107
|
+
}.to_json
|
108
|
+
end
|
109
|
+
|
110
|
+
it "should locate a resource" do
|
111
|
+
app.get '/items/:item_id', requires: [ :item ] do
|
112
|
+
@item.to_json
|
113
|
+
end
|
114
|
+
|
115
|
+
get '/items/1'
|
116
|
+
last_response.status.should == 200
|
117
|
+
last_response.body.should == {}.to_json
|
118
|
+
|
119
|
+
get '/items/2'
|
120
|
+
last_response.status.should == 404
|
121
|
+
last_response.body.should match /No such resource/
|
122
|
+
end
|
123
|
+
end
|
metadata
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sinatra-api-helpers
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ahmad Amireh
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-09-19 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: json
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: sinatra
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rack-test
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: yard
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 0.8.0
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 0.8.0
|
94
|
+
description:
|
95
|
+
email: ahmad@algollabs.com
|
96
|
+
executables: []
|
97
|
+
extensions: []
|
98
|
+
extra_rdoc_files: []
|
99
|
+
files:
|
100
|
+
- lib/sinatra-api-helpers.rb
|
101
|
+
- lib/sinatra/api/helpers.rb
|
102
|
+
- lib/sinatra/api/version.rb
|
103
|
+
- lib/sinatra/api/ext/hash.rb
|
104
|
+
- spec/unit/helpers_spec.rb
|
105
|
+
- spec/helpers/router.rb
|
106
|
+
- spec/spec_helper.rb
|
107
|
+
- LICENSE
|
108
|
+
- README.md
|
109
|
+
- .rspec
|
110
|
+
- .yardopts
|
111
|
+
- sinatra-api-helpers.gemspec
|
112
|
+
homepage: https://github.com/amireh/sinatra-api-helpers
|
113
|
+
licenses:
|
114
|
+
- MIT
|
115
|
+
post_install_message:
|
116
|
+
rdoc_options: []
|
117
|
+
require_paths:
|
118
|
+
- lib
|
119
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
120
|
+
none: false
|
121
|
+
requirements:
|
122
|
+
- - ! '>='
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 1.9.3
|
125
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
126
|
+
none: false
|
127
|
+
requirements:
|
128
|
+
- - ! '>='
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
requirements: []
|
132
|
+
rubyforge_project:
|
133
|
+
rubygems_version: 1.8.23
|
134
|
+
signing_key:
|
135
|
+
specification_version: 3
|
136
|
+
summary: Handy helpers for writing RESTful APIs in Sinatra.
|
137
|
+
test_files: []
|
138
|
+
has_rdoc: yard
|