sinatra-api 1.0.0 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.yardopts CHANGED
@@ -1 +1 @@
1
- --protected -m markdown -r README.md
1
+ --protected --private -m markdown -r README.md
@@ -22,18 +22,18 @@
22
22
  module Sinatra
23
23
  module API
24
24
  module Callbacks
25
- attr_accessor :api_callbacks
25
+ attr_accessor :callbacks
26
26
 
27
27
  def self.extended(base)
28
- base.api_callbacks = {}
28
+ base.callbacks = {}
29
29
  end
30
30
 
31
31
  def on(event, &callback)
32
- (self.api_callbacks[event.to_sym] ||= []) << callback
32
+ (self.callbacks[event.to_sym] ||= []) << callback
33
33
  end
34
34
 
35
35
  def trigger(event, *args)
36
- callbacks = self.api_callbacks[event.to_sym] || []
36
+ callbacks = self.callbacks[event.to_sym] || []
37
37
  callbacks.each do |callback|
38
38
  callback.call(*args)
39
39
  end
@@ -19,217 +19,11 @@
19
19
  # SOFTWARE.
20
20
  #
21
21
 
22
- module Sinatra
23
- # TODO: accept nested parameters
24
- module API
25
- public
26
-
27
- # attr_accessor :resource_aliases
28
-
29
- module Helpers
30
- def api_call?
31
- (request.accept || '').to_s.include?('json') ||
32
- (request.content_type||'').to_s.include?('json')
33
- end
34
-
35
- # Define the required API arguments map. Any item defined
36
- # not found in the supplied parameters of the API call will
37
- # result in a 400 RC with a proper message marking the missing
38
- # field.
39
- #
40
- # The map is a Hash of parameter keys and optional validator blocks.
41
- #
42
- # @example A map of required API call arguments
43
- # api_required!({ title: nil, user_id: nil })
44
- #
45
- # Each entry can be optionally mapped to a validation proc that will
46
- # be invoked *if* the field was supplied. The proc will be passed
47
- # the value of the field.
48
- #
49
- # If the value is invalid and you need to suspend the request, you
50
- # must return a String object with an appropriate error message.
51
- #
52
- # @example Rejecting a title if it's rude
53
- # api_required!({
54
- # :title => lambda { |t| return "Don't be rude" if t && t =~ /rude/ }
55
- # })
56
- #
57
- # @note
58
- # The supplied value passed to validation blocks is not pre-processed,
59
- # so you must make sure that you check for nils or bad values in validator blocks!
60
- def api_required!(args, h = params)
61
- args.each_pair { |name, cnd|
62
- if cnd.is_a?(Hash)
63
- api_required!(cnd, h[name])
64
- next
65
- end
66
-
67
- parse_api_argument(h, name, cnd, :required)
68
- }
69
- end
70
-
71
- # Same as #api_required! except that fields defined in this map
72
- # are optional and will be used only if they're supplied.
73
- #
74
- # @see #api_required!
75
- def api_optional!(args, h = params)
76
- args.each_pair { |name, cnd|
77
- if cnd.is_a?(Hash)
78
- api_optional!(cnd, h[name])
79
- next
80
- end
81
-
82
- parse_api_argument(h, name, cnd, :optional)
83
- }
84
- end
85
-
86
- # Consumes supplied parameters with the given keys from the API
87
- # parameter map, and yields the consumed values for processing by
88
- # the supplied block (if any).
89
- #
90
- # This is useful if:
91
- # 1. a certain parameter does not correspond to a model attribute
92
- # and needs to be renamed, or is used in a validation context
93
- # 2. the data needs special treatment
94
- # 3. the data needs to be (re)formatted
95
- #
96
- def api_consume!(keys)
97
- out = nil
98
-
99
- keys = [ keys ] unless keys.is_a?(Array)
100
- keys.each do |k|
101
- if val = @api[:required].delete(k.to_sym)
102
- out = val
103
- out = yield(val) if block_given?
104
- end
105
-
106
- if val = @api[:optional].delete(k.to_sym)
107
- out = val
108
- out = yield(val) if block_given?
109
- end
110
- end
111
-
112
- out
113
- end
114
-
115
- def api_transform!(key, &handler)
116
- if val = @api[:required][key.to_sym]
117
- @api[:required][key.to_sym] = yield(val) if block_given?
118
- end
119
-
120
- if val = @api[:optional][key.to_sym]
121
- @api[:optional][key.to_sym] = yield(val) if block_given?
122
- end
123
- end
124
-
125
- def api_has_param?(key)
126
- @api[:optional].has_key?(key)
127
- end
128
-
129
- def api_param(key)
130
- @api[:optional][key.to_sym] || @api[:required][key.to_sym]
131
- end
132
-
133
- # Returns a Hash of the *supplied* request parameters. Rejects
134
- # any parameter that was not defined in the REQUIRED or OPTIONAL
135
- # maps (or was consumed).
136
- #
137
- # @param Hash q A Hash of attributes to merge with the parameters,
138
- # useful for defining defaults
139
- def api_params(q = {})
140
- @api[:optional].deep_merge(@api[:required]).deep_merge(q)
141
- end
142
-
143
- def api_clear!()
144
- @api = { required: {}, optional: {} }
145
- end
146
-
147
- alias_method :api_reset!, :api_clear!
148
-
149
- # Attempt to locate a resource based on an ID supplied in a request parameter.
150
- #
151
- # If the param map contains a resource id (ie, :folder_id),
152
- # we attempt to locate and expose it to the route.
153
- #
154
- # A 404 is raised if:
155
- # 1. the scope is missing (@space for folder, @space or @folder for page)
156
- # 2. the resource couldn't be identified in its scope (@space or @folder)
157
- #
158
- # If the resources were located, they're accessible using @folder or @page.
159
- #
160
- # The route can be halted using the :requires => [] condition when it expects
161
- # a resource.
162
- #
163
- # @example using :requires to reject a request with an invalid @page
164
- # get '/folders/:folder_id/pages/:page_id', :requires => [ :page ] do
165
- # @page.show # page is good
166
- # @folder.show # so is its folder
167
- # end
168
- #
169
- def __api_locate_resource(r, container = nil)
170
-
171
- resource_id = params[r + '_id'].to_i
172
- rklass = r.camelize
173
-
174
- collection = case
175
- when container.nil?; eval "#{ResourcePrefix}#{rklass}"
176
- else; container.send("#{r.to_plural}")
177
- end
178
-
179
- puts "locating resource #{r} with id #{resource_id} from #{collection} [#{container}]"
180
-
181
- resource = collection.get(resource_id)
182
-
183
- if !resource
184
- m = "No such resource: #{rklass}##{resource_id}"
185
- if container
186
- m << " in #{container.class.name.to_s}##{container.id}"
187
- end
188
-
189
- halt 404, m
190
- end
191
-
192
- if respond_to?(:can?)
193
- unless can? :access, resource
194
- halt 403, "You do not have access to this #{rklass} resource."
195
- end
196
- end
197
-
198
- instance_variable_set('@'+r, resource)
199
-
200
- API.trigger :resource_located, resource, r
201
-
202
- API.aliases_for(r).each { |resource_alias|
203
- instance_variable_set('@'+resource_alias, resource)
204
- puts "API: resource #{rklass} exported to @#{resource_alias}"
205
- }
206
-
207
- resource
208
- end
209
-
210
- private
211
-
212
- def parse_api_argument(h = params, name, cnd, type)
213
- cnd ||= lambda { |*_| true }
214
- name = name.to_s
215
-
216
- unless [:required, :optional].include?(type)
217
- raise ArgumentError, 'API Argument type must be either :required or :optional'
218
- end
219
-
220
- if !h.has_key?(name)
221
- if type == :required
222
- halt 400, "Missing required parameter :#{name}"
223
- end
224
- else
225
- if cnd.respond_to?(:call)
226
- errmsg = cnd.call(h[name])
227
- halt 400, { :"#{name}" => errmsg } if errmsg && errmsg.is_a?(String)
228
- end
229
-
230
- @api[type][name.to_sym] = h[name]
231
- end
232
- end
22
+ module Sinatra::API
23
+ module Helpers
24
+ def api_call?
25
+ (request.accept || '').to_s.include?('json') ||
26
+ (request.content_type||'').to_s.include?('json')
233
27
  end
234
28
  end
235
29
  end
@@ -0,0 +1,167 @@
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::API
23
+ # API for defining parameters an endpoint requires or accepts, their types,
24
+ # and optional validators.
25
+ #
26
+ # TODO: accept nested parameters
27
+ module Parameters
28
+ # Define the required API arguments map. Any item defined
29
+ # not found in the supplied parameters of the API call will
30
+ # result in a 400 RC with a proper message marking the missing
31
+ # field.
32
+ #
33
+ # The map is a Hash of parameter keys and optional validator blocks.
34
+ #
35
+ # @example A map of required API call arguments
36
+ # api_required!({ title: nil, user_id: nil })
37
+ #
38
+ # Each entry can be optionally mapped to a validation proc that will
39
+ # be invoked *if* the field was supplied. The proc will be passed
40
+ # the value of the field.
41
+ #
42
+ # If the value is invalid and you need to suspend the request, you
43
+ # must return a String object with an appropriate error message.
44
+ #
45
+ # @example Rejecting a title if it's rude
46
+ # api_required!({
47
+ # :title => lambda { |t| return "Don't be rude" if t && t =~ /rude/ }
48
+ # })
49
+ #
50
+ # @note
51
+ # The supplied value passed to validation blocks is not pre-processed,
52
+ # so you must make sure that you check for nils or bad values in validator blocks!
53
+ def api_required!(args, h = params)
54
+ args.each_pair do |name, cnd|
55
+ if cnd.is_a?(Hash)
56
+ api_required!(cnd, h[name])
57
+ next
58
+ end
59
+
60
+ parse_api_argument(h, name, cnd, :required)
61
+ end
62
+ end
63
+
64
+ # Same as #api_required! except that fields defined in this map
65
+ # are optional and will be used only if they're supplied.
66
+ #
67
+ # @see #api_required!
68
+ def api_optional!(args, h = params)
69
+ args.each_pair { |name, cnd|
70
+ if cnd.is_a?(Hash)
71
+ api_optional!(cnd, h[name])
72
+ next
73
+ end
74
+
75
+ parse_api_argument(h, name, cnd, :optional)
76
+ }
77
+ end
78
+
79
+ # Consumes supplied parameters with the given keys from the API
80
+ # parameter map, and yields the consumed values for processing by
81
+ # the supplied block (if any).
82
+ #
83
+ # This is useful if:
84
+ # 1. a certain parameter does not correspond to a model attribute
85
+ # and needs to be renamed, or is used in a validation context
86
+ # 2. the data needs special treatment
87
+ # 3. the data needs to be (re)formatted
88
+ #
89
+ def api_consume!(keys)
90
+ out = nil
91
+
92
+ keys = [ keys ] unless keys.is_a?(Array)
93
+ keys.each do |k|
94
+ if val = @api[:required].delete(k.to_sym)
95
+ out = val
96
+ out = yield(val) if block_given?
97
+ end
98
+
99
+ if val = @api[:optional].delete(k.to_sym)
100
+ out = val
101
+ out = yield(val) if block_given?
102
+ end
103
+ end
104
+
105
+ out
106
+ end
107
+
108
+ def api_transform!(key, &handler)
109
+ if val = @api[:required][key.to_sym]
110
+ @api[:required][key.to_sym] = yield(val) if block_given?
111
+ end
112
+
113
+ if val = @api[:optional][key.to_sym]
114
+ @api[:optional][key.to_sym] = yield(val) if block_given?
115
+ end
116
+ end
117
+
118
+ def api_has_param?(key)
119
+ @api[:optional].has_key?(key)
120
+ end
121
+
122
+ def api_param(key)
123
+ @api[:optional][key.to_sym] || @api[:required][key.to_sym]
124
+ end
125
+
126
+ # Returns a Hash of the *supplied* request parameters. Rejects
127
+ # any parameter that was not defined in the REQUIRED or OPTIONAL
128
+ # maps (or was consumed).
129
+ #
130
+ # @param [Hash] q
131
+ # A Hash of attributes to merge with the parameters, useful for defining
132
+ # defaults.
133
+ def api_params(q = {})
134
+ @api[:optional].deep_merge(@api[:required]).deep_merge(q)
135
+ end
136
+
137
+ def api_clear!()
138
+ @api = { required: {}, optional: {} }
139
+ end
140
+
141
+ alias_method :api_reset!, :api_clear!
142
+
143
+ private
144
+
145
+ def parse_api_argument(h = params, name, cnd, type)
146
+ cnd ||= lambda { |*_| true }
147
+ name = name.to_s
148
+
149
+ unless [:required, :optional].include?(type)
150
+ raise ArgumentError, 'API Argument type must be either :required or :optional'
151
+ end
152
+
153
+ if !h.has_key?(name)
154
+ if type == :required
155
+ halt 400, "Missing required parameter :#{name}"
156
+ end
157
+ else
158
+ if cnd.respond_to?(:call)
159
+ errmsg = cnd.call(h[name])
160
+ halt 400, { :"#{name}" => errmsg } if errmsg && errmsg.is_a?(String)
161
+ end
162
+
163
+ @api[type][name.to_sym] = h[name]
164
+ end
165
+ end
166
+ end
167
+ end
@@ -5,11 +5,7 @@ module Sinatra
5
5
 
6
6
  def self.extended(base)
7
7
  base.resource_aliases = {}
8
- base.on :resource_located do |resource, name|
9
- base.aliases_for(name).each do |resource_alias|
10
- instance_variable_set("@#{resource_alias}", resource)
11
- end
12
- end
8
+ base.on :resource_located, &method(:export_alias)
13
9
  end
14
10
 
15
11
  def alias_resource(original, resource_alias)
@@ -31,6 +27,15 @@ module Sinatra
31
27
  def reset_aliases!
32
28
  self.resource_aliases = {}
33
29
  end
30
+
31
+ private
32
+
33
+ def self.export_alias(resource, name)
34
+ base = Sinatra::API
35
+ base.aliases_for(name).each do |resource_alias|
36
+ base.instance.instance_variable_set("@#{resource_alias}", resource)
37
+ end
38
+ end
34
39
  end
35
40
  end
36
41
  end
@@ -0,0 +1,84 @@
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::API
23
+ # API for defining parameters an endpoint requires or accepts, their types,
24
+ # and optional validators.
25
+ #
26
+ module Resources
27
+ private
28
+
29
+ # Attempt to locate a resource based on an ID supplied in a request parameter.
30
+ #
31
+ # If the param map contains a resource id (ie, :folder_id),
32
+ # we attempt to locate and expose it to the route.
33
+ #
34
+ # A 404 is raised if:
35
+ # 1. the scope is missing (@space for folder, @space or @folder for page)
36
+ # 2. the resource couldn't be identified in its scope (@space or @folder)
37
+ #
38
+ # If the resources were located, they're accessible using @folder or @page.
39
+ #
40
+ # The route can be halted using the :requires => [] condition when it expects
41
+ # a resource.
42
+ #
43
+ # @example using :requires to reject a request with an invalid @page
44
+ # get '/folders/:folder_id/pages/:page_id', :requires => [ :page ] do
45
+ # @page.show # page is good
46
+ # @folder.show # so is its folder
47
+ # end
48
+ #
49
+ def api_locate_resource(r, container = nil)
50
+ resource_id = params[r + '_id'].to_i
51
+ rklass = r.camelize
52
+
53
+ collection = case
54
+ when container.nil?; eval "#{ResourcePrefix}#{rklass}"
55
+ else; container.send("#{r.to_plural}")
56
+ end
57
+
58
+ puts "locating resource #{r} with id #{resource_id} from #{collection} [#{container}]"
59
+
60
+ resource = collection.get(resource_id)
61
+
62
+ if !resource
63
+ m = "No such resource: #{rklass}##{resource_id}"
64
+ if container
65
+ m << " in #{container.class.name.to_s}##{container.id}"
66
+ end
67
+
68
+ halt 404, m
69
+ end
70
+
71
+ if respond_to?(:can?)
72
+ unless can? :access, resource
73
+ halt 403, "You do not have access to this #{rklass} resource."
74
+ end
75
+ end
76
+
77
+ instance_variable_set('@'+r, resource)
78
+
79
+ Sinatra::API.trigger :resource_located, resource, r
80
+
81
+ resource
82
+ end
83
+ end
84
+ end
@@ -21,6 +21,6 @@
21
21
 
22
22
  module Sinatra
23
23
  module API
24
- VERSION = "1.0.0"
24
+ VERSION = "1.0.2"
25
25
  end
26
26
  end
data/lib/sinatra/api.rb CHANGED
@@ -30,6 +30,8 @@ require 'sinatra/api/version'
30
30
  require 'sinatra/api/callbacks'
31
31
  require 'sinatra/api/helpers'
32
32
  require 'sinatra/api/resource_aliases'
33
+ require 'sinatra/api/resources'
34
+ require 'sinatra/api/parameters'
33
35
 
34
36
  module Sinatra
35
37
  module API
@@ -37,37 +39,48 @@ module Sinatra
37
39
  extend ResourceAliases
38
40
 
39
41
  class << self
42
+ # @!attribute logger
43
+ # @return [ActiveSupport::Logger]
44
+ # A Logger instance.
40
45
  attr_accessor :logger
41
- end
42
46
 
43
- ResourcePrefix = '::'
47
+ # @!attribute instance
48
+ # @return [Sinatra::Application]
49
+ # The Sinatra instance that is evaluating the current request.
50
+ attr_accessor :instance
44
51
 
45
- # Parse a JSON construct from a string stream.
46
- #
47
- # Override this to use a custom JSON parser, if necessary.
48
- #
49
- # @param [String] stream The raw JSON stream.
50
- #
51
- # @return [Hash] A Hash of the parsed JSON.
52
- def parse_json(stream)
53
- ::JSON.parse(stream)
52
+ # Parse a JSON construct from a string stream.
53
+ #
54
+ # Override this to use a custom JSON parser, if necessary.
55
+ #
56
+ # @param [String] stream The raw JSON stream.
57
+ #
58
+ # @return [Hash] A Hash of the parsed JSON.
59
+ def parse_json(stream)
60
+ ::JSON.parse(stream)
61
+ end
54
62
  end
55
63
 
64
+ ResourcePrefix = '::'
65
+
56
66
  def self.registered(app)
67
+ base = self
57
68
  self.logger = ActiveSupport::Logger.new(STDOUT)
58
69
 
59
- app.helpers Helpers
70
+ app.helpers Helpers, Parameters, Resources
60
71
  app.before do
72
+ base.instance = self
73
+
61
74
  @api = { required: {}, optional: {} }
62
75
  @parent_resource = nil
63
76
 
64
77
  if api_call?
65
78
  request.body.rewind
66
- body = request.body.read.to_s || ''
79
+ raw_json = request.body.read.to_s || ''
67
80
 
68
- unless body.empty?
81
+ unless raw_json.empty?
69
82
  begin
70
- params.merge!(parse_json(body))
83
+ params.merge!(base.parse_json(raw_json))
71
84
  rescue ::JSON::ParserError => e
72
85
  logger.warn e.message
73
86
  logger.warn e.backtrace
@@ -82,7 +95,7 @@ module Sinatra
82
95
  condition do
83
96
  @required = resources.collect { |r| r.to_s }
84
97
  @required.each do |r|
85
- @parent_resource = __api_locate_resource(r, @parent_resource)
98
+ @parent_resource = api_locate_resource(r, @parent_resource)
86
99
  end
87
100
  end
88
101
  end
@@ -0,0 +1,15 @@
1
+ describe Sinatra::API::Helpers do
2
+ include_examples 'integration specs'
3
+
4
+ it 'should catch and reject malformed JSON' do
5
+ app.post '/' do
6
+ api_required!({
7
+ id: nil
8
+ })
9
+ end
10
+
11
+ post '/', 'x]'
12
+ last_response.status.should == 400
13
+ last_response.body.should match(/malformed json/i)
14
+ end
15
+ end
@@ -1,5 +1,8 @@
1
1
  shared_examples_for "integration specs" do
2
2
  before :each do
3
3
  Router.purge('/')
4
+
5
+ header 'Content-Type', 'application/json'
6
+ header 'Accept', 'application/json'
4
7
  end
5
8
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sinatra-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -132,10 +132,13 @@ files:
132
132
  - lib/sinatra/api.rb
133
133
  - lib/sinatra/api/callbacks.rb
134
134
  - lib/sinatra/api/helpers.rb
135
+ - lib/sinatra/api/parameters.rb
135
136
  - lib/sinatra/api/resource_aliases.rb
137
+ - lib/sinatra/api/resources.rb
136
138
  - lib/sinatra/api/version.rb
137
139
  - spec/unit/callbacks_spec.rb
138
140
  - spec/unit/resource_aliases_spec.rb
141
+ - spec/integration/api_spec.rb
139
142
  - spec/integration/helpers_spec.rb
140
143
  - spec/integration/resource_aliases_spec.rb
141
144
  - spec/support/integration_specs.rb