api-regulator 0.1.17 → 0.1.18
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.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/lib/api_regulator/api.rb +7 -3
- data/lib/api_regulator/controller_mixin.rb +15 -2
- data/lib/api_regulator/open_api_generator.rb +11 -3
- data/lib/api_regulator/param.rb +64 -5
- data/lib/api_regulator/shared_schema.rb +2 -2
- data/lib/api_regulator/validation_error.rb +16 -1
- data/lib/api_regulator/validator.rb +68 -16
- data/lib/api_regulator/version.rb +1 -1
- data/lib/api_regulator/webhook.rb +2 -2
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 14218b23d39f49f124677c3c4e1a20b9ce71ddc6c4908f45850b6d65a898ecb0
|
4
|
+
data.tar.gz: a60a16f1df4d29586b2a85bbfcd7a37287b7fdfdc56574651eb7b89b595e4481
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c2bd79c082c6e2f6f3eaf220947b25c01d7360c1b0486acae1f402065a3fb3be8db454c3fc651f45800ee805f4f4edb6f59787c7148a2128095b8dd20b07820b
|
7
|
+
data.tar.gz: d176e785342fb4ec6674621651a439cc0d2c8fefb425fb2b5dbc447bd4aaf9e11eb8b2e1e1d0f09ad1dc5a658a15a0e5bc23f4c1c8524a711c85f5cff3b279fa
|
data/Gemfile.lock
CHANGED
data/lib/api_regulator/api.rb
CHANGED
@@ -16,8 +16,8 @@ module ApiRegulator
|
|
16
16
|
instance_eval(&block) if block_given?
|
17
17
|
end
|
18
18
|
|
19
|
-
def param(name, type = nil, item_type: nil, desc: "", location: :body, **options, &block)
|
20
|
-
param = Param.new(name, type, item_type: item_type, desc: desc, location: location, api: self, **options, &block)
|
19
|
+
def param(name, type = nil, item_type: nil, desc: "", location: :body, allow_arbitrary_keys: false, **options, &block)
|
20
|
+
param = Param.new(name, type, item_type: item_type, desc: desc, location: location, api: self, allow_arbitrary_keys: allow_arbitrary_keys, **options, &block)
|
21
21
|
@params << param
|
22
22
|
end
|
23
23
|
|
@@ -100,8 +100,12 @@ module ApiRegulator
|
|
100
100
|
@api_definitions ||= []
|
101
101
|
end
|
102
102
|
|
103
|
+
def set_api_definitions(defs)
|
104
|
+
@api_definitions = defs
|
105
|
+
end
|
106
|
+
|
103
107
|
def reset_api_definitions
|
104
|
-
|
108
|
+
set_api_definitions([])
|
105
109
|
end
|
106
110
|
end
|
107
111
|
end
|
@@ -1,15 +1,16 @@
|
|
1
1
|
module ApiRegulator
|
2
2
|
module ControllerMixin
|
3
3
|
def validate_params!
|
4
|
+
check_for_extra_params!
|
5
|
+
|
4
6
|
validator_class = Validator.get(params[:controller], params[:action])
|
5
7
|
|
6
8
|
unless validator_class
|
7
9
|
raise "No validator found for HTTP method #{request.method} and API path #{api_definition.path}"
|
8
10
|
end
|
9
|
-
|
10
11
|
validator = validator_class.new(api_params)
|
11
12
|
unless validator.valid?(params[:action].to_sym)
|
12
|
-
raise ApiRegulator::
|
13
|
+
raise ApiRegulator::InvalidParams.new(validator.errors)
|
13
14
|
end
|
14
15
|
end
|
15
16
|
|
@@ -63,5 +64,17 @@ module ApiRegulator
|
|
63
64
|
end
|
64
65
|
end
|
65
66
|
end
|
67
|
+
|
68
|
+
def check_for_extra_params!
|
69
|
+
actual_keys = Validator.flatten_keys(params.to_unsafe_h)
|
70
|
+
|
71
|
+
# Identify extra keys
|
72
|
+
extra_keys = Validator.find_extra_keys(actual_keys, api_definition.params)
|
73
|
+
|
74
|
+
# Raise an error if there are unexpected keys
|
75
|
+
unless extra_keys.empty?
|
76
|
+
raise ApiRegulator::UnexpectedParams.new(extra_keys)
|
77
|
+
end
|
78
|
+
end
|
66
79
|
end
|
67
80
|
end
|
@@ -99,6 +99,7 @@ module ApiRegulator
|
|
99
99
|
def self.generate_object_schema(param)
|
100
100
|
object_schema = expand_nested_params(param.children)
|
101
101
|
object_schema[:description] = param.desc if param.desc.present?
|
102
|
+
object_schema[:type] = param.allow_nil? ? ['object', 'null'] : 'object'
|
102
103
|
object_schema
|
103
104
|
end
|
104
105
|
|
@@ -119,10 +120,17 @@ module ApiRegulator
|
|
119
120
|
|
120
121
|
if param.parameter?
|
121
122
|
schema[:in] = param.location
|
122
|
-
schema[:schema] = { type: param.
|
123
|
+
schema[:schema] = { type: param.schema_type }
|
124
|
+
if param.type == :object && param.allow_arbitrary_keys
|
125
|
+
schema[:schema][:additionalProperties] = true
|
126
|
+
end
|
127
|
+
if param.type == :object && param.location == :query
|
128
|
+
schema[:explode] = true
|
129
|
+
schema[:style] = "deepObject"
|
130
|
+
end
|
123
131
|
generate_param_schema_details(param, schema[:schema])
|
124
132
|
else
|
125
|
-
schema[:type] = param.
|
133
|
+
schema[:type] = param.schema_type
|
126
134
|
generate_param_schema_details(param, schema)
|
127
135
|
end
|
128
136
|
schema
|
@@ -151,7 +159,7 @@ module ApiRegulator
|
|
151
159
|
end
|
152
160
|
|
153
161
|
if numericality = param.options[:numericality]
|
154
|
-
schema[:type] = numericality[:only_integer] ||
|
162
|
+
schema[:type] = numericality[:only_integer] || param.type == :integer ? 'integer' : 'number'
|
155
163
|
schema[:minimum] = numericality[:greater_than_or_equal_to] if numericality[:greater_than_or_equal_to]
|
156
164
|
schema[:maximum] = numericality[:less_than_or_equal_to] if numericality[:less_than_or_equal_to]
|
157
165
|
schema[:exclusiveMinimum] = numericality[:greater_than] if numericality[:greater_than]
|
data/lib/api_regulator/param.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
module ApiRegulator
|
2
2
|
class Param
|
3
|
-
attr_reader :name, :type, :options, :item_type, :desc, :location, :children, :api
|
3
|
+
attr_reader :name, :type, :options, :item_type, :desc, :location, :children, :api, :allow_arbitrary_keys
|
4
4
|
|
5
|
-
def initialize(name, type = nil, item_type: nil, desc: "", location: :body, api: nil, **options, &block)
|
5
|
+
def initialize(name, type = nil, item_type: nil, desc: "", location: :body, api: nil, allow_arbitrary_keys: false, **options, &block)
|
6
6
|
@name = name
|
7
7
|
@type = type&.to_sym || (block_given? ? :object : :string)
|
8
8
|
@item_type = item_type
|
@@ -11,12 +11,13 @@ module ApiRegulator
|
|
11
11
|
@options = options
|
12
12
|
@children = []
|
13
13
|
@api = api
|
14
|
+
@allow_arbitrary_keys = allow_arbitrary_keys
|
14
15
|
|
15
16
|
instance_eval(&block) if block_given?
|
16
17
|
end
|
17
18
|
|
18
|
-
def param(name, type = nil, item_type: nil, desc: "", location: :body, **options, &block)
|
19
|
-
child = Param.new(name, type, item_type: item_type, desc: desc, location: location, api: api, **options, &block)
|
19
|
+
def param(name, type = nil, item_type: nil, desc: "", location: :body, allow_arbitrary_keys: false, **options, &block)
|
20
|
+
child = Param.new(name, type, item_type: item_type, desc: desc, location: location, api: api, allow_arbitrary_keys: allow_arbitrary_keys, **options, &block)
|
20
21
|
@children << child
|
21
22
|
end
|
22
23
|
|
@@ -46,7 +47,9 @@ module ApiRegulator
|
|
46
47
|
return false if @options[:presence].nil?
|
47
48
|
|
48
49
|
if @options[:presence].is_a?(Hash)
|
49
|
-
if
|
50
|
+
if @options[:presence].key?(:allow_nil)
|
51
|
+
!@options[:presence][:allow_nil]
|
52
|
+
elsif context.nil?
|
50
53
|
true # No context provided
|
51
54
|
elsif @options[:presence][:required_on].present?
|
52
55
|
Array(@options[:presence][:required_on]).map(&:to_sym).include?(context.to_sym)
|
@@ -60,6 +63,54 @@ module ApiRegulator
|
|
60
63
|
end
|
61
64
|
end
|
62
65
|
|
66
|
+
def allow_nil?
|
67
|
+
@options.any? do |_, opts|
|
68
|
+
opts.is_a?(Hash) && opts[:allow_nil]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def allowed_keys(parent_key = nil)
|
73
|
+
key = parent_key.present? ? "#{parent_key}.#{name}" : name.to_s
|
74
|
+
key += "[]" if array?
|
75
|
+
|
76
|
+
if options[:ref]
|
77
|
+
shared_schema = ApiRegulator.shared_schema(options[:ref])
|
78
|
+
shared_schema.params.flat_map do |param|
|
79
|
+
param.allowed_keys
|
80
|
+
end
|
81
|
+
elsif children.any?
|
82
|
+
key = nil if key == "root"
|
83
|
+
child_keys = children.flat_map do |child|
|
84
|
+
child.allowed_keys(key)
|
85
|
+
end
|
86
|
+
child_keys << key if allow_nil?
|
87
|
+
child_keys
|
88
|
+
else
|
89
|
+
key
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def allowed_arbitrary_keys(parent_key = nil)
|
94
|
+
key = parent_key.present? ? "#{parent_key}.#{name}" : name.to_s
|
95
|
+
key += "[]" if array?
|
96
|
+
|
97
|
+
if options[:ref]
|
98
|
+
shared_schema = ApiRegulator.shared_schema(options[:ref])
|
99
|
+
shared_schema.params.flat_map do |param|
|
100
|
+
param.allowed_arbitrary_keys
|
101
|
+
end.compact
|
102
|
+
elsif children.any?
|
103
|
+
key = nil if key == "root"
|
104
|
+
child_keys = children.flat_map do |child|
|
105
|
+
child.allowed_arbitrary_keys(key)
|
106
|
+
end.compact
|
107
|
+
child_keys << key if allow_nil? && allow_arbitrary_keys
|
108
|
+
child_keys
|
109
|
+
else
|
110
|
+
key if allow_arbitrary_keys
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
63
114
|
def body?
|
64
115
|
location == :body
|
65
116
|
end
|
@@ -83,5 +134,13 @@ module ApiRegulator
|
|
83
134
|
def array?
|
84
135
|
type.to_sym == :array
|
85
136
|
end
|
137
|
+
|
138
|
+
def schema_type
|
139
|
+
if allow_nil?
|
140
|
+
[type.to_s.downcase, "null"]
|
141
|
+
else
|
142
|
+
type.to_s.downcase
|
143
|
+
end
|
144
|
+
end
|
86
145
|
end
|
87
146
|
end
|
@@ -10,8 +10,8 @@ module ApiRegulator
|
|
10
10
|
instance_eval(&block) if block_given?
|
11
11
|
end
|
12
12
|
|
13
|
-
def param(name, type = nil, item_type: nil, desc: "", location: :body, **options, &block)
|
14
|
-
param = Param.new(name, type, item_type: item_type, desc: desc, location: location, **options, &block)
|
13
|
+
def param(name, type = nil, item_type: nil, desc: "", location: :body, allow_arbitrary_keys: false, **options, &block)
|
14
|
+
param = Param.new(name, type, item_type: item_type, desc: desc, location: location, allow_arbitrary_keys: allow_arbitrary_keys, **options, &block)
|
15
15
|
@params << param
|
16
16
|
end
|
17
17
|
|
@@ -1,6 +1,21 @@
|
|
1
1
|
module ApiRegulator
|
2
2
|
class ValidationError < StandardError
|
3
|
-
attr_reader :
|
3
|
+
attr_reader :details
|
4
|
+
end
|
5
|
+
|
6
|
+
class UnexpectedParams < ValidationError
|
7
|
+
def initialize(unexpected_keys)
|
8
|
+
@details = {}
|
9
|
+
unexpected_keys.each do |key|
|
10
|
+
@details[key] = ["is unexpected"]
|
11
|
+
end
|
12
|
+
|
13
|
+
super("Unexpected parameters")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class InvalidParams < ValidationError
|
18
|
+
attr_reader :errors
|
4
19
|
|
5
20
|
def initialize(errors)
|
6
21
|
@errors = errors
|
@@ -193,11 +193,8 @@ module ApiRegulator
|
|
193
193
|
def validate_nested_object(attribute, nested_validator_class, param)
|
194
194
|
raw_value = @raw_attributes[attribute]
|
195
195
|
|
196
|
-
# Skip validation if optional and not present
|
197
|
-
return if raw_value.nil? && param.options[:optional]
|
198
|
-
|
199
196
|
if raw_value.nil?
|
200
|
-
errors.add(attribute, "can't be blank") if param.
|
197
|
+
errors.add(attribute, "can't be blank") if param.required?(validation_context)
|
201
198
|
return
|
202
199
|
end
|
203
200
|
|
@@ -245,13 +242,10 @@ module ApiRegulator
|
|
245
242
|
include ActiveModel::Attributes
|
246
243
|
include AttributeDefinitionMixin
|
247
244
|
|
248
|
-
@validators = {}
|
249
|
-
|
250
245
|
def self.build_all(api_definitions)
|
251
246
|
api_definitions.each do |api_definition|
|
252
|
-
class_name =
|
247
|
+
class_name = build_class_name(api_definition.controller_path, api_definition.action_name)
|
253
248
|
validator_class = build_class(api_definition.params, api_definition.action_name)
|
254
|
-
@validators[[api_definition.controller_path, api_definition.action_name]] = validator_class
|
255
249
|
Validator.const_set(class_name, validator_class)
|
256
250
|
end
|
257
251
|
end
|
@@ -259,17 +253,23 @@ module ApiRegulator
|
|
259
253
|
def self.build_response_validators(api_definitions = ApiRegulator.api_definitions)
|
260
254
|
api_definitions.each do |api_definition|
|
261
255
|
api_definition.responses.each do |code, params|
|
262
|
-
class_name =
|
263
|
-
|
256
|
+
class_name = build_class_name(api_definition.controller_path, api_definition.action_name, code)
|
264
257
|
validator_class = build_class(params.children, api_definition.action_name)
|
265
|
-
@validators[[api_definition.controller_path, api_definition.action_name, code]] = validator_class
|
266
258
|
Validator.const_set(class_name, validator_class)
|
267
259
|
end
|
268
260
|
end
|
269
261
|
end
|
270
262
|
|
271
|
-
def self.
|
272
|
-
|
263
|
+
def self.build_class_name(controller_path, action, code = nil, prefix: false)
|
264
|
+
class_name = "#{controller_path}/#{action}"
|
265
|
+
class_name += "/Response#{code}" if code.present?
|
266
|
+
class_name = class_name.gsub("/", "_").camelcase
|
267
|
+
class_name = "ApiRegulator::Validator::" + class_name if prefix
|
268
|
+
class_name
|
269
|
+
end
|
270
|
+
|
271
|
+
def self.get(controller_path, action, code = nil)
|
272
|
+
build_class_name(controller_path, action, code, prefix: true).constantize
|
273
273
|
end
|
274
274
|
|
275
275
|
def self.build_class(params, action_name)
|
@@ -294,6 +294,10 @@ module ApiRegulator
|
|
294
294
|
end
|
295
295
|
|
296
296
|
def self.validate_response(controller, action, code, body)
|
297
|
+
body = {} if body.empty?
|
298
|
+
|
299
|
+
check_for_extra_response_params!(controller, action, code, body)
|
300
|
+
|
297
301
|
validator_class = get(controller, action, code)
|
298
302
|
|
299
303
|
unless validator_class
|
@@ -302,12 +306,60 @@ module ApiRegulator
|
|
302
306
|
|
303
307
|
validator = validator_class.new(body)
|
304
308
|
unless validator.valid?(:response) # using :response for validation context
|
305
|
-
raise ApiRegulator::
|
309
|
+
raise ApiRegulator::InvalidParams.new(validator.errors)
|
306
310
|
end
|
307
311
|
end
|
308
312
|
|
309
|
-
|
310
|
-
|
313
|
+
|
314
|
+
def self.check_for_extra_response_params!(controller, action, code, body)
|
315
|
+
actual_keys = flatten_keys(body)
|
316
|
+
|
317
|
+
api_definition ||= ApiRegulator.api_definitions.find do |d|
|
318
|
+
d.controller_path == controller && d.action_name == action.to_s
|
319
|
+
end
|
320
|
+
response_definition = api_definition.responses[code]
|
321
|
+
|
322
|
+
# Identify extra keys
|
323
|
+
extra_keys = find_extra_keys(actual_keys, [response_definition])
|
324
|
+
|
325
|
+
# Raise an error if there are unexpected keys
|
326
|
+
unless extra_keys.empty?
|
327
|
+
raise ApiRegulator::UnexpectedParams.new(extra_keys)
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
def self.flatten_keys(hash, parent_key = nil)
|
332
|
+
hash.each_with_object([]) do |(key, value), keys|
|
333
|
+
next if ["controller", "action"].include?(key)
|
334
|
+
|
335
|
+
full_key = parent_key ? "#{parent_key}.#{key}" : key.to_s
|
336
|
+
|
337
|
+
if value.is_a?(Hash)
|
338
|
+
keys.concat(flatten_keys(value, full_key))
|
339
|
+
elsif value.is_a?(Array)
|
340
|
+
value.each_with_index do |item, index|
|
341
|
+
if item.is_a?(Hash)
|
342
|
+
keys.concat(flatten_keys(item, "#{full_key}[#{index}]"))
|
343
|
+
else
|
344
|
+
keys << "#{full_key}[#{index}]"
|
345
|
+
end
|
346
|
+
end
|
347
|
+
else
|
348
|
+
keys << full_key
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
def self.find_extra_keys(actual_keys, params)
|
354
|
+
allowed_keys = params.flat_map(&:allowed_keys)
|
355
|
+
allowed_arbitrary_keys = params.flat_map(&:allowed_arbitrary_keys).compact
|
356
|
+
extras = []
|
357
|
+
actual_keys.each do |key|
|
358
|
+
next if allowed_keys.include?(key.gsub(/\[\d+\]/, "[]"))
|
359
|
+
next if allowed_arbitrary_keys.any? { |aribitrary_key| key.starts_with?(aribitrary_key) }
|
360
|
+
extras << key
|
361
|
+
end
|
362
|
+
extras
|
311
363
|
end
|
312
364
|
end
|
313
365
|
end
|
@@ -13,8 +13,8 @@ module ApiRegulator
|
|
13
13
|
instance_eval(&block) if block_given?
|
14
14
|
end
|
15
15
|
|
16
|
-
def param(name, type = nil, item_type: nil, desc: "", location: :body, **options, &block)
|
17
|
-
param = Param.new(name, type, item_type: item_type, desc: desc, location: location, **options, &block)
|
16
|
+
def param(name, type = nil, item_type: nil, desc: "", location: :body, allow_arbitrary_keys: false, **options, &block)
|
17
|
+
param = Param.new(name, type, item_type: item_type, desc: desc, location: location, allow_arbitrary_keys: allow_arbitrary_keys, **options, &block)
|
18
18
|
@params << param
|
19
19
|
end
|
20
20
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: api-regulator
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.18
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Geoff Massanek
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-02-
|
11
|
+
date: 2025-02-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|