sinatra-api 1.0.2 → 1.1.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/.yardopts CHANGED
@@ -1 +1 @@
1
- --protected --private -m markdown -r README.md
1
+ --protected -m markdown -r README.md
data/lib/sinatra/api.rb CHANGED
@@ -27,11 +27,17 @@ require 'sinatra/base'
27
27
  require 'active_support/core_ext/hash'
28
28
  require 'active_support/core_ext/string'
29
29
  require 'sinatra/api/version'
30
+ require 'sinatra/api/config'
30
31
  require 'sinatra/api/callbacks'
31
32
  require 'sinatra/api/helpers'
33
+ require 'sinatra/api/error_handler'
32
34
  require 'sinatra/api/resource_aliases'
33
35
  require 'sinatra/api/resources'
34
36
  require 'sinatra/api/parameters'
37
+ require 'sinatra/api/parameter_validator'
38
+ require 'sinatra/api/parameter_validators/string_validator'
39
+ require 'sinatra/api/parameter_validators/integer_validator'
40
+ require 'sinatra/api/parameter_validators/float_validator'
35
41
 
36
42
  module Sinatra
37
43
  module API
@@ -49,6 +55,16 @@ module Sinatra
49
55
  # The Sinatra instance that is evaluating the current request.
50
56
  attr_accessor :instance
51
57
 
58
+ # @!attribute instance
59
+ # @return [Sinatra::Base]
60
+ # The Sinatra application.
61
+ attr_accessor :app
62
+
63
+ # @!attribute config
64
+ # @return [Sinatra::API::Config]
65
+ # Runtime configuration options.
66
+ attr_accessor :config
67
+
52
68
  # Parse a JSON construct from a string stream.
53
69
  #
54
70
  # Override this to use a custom JSON parser, if necessary.
@@ -59,45 +75,52 @@ module Sinatra
59
75
  def parse_json(stream)
60
76
  ::JSON.parse(stream)
61
77
  end
78
+
79
+ def configure(options = {})
80
+ self.config = Config.new(options)
81
+ end
82
+
83
+ def process!(params, request)
84
+ request.body.rewind
85
+ raw_json = request.body.read.to_s || ''
86
+
87
+ unless raw_json.empty?
88
+ begin
89
+ params.merge!(parse_json(raw_json))
90
+ rescue ::JSON::ParserError => e
91
+ logger.warn e.message
92
+ logger.warn e.backtrace
93
+
94
+ instance.halt 400, "Malformed JSON content"
95
+ end
96
+ end
97
+ end
62
98
  end
63
99
 
64
100
  ResourcePrefix = '::'
65
101
 
66
102
  def self.registered(app)
67
- base = self
103
+ api = self
104
+ self.app = app
68
105
  self.logger = ActiveSupport::Logger.new(STDOUT)
69
-
106
+ self.logger.level = 100
70
107
  app.helpers Helpers, Parameters, Resources
71
- app.before do
72
- base.instance = self
73
-
74
- @api = { required: {}, optional: {} }
75
- @parent_resource = nil
76
108
 
77
- if api_call?
78
- request.body.rewind
79
- raw_json = request.body.read.to_s || ''
109
+ ParameterValidator.install(api)
80
110
 
81
- unless raw_json.empty?
82
- begin
83
- params.merge!(base.parse_json(raw_json))
84
- rescue ::JSON::ParserError => e
85
- logger.warn e.message
86
- logger.warn e.backtrace
111
+ on :with_errors_setting do |setting|
112
+ app.helpers ErrorHandler if setting
113
+ end
87
114
 
88
- halt 400, "Malformed JSON content"
89
- end
90
- end
91
- end
115
+ on :verbose_setting do |setting|
116
+ logger.level = setting ? 0 : 100
92
117
  end
93
118
 
94
- app.set(:requires) do |*resources|
95
- condition do
96
- @required = resources.collect { |r| r.to_s }
97
- @required.each do |r|
98
- @parent_resource = api_locate_resource(r, @parent_resource)
99
- end
100
- end
119
+ app.before do
120
+ api.instance = self
121
+ api.trigger :request, self
122
+
123
+ api.process!(params, request) if api_call?
101
124
  end
102
125
  end
103
126
  end
@@ -21,6 +21,7 @@
21
21
 
22
22
  module Sinatra
23
23
  module API
24
+ # An event pub/sub interface.
24
25
  module Callbacks
25
26
  attr_accessor :callbacks
26
27
 
@@ -28,10 +29,38 @@ module Sinatra
28
29
  base.callbacks = {}
29
30
  end
30
31
 
32
+ # Add a callback to a given event.
33
+ #
34
+ # @example Listening to :resource_located events
35
+ #
36
+ # Sinatra::API.on :resource_located do |resource, name|
37
+ # if resource.is_a?(Monkey)
38
+ # resource.eat_banana
39
+ # end
40
+ # end
41
+ #
42
+ # @example A callback with an instance method
43
+ #
44
+ # class Monkey
45
+ # def initialize
46
+ # # This means that the monkey will eat a banana everytime a resource
47
+ # # is located.
48
+ # Sinatra::API.on :resource_located, &method(:eat_banana)
49
+ # end
50
+ #
51
+ # def eat_banana(*args)
52
+ # end
53
+ # end
31
54
  def on(event, &callback)
32
55
  (self.callbacks[event.to_sym] ||= []) << callback
33
56
  end
34
57
 
58
+ # Broadcast an event to subscribed callbacks.
59
+ #
60
+ # @example Triggering an event with an argument
61
+ #
62
+ # Sinatra::API.trigger :special_event, 'Special Argument'
63
+ #
35
64
  def trigger(event, *args)
36
65
  callbacks = self.callbacks[event.to_sym] || []
37
66
  callbacks.each do |callback|
@@ -0,0 +1,42 @@
1
+ module Sinatra::API
2
+ class Config
3
+ attr_accessor :with_errors
4
+ attr_accessor :verbose
5
+
6
+ Defaults = {
7
+ with_errors: true,
8
+ verbose: false
9
+ }
10
+
11
+ def initialize(options = {})
12
+ api = Sinatra::API
13
+
14
+ options = {}.merge(Config::Defaults).merge(options)
15
+ options.each_pair do |key, setting|
16
+ unless self.respond_to?(key)
17
+ api.logger.warn "Unknown option #{key} => #{setting}"
18
+ next
19
+ end
20
+
21
+ self[key] = setting if changed?(key, setting)
22
+ end
23
+
24
+ super()
25
+ end
26
+
27
+ def [](key)
28
+ self.send key rescue nil
29
+ end
30
+
31
+ def []=(key, value)
32
+ self.send("#{key}=", value)
33
+ Sinatra::API.trigger "#{key}_setting", value
34
+ end
35
+
36
+ private
37
+
38
+ def changed?(key, value)
39
+ self[key] != value
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,54 @@
1
+ module Sinatra::API
2
+ module ErrorHandler
3
+ def format_api_error(message)
4
+ field_errors = {}
5
+
6
+ message = case
7
+ when message.is_a?(String)
8
+ [ message ]
9
+ when message.is_a?(Array)
10
+ message
11
+ when message.is_a?(Hash)
12
+ field_errors = message
13
+ message.collect { |k,v| v }
14
+ when message.is_a?(DataMapper::Validations::ValidationErrors)
15
+ field_errors = message.to_hash
16
+ message.to_hash.collect { |k,v| v }.flatten
17
+ else
18
+ [ "unexpected response: #{message.class} -> #{message}" ]
19
+ end
20
+
21
+ [ field_errors, message ]
22
+ end
23
+
24
+ def handle_api_error(message = response.body)
25
+ # Support for CORS pre-flight requests
26
+ if request.request_method == 'OPTIONS'
27
+ content_type :text
28
+ halt response.status
29
+ end
30
+
31
+ status response.status
32
+ content_type :json
33
+
34
+ field_errors, message = *format_api_error(message)
35
+
36
+ {
37
+ code: response.status,
38
+ status: 'error',
39
+ messages: message,
40
+ field_errors: field_errors
41
+ }.to_json
42
+ end
43
+
44
+ def self.included(base)
45
+ base.error 400 do
46
+ handle_api_error if api_call?
47
+ end
48
+
49
+ base.error 404 do
50
+ handle_api_error if api_call?
51
+ end
52
+ end
53
+ end
54
+ end
@@ -21,6 +21,7 @@
21
21
 
22
22
  module Sinatra::API
23
23
  module Helpers
24
+ # Determine if this is a JSON API request.
24
25
  def api_call?
25
26
  (request.accept || '').to_s.include?('json') ||
26
27
  (request.content_type||'').to_s.include?('json')
@@ -0,0 +1,90 @@
1
+ module Sinatra::API
2
+ class ParameterValidator
3
+ attr_accessor :typenames
4
+
5
+ def initialize(*typenames)
6
+ self.typenames = typenames.flatten
7
+ super()
8
+ end
9
+
10
+ # Validate a given parameter value.
11
+ #
12
+ # @param [Any] value
13
+ # The parameter value to validate.
14
+ #
15
+ # @param [Hash] options
16
+ # Custom validator options defined by the user.
17
+ #
18
+ # @return [String] Error message if the parameter value is invalid.
19
+ # @return [Any] Any other value means the parameter is valid.
20
+ def validate(value, options = {})
21
+ raise NotImplementedError
22
+ end
23
+
24
+ class << self
25
+ attr_accessor :validators, :api
26
+
27
+ def install(api)
28
+ self.api = api
29
+ self.api.on :parameter_parsed, &method(:run_validators!)
30
+
31
+ install_validators
32
+ end
33
+
34
+ private
35
+
36
+ def run_validators!(key, hash, definition)
37
+ value = hash[key]
38
+ typename = definition[:type]
39
+ validator = definition[:validator]
40
+ validator ||= self.validators[typename]
41
+ definition[:coerce] = true unless definition.has_key?(:coerce)
42
+
43
+ if validator
44
+ # Backwards compatibility:
45
+ #
46
+ # validators were plain procs that received the value to validate.
47
+ if validator.respond_to?(:call)
48
+ rc = validator.call(value)
49
+ # Strongly-defined ParameterValidator objects
50
+ elsif validator.respond_to?(:validate)
51
+ rc = validator.validate(value, definition)
52
+ # ?
53
+ else
54
+ raise 'Invalid ParameterValidator, must respond to #call or #validate'
55
+ end
56
+
57
+ if rc.is_a?(String)
58
+ self.api.instance.halt 400, { :"#{key}" => rc }
59
+ end
60
+
61
+ # coerce the value, if viable
62
+ if validator.respond_to?(:coerce) && definition[:coerce].present?
63
+ hash[key] = validator.coerce(value, definition)
64
+ end
65
+ end
66
+ end
67
+
68
+ # Insantiate an instance of every Sinatra::API::SomethingValidator
69
+ # class defined and register them with the typenames they cover.
70
+ def install_validators
71
+ self.validators = {}
72
+
73
+ validator_klasses = self.api.constants.select { |k| k =~ /Validator$/ }
74
+ validator_klasses.each do |klass_id|
75
+ klass = self.api.const_get(klass_id)
76
+ validator = klass.new
77
+
78
+ unless validator.respond_to?(:validate)
79
+ raise "Invalid ParameterValidator #{klass_id}, must respond to #validate"
80
+ end
81
+
82
+ validator.typenames.each do |typename|
83
+ api.logger.debug "Validator defined: #{typename}"
84
+ validators[typename.to_s.downcase.to_sym] = validator
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,17 @@
1
+ module Sinatra::API
2
+ class FloatValidator < ParameterValidator
3
+ def initialize
4
+ super(:float)
5
+ end
6
+
7
+ def validate(value, options)
8
+ Float(value)
9
+ rescue
10
+ "Not a valid float."
11
+ end
12
+
13
+ def coerce(value, options)
14
+ Float(value)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module Sinatra::API
2
+ class IntegerValidator < ParameterValidator
3
+ def initialize
4
+ super(:integer)
5
+ end
6
+
7
+ def validate(value, options)
8
+ Integer(value)
9
+ rescue
10
+ "Not a valid integer."
11
+ end
12
+
13
+ def coerce(value, options)
14
+ Integer(value)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module Sinatra::API
2
+ class StringValidator < ParameterValidator
3
+ def initialize
4
+ super(:string, String)
5
+ end
6
+
7
+ def validate(value, options)
8
+ unless value.is_a?(String)
9
+ return "Expected value to be of type String, got #{value.class.name}"
10
+ end
11
+
12
+ if options[:format] && !value =~ options[:format]
13
+ return "Invalid format."
14
+ end
15
+ end
16
+ end
17
+ end
@@ -51,13 +51,15 @@ module Sinatra::API
51
51
  # The supplied value passed to validation blocks is not pre-processed,
52
52
  # so you must make sure that you check for nils or bad values in validator blocks!
53
53
  def api_required!(args, h = params)
54
+ args = api_parameters_to_hash(args) if args.is_a?(Array)
55
+
54
56
  args.each_pair do |name, cnd|
55
57
  if cnd.is_a?(Hash)
56
58
  api_required!(cnd, h[name])
57
59
  next
58
60
  end
59
61
 
60
- parse_api_argument(h, name, cnd, :required)
62
+ parse_api_parameter(name, cnd, :required, h)
61
63
  end
62
64
  end
63
65
 
@@ -66,26 +68,33 @@ module Sinatra::API
66
68
  #
67
69
  # @see #api_required!
68
70
  def api_optional!(args, h = params)
71
+ args = api_parameters_to_hash(args) if args.is_a?(Array)
72
+
69
73
  args.each_pair { |name, cnd|
70
74
  if cnd.is_a?(Hash)
71
75
  api_optional!(cnd, h[name])
72
76
  next
73
77
  end
74
78
 
75
- parse_api_argument(h, name, cnd, :optional)
79
+ parse_api_parameter(name, cnd, :optional, h)
76
80
  }
77
81
  end
78
82
 
83
+ def api_parameter!(id, options = {}, hash = params)
84
+ parameter_type = options[:required] ? :required : :optional
85
+ parameter_validator = options[:validator]
86
+
87
+ parse_api_parameter(id, parameter_validator, parameter_type, hash, options)
88
+ end
89
+
79
90
  # Consumes supplied parameters with the given keys from the API
80
91
  # parameter map, and yields the consumed values for processing by
81
92
  # the supplied block (if any).
82
93
  #
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
94
+ # This is useful when a certain parameter does not correspond to a model
95
+ # attribute and needs to be renamed, or is used only in a validation context.
88
96
  #
97
+ # Use #api_transform! if you only need to convert the value or process it.
89
98
  def api_consume!(keys)
90
99
  out = nil
91
100
 
@@ -105,23 +114,38 @@ module Sinatra::API
105
114
  out
106
115
  end
107
116
 
117
+ # Transform the value for the given parameter in-place. Useful for
118
+ # post-processing or converting raw values.
119
+ #
120
+ # @param [String, Symbol] key
121
+ # The key of the parameter defined earlier.
122
+ #
123
+ # @param [#call] handler
124
+ # A callable construct that will receive the original value and should
125
+ # return the transformed one.
108
126
  def api_transform!(key, &handler)
109
- if val = @api[:required][key.to_sym]
110
- @api[:required][key.to_sym] = yield(val) if block_given?
127
+ key = key.to_sym
128
+
129
+ if val = @api[:required][key]
130
+ @api[:required][key] = yield(val) if block_given?
111
131
  end
112
132
 
113
- if val = @api[:optional][key.to_sym]
114
- @api[:optional][key.to_sym] = yield(val) if block_given?
133
+ if val = @api[:optional][key]
134
+ @api[:optional][key] = yield(val) if block_given?
115
135
  end
116
136
  end
117
137
 
138
+ # Is the specified *optional* parameter supplied by the request?
118
139
  def api_has_param?(key)
119
140
  @api[:optional].has_key?(key)
120
141
  end
142
+ alias_method :has_api_parameter?, :api_has_param?
121
143
 
144
+ # Get the value of the given API parameter, if any.
122
145
  def api_param(key)
123
146
  @api[:optional][key.to_sym] || @api[:required][key.to_sym]
124
147
  end
148
+ alias_method :api_parameter, :api_param
125
149
 
126
150
  # Returns a Hash of the *supplied* request parameters. Rejects
127
151
  # any parameter that was not defined in the REQUIRED or OPTIONAL
@@ -135,18 +159,22 @@ module Sinatra::API
135
159
  end
136
160
 
137
161
  def api_clear!()
138
- @api = { required: {}, optional: {} }
162
+ @api = {
163
+ required: {}.with_indifferent_access,
164
+ optional: {}.with_indifferent_access
165
+ }.with_indifferent_access
139
166
  end
140
-
141
167
  alias_method :api_reset!, :api_clear!
142
168
 
143
169
  private
144
170
 
145
- def parse_api_argument(h = params, name, cnd, type)
146
- cnd ||= lambda { |*_| true }
171
+ def parse_api_parameter(name, cnd, type, h = params, options = {})
172
+ # cnd ||= lambda { |*_| true }
147
173
  name = name.to_s
148
174
 
149
- unless [:required, :optional].include?(type)
175
+ options[:validator] ||= cnd
176
+
177
+ unless [ :required, :optional ].include?(type)
150
178
  raise ArgumentError, 'API Argument type must be either :required or :optional'
151
179
  end
152
180
 
@@ -155,13 +183,27 @@ module Sinatra::API
155
183
  halt 400, "Missing required parameter :#{name}"
156
184
  end
157
185
  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
186
+ Sinatra::API.trigger :parameter_parsed, name, h, options
187
+
188
+ # if cnd.respond_to?(:call)
189
+ # errmsg = cnd.call(h[name])
190
+ # halt 400, { :"#{name}" => errmsg } if errmsg && errmsg.is_a?(String)
191
+ # end
162
192
 
163
193
  @api[type][name.to_sym] = h[name]
164
194
  end
165
195
  end
196
+
197
+ def api_parameters_to_hash(args)
198
+ converted = {}
199
+ args.each { |name| converted[name] = nil }
200
+ converted
201
+ end
202
+
203
+ def self.included(app)
204
+ Sinatra::API.on :request do |request_scope|
205
+ request_scope.instance_eval &:api_reset!
206
+ end
207
+ end
166
208
  end
167
209
  end