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 +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
|