sinatra-api 1.0.2 → 1.1.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 --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