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 +1 -1
- data/lib/sinatra/api.rb +50 -27
- data/lib/sinatra/api/callbacks.rb +29 -0
- data/lib/sinatra/api/config.rb +42 -0
- data/lib/sinatra/api/error_handler.rb +54 -0
- data/lib/sinatra/api/helpers.rb +1 -0
- data/lib/sinatra/api/parameter_validator.rb +90 -0
- data/lib/sinatra/api/parameter_validators/float_validator.rb +17 -0
- data/lib/sinatra/api/parameter_validators/integer_validator.rb +17 -0
- data/lib/sinatra/api/parameter_validators/string_validator.rb +17 -0
- data/lib/sinatra/api/parameters.rb +62 -20
- data/lib/sinatra/api/resource_aliases.rb +1 -1
- data/lib/sinatra/api/resources.rb +22 -3
- data/lib/sinatra/api/version.rb +1 -1
- data/spec/helpers/api_calls.rb +33 -0
- data/spec/helpers/rack_test_api_response.rb +45 -0
- data/spec/helpers/rspec_api_response_matchers.rb +135 -0
- data/spec/integration/parameter_validators/float_validator_spec.rb +85 -0
- data/spec/integration/parameter_validators/integer_validator_spec.rb +66 -0
- data/spec/integration/parameter_validators/string_validator_spec.rb +25 -0
- data/spec/integration/parameter_validators_spec.rb +3 -0
- data/spec/integration/parameters_spec.rb +144 -0
- data/spec/integration/resources_spec.rb +17 -0
- data/spec/spec_helper.rb +5 -11
- data/spec/unit/callbacks_spec.rb +1 -98
- metadata +16 -2
- data/spec/integration/helpers_spec.rb +0 -92
data/.yardopts
CHANGED
@@ -1 +1 @@
|
|
1
|
-
--protected
|
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
|
-
|
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
|
-
|
78
|
-
request.body.rewind
|
79
|
-
raw_json = request.body.read.to_s || ''
|
109
|
+
ParameterValidator.install(api)
|
80
110
|
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
-
|
89
|
-
|
90
|
-
end
|
91
|
-
end
|
115
|
+
on :verbose_setting do |setting|
|
116
|
+
logger.level = setting ? 0 : 100
|
92
117
|
end
|
93
118
|
|
94
|
-
app.
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
data/lib/sinatra/api/helpers.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
84
|
-
#
|
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
|
-
|
110
|
-
|
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
|
114
|
-
@api[:optional][key
|
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 = {
|
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
|
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
|
-
|
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
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
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
|