api-regulator 0.1.11 → 0.1.13
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 +28 -21
- data/README.md +8 -8
- data/lib/api_regulator/api.rb +1 -1
- data/lib/api_regulator/controller_mixin.rb +1 -1
- data/lib/api_regulator/open_api_generator.rb +2 -2
- data/lib/api_regulator/param.rb +24 -5
- data/lib/api_regulator/validator.rb +32 -24
- data/lib/api_regulator/version.rb +1 -1
- 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: dd01d986f35199767ed09aeca6b5a5b0b19441113fa10b2fa5b34a511a4773ab
|
4
|
+
data.tar.gz: 02e722bd4b5df9a728a9ff1a339922cd891e9ae0eca9bf516b653bab3132c379
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 42b77076b73dffc526ef004bc182c63cd3c45bbfaa8d5102796a92964c951da6351311bce75a89955f74cb851221b566c3188f5477ccb51355c09c588d70fc59
|
7
|
+
data.tar.gz: 50888b26e95137d81dad5b8cb2777a065613f87fe537e2a1b456bcfe986b41a9e2fa45ce23c77688098421c4e5f8aebe31184f3065764e83dbf44d3c9e6caa4b
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
api-regulator (0.1.
|
4
|
+
api-regulator (0.1.13)
|
5
5
|
activemodel (~> 8.0)
|
6
6
|
activesupport (~> 8.0)
|
7
7
|
|
@@ -81,26 +81,27 @@ GEM
|
|
81
81
|
uri (>= 0.13.1)
|
82
82
|
base64 (0.2.0)
|
83
83
|
benchmark (0.4.0)
|
84
|
-
bigdecimal (3.1.
|
84
|
+
bigdecimal (3.1.9)
|
85
85
|
builder (3.3.0)
|
86
86
|
byebug (11.1.3)
|
87
|
-
concurrent-ruby (1.3.
|
88
|
-
connection_pool (2.
|
87
|
+
concurrent-ruby (1.3.5)
|
88
|
+
connection_pool (2.5.0)
|
89
89
|
crass (1.0.6)
|
90
90
|
date (3.4.1)
|
91
91
|
diff-lcs (1.5.1)
|
92
92
|
drb (2.2.1)
|
93
|
-
erubi (1.13.
|
93
|
+
erubi (1.13.1)
|
94
94
|
globalid (1.2.1)
|
95
95
|
activesupport (>= 6.1)
|
96
|
-
i18n (1.14.
|
96
|
+
i18n (1.14.7)
|
97
97
|
concurrent-ruby (~> 1.0)
|
98
|
-
io-console (0.
|
99
|
-
irb (1.
|
98
|
+
io-console (0.8.0)
|
99
|
+
irb (1.15.1)
|
100
|
+
pp (>= 0.6.0)
|
100
101
|
rdoc (>= 4.0.0)
|
101
102
|
reline (>= 0.4.2)
|
102
|
-
logger (1.6.
|
103
|
-
loofah (2.
|
103
|
+
logger (1.6.5)
|
104
|
+
loofah (2.24.0)
|
104
105
|
crass (~> 1.0.2)
|
105
106
|
nokogiri (>= 1.12.0)
|
106
107
|
mail (2.8.1)
|
@@ -112,7 +113,7 @@ GEM
|
|
112
113
|
mini_mime (1.1.5)
|
113
114
|
mini_portile2 (2.8.8)
|
114
115
|
minitest (5.25.4)
|
115
|
-
net-imap (0.5.
|
116
|
+
net-imap (0.5.5)
|
116
117
|
date
|
117
118
|
net-protocol
|
118
119
|
net-pop (0.1.2)
|
@@ -122,18 +123,23 @@ GEM
|
|
122
123
|
net-smtp (0.5.0)
|
123
124
|
net-protocol
|
124
125
|
nio4r (2.7.4)
|
125
|
-
nokogiri (1.
|
126
|
+
nokogiri (1.18.2)
|
126
127
|
mini_portile2 (~> 2.8.2)
|
127
128
|
racc (~> 1.4)
|
128
|
-
nokogiri (1.
|
129
|
+
nokogiri (1.18.2-arm64-darwin)
|
129
130
|
racc (~> 1.4)
|
130
|
-
|
131
|
+
pp (0.6.2)
|
132
|
+
prettyprint
|
133
|
+
prettyprint (0.2.0)
|
134
|
+
psych (5.2.3)
|
135
|
+
date
|
131
136
|
stringio
|
132
137
|
racc (1.8.1)
|
133
138
|
rack (3.1.8)
|
134
|
-
rack-session (2.
|
139
|
+
rack-session (2.1.0)
|
140
|
+
base64 (>= 0.1.0)
|
135
141
|
rack (>= 3.0.0)
|
136
|
-
rack-test (2.
|
142
|
+
rack-test (2.2.0)
|
137
143
|
rack (>= 1.3)
|
138
144
|
rackup (2.2.1)
|
139
145
|
rack (>= 3)
|
@@ -167,9 +173,9 @@ GEM
|
|
167
173
|
thor (~> 1.0, >= 1.2.2)
|
168
174
|
zeitwerk (~> 2.6)
|
169
175
|
rake (13.2.1)
|
170
|
-
rdoc (6.
|
176
|
+
rdoc (6.11.0)
|
171
177
|
psych (>= 4.0.0)
|
172
|
-
reline (0.
|
178
|
+
reline (0.6.0)
|
173
179
|
io-console (~> 0.5)
|
174
180
|
rspec (3.13.0)
|
175
181
|
rspec-core (~> 3.13.0)
|
@@ -191,8 +197,8 @@ GEM
|
|
191
197
|
rspec-expectations (~> 3.10)
|
192
198
|
rspec-mocks (~> 3.10)
|
193
199
|
rspec-support (~> 3.10)
|
194
|
-
rspec-support (3.13.
|
195
|
-
securerandom (0.4.
|
200
|
+
rspec-support (3.13.2)
|
201
|
+
securerandom (0.4.1)
|
196
202
|
stringio (3.1.2)
|
197
203
|
thor (1.3.2)
|
198
204
|
timeout (0.4.3)
|
@@ -200,7 +206,8 @@ GEM
|
|
200
206
|
concurrent-ruby (~> 1.0)
|
201
207
|
uri (1.0.2)
|
202
208
|
useragent (0.16.11)
|
203
|
-
websocket-driver (0.7.
|
209
|
+
websocket-driver (0.7.7)
|
210
|
+
base64
|
204
211
|
websocket-extensions (>= 0.1.0)
|
205
212
|
websocket-extensions (0.1.5)
|
206
213
|
zeitwerk (2.7.1)
|
data/README.md
CHANGED
@@ -19,15 +19,7 @@ ApiRegulator relies on **Active Model validations** for parameter validation, ma
|
|
19
19
|
Share common response schemas across multiple endpoints for DRY definitions.
|
20
20
|
|
21
21
|
## ToDo
|
22
|
-
- [ ] More tests
|
23
|
-
- [ ] Invalid Configurations (errors for invalid types)
|
24
|
-
- [ ] Empty Shared Schemas
|
25
|
-
- [ ] Custom Length and Numericality Options
|
26
|
-
- [ ] Publish to rubygems
|
27
|
-
- [ ] See if we're missing any other OpenAPI directives we could use
|
28
|
-
- [ ] nullable params
|
29
22
|
- [ ] Handling of extra undocumented params
|
30
|
-
- [ ] Set up CI / CD
|
31
23
|
|
32
24
|
## Installation
|
33
25
|
|
@@ -149,5 +141,13 @@ Generate OpenAPI documentation using the provided Rake tasks:
|
|
149
141
|
4. Push to the branch (git push origin feature/new-feature).
|
150
142
|
5. Open a pull request.
|
151
143
|
|
144
|
+
## Releasing a new version
|
145
|
+
`api-regulator` is published to [rubygems](https://rubygems.org/gems/api-regulator). To release a new version,
|
146
|
+
1. In your PR, bump the version in `lib/api_regulator/version.rb`
|
147
|
+
2. Run `bundle install` (this should update your `Gemfile.lock`)
|
148
|
+
3. Go through code review and merge your PR
|
149
|
+
4. Trigger the [`Release Gem`](https://github.com/Stellarcred/api-regulator/actions/workflows/release.yml) workflow from the main branch.
|
150
|
+
5. Verify your new version is available in [rubygems](https://rubygems.org/gems/api-regulator).
|
151
|
+
|
152
152
|
## License
|
153
153
|
This gem is available as open-source software under the [MIT License](https://mit-license.org/).
|
data/lib/api_regulator/api.rb
CHANGED
@@ -17,7 +17,7 @@ module ApiRegulator
|
|
17
17
|
end
|
18
18
|
|
19
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, **options, &block)
|
20
|
+
param = Param.new(name, type, item_type: item_type, desc: desc, location: location, api: self, **options, &block)
|
21
21
|
@params << param
|
22
22
|
end
|
23
23
|
|
@@ -78,7 +78,7 @@ module ApiRegulator
|
|
78
78
|
end
|
79
79
|
|
80
80
|
# Add to required array if marked as required
|
81
|
-
schema[:required] << param.name if param.
|
81
|
+
schema[:required] << param.name if param.required?
|
82
82
|
end
|
83
83
|
|
84
84
|
# Remove the required key if it's empty (not needed in OpenAPI spec)
|
@@ -107,7 +107,7 @@ module ApiRegulator
|
|
107
107
|
generate_param_schema(p)
|
108
108
|
.merge({
|
109
109
|
name: p.name,
|
110
|
-
required: p.location == :path || p.
|
110
|
+
required: p.location == :path || p.required? || false
|
111
111
|
})
|
112
112
|
end
|
113
113
|
end
|
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
|
3
|
+
attr_reader :name, :type, :options, :item_type, :desc, :location, :children, :api
|
4
4
|
|
5
|
-
def initialize(name, type = nil, item_type: nil, desc: "", location: :body, **options, &block)
|
5
|
+
def initialize(name, type = nil, item_type: nil, desc: "", location: :body, api: nil, **options, &block)
|
6
6
|
@name = name
|
7
7
|
@type = type&.to_sym || (block_given? ? :object : :string)
|
8
8
|
@item_type = item_type
|
@@ -10,12 +10,13 @@ module ApiRegulator
|
|
10
10
|
@desc = desc
|
11
11
|
@options = options
|
12
12
|
@children = []
|
13
|
+
@api = api
|
13
14
|
|
14
15
|
instance_eval(&block) if block_given?
|
15
16
|
end
|
16
17
|
|
17
18
|
def param(name, type = nil, item_type: nil, desc: "", location: :body, **options, &block)
|
18
|
-
child = Param.new(name, type, item_type: item_type, desc: desc, location: location, **options, &block)
|
19
|
+
child = Param.new(name, type, item_type: item_type, desc: desc, location: location, api: api, **options, &block)
|
19
20
|
@children << child
|
20
21
|
end
|
21
22
|
|
@@ -37,8 +38,26 @@ module ApiRegulator
|
|
37
38
|
end
|
38
39
|
end
|
39
40
|
|
40
|
-
def
|
41
|
-
|
41
|
+
def api_action_name
|
42
|
+
api&.action_name
|
43
|
+
end
|
44
|
+
|
45
|
+
def required?(context = api_action_name)
|
46
|
+
return false if @options[:presence].nil?
|
47
|
+
|
48
|
+
if @options[:presence].is_a?(Hash)
|
49
|
+
if context.nil?
|
50
|
+
true # No context provided
|
51
|
+
elsif @options[:presence][:required_on].present?
|
52
|
+
Array(@options[:presence][:required_on]).map(&:to_sym).include?(context.to_sym)
|
53
|
+
elsif @options[:presence][:required_except_on].present?
|
54
|
+
Array(@options[:presence][:required_except_on]).map(&:to_sym).exclude?(context.to_sym)
|
55
|
+
else
|
56
|
+
true # TODO: should we try to handle :if or :unless procs?
|
57
|
+
end
|
58
|
+
else
|
59
|
+
!!@options[:presence]
|
60
|
+
end
|
42
61
|
end
|
43
62
|
|
44
63
|
def body?
|
@@ -10,39 +10,43 @@ module ApiRegulator
|
|
10
10
|
end
|
11
11
|
|
12
12
|
class_methods do
|
13
|
-
def define_attribute_and_validations(param, parent_key
|
13
|
+
def define_attribute_and_validations(param, parent_key: nil, validation_context: nil)
|
14
14
|
# Construct the full key
|
15
15
|
full_key = parent_key ? "#{parent_key}.#{param.name}".to_sym : param.name.to_sym
|
16
16
|
|
17
17
|
case param.type
|
18
18
|
when :array
|
19
|
-
define_array_validations(param, full_key)
|
19
|
+
define_array_validations(param, full_key, validation_context)
|
20
20
|
when :object
|
21
|
-
define_object_validations(param, full_key)
|
21
|
+
define_object_validations(param, full_key, validation_context)
|
22
22
|
else
|
23
|
-
define_scalar_validations(param, full_key)
|
23
|
+
define_scalar_validations(param, full_key, validation_context)
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
|
-
def define_scalar_validations(param, full_key)
|
27
|
+
def define_scalar_validations(param, full_key, validation_context)
|
28
28
|
# Define scalar attributes
|
29
29
|
attribute full_key, param.type if param.type
|
30
30
|
self.defined_attributes += [full_key]
|
31
31
|
|
32
32
|
param.options.each do |option, value|
|
33
|
-
validates full_key, option => value,
|
33
|
+
validates full_key, option => value, if: ->(record) {
|
34
|
+
param.required?(validation_context) || record.raw_attributes.key?(param.name)
|
35
|
+
}
|
34
36
|
end
|
35
37
|
|
36
38
|
# Add type-specific validations
|
37
|
-
|
38
|
-
validate -> {
|
39
|
-
validate -> {
|
39
|
+
case param.type
|
40
|
+
when :boolean then validate -> { validate_boolean(full_key) }
|
41
|
+
when :integer then validate -> { validate_integer(full_key) }
|
42
|
+
when :string then validate -> { validate_string(full_key) }
|
43
|
+
end
|
40
44
|
end
|
41
45
|
|
42
46
|
|
43
|
-
def define_object_validations(param, full_key)
|
47
|
+
def define_object_validations(param, full_key, validation_context)
|
44
48
|
# Build nested validator class
|
45
|
-
nested_validator_class = build_nested_validator_class(param.children, param.name, self)
|
49
|
+
nested_validator_class = build_nested_validator_class(param.children, param.name, self, validation_context)
|
46
50
|
|
47
51
|
# Add a custom validation for the nested object
|
48
52
|
validate -> { validate_nested_object(full_key, nested_validator_class, param) }
|
@@ -51,10 +55,10 @@ module ApiRegulator
|
|
51
55
|
nested_validators[full_key] = nested_validator_class
|
52
56
|
end
|
53
57
|
|
54
|
-
def define_array_validations(param, full_key)
|
58
|
+
def define_array_validations(param, full_key, validation_context)
|
55
59
|
if param.children.any?
|
56
60
|
# Build a nested validator class for array items
|
57
|
-
item_validator_class = build_nested_validator_class(param.children, param.name, self)
|
61
|
+
item_validator_class = build_nested_validator_class(param.children, param.name, self, validation_context)
|
58
62
|
validate -> { validate_array_of_objects(full_key, item_validator_class, param) }
|
59
63
|
|
60
64
|
# Store the nested validator
|
@@ -68,7 +72,7 @@ module ApiRegulator
|
|
68
72
|
end
|
69
73
|
end
|
70
74
|
|
71
|
-
def build_nested_validator_class(children, parent_key, parent_class)
|
75
|
+
def build_nested_validator_class(children, parent_key, parent_class, validation_context)
|
72
76
|
# Create a unique class name based on the parent key
|
73
77
|
class_name = "#{parent_key.to_s.camelize}"
|
74
78
|
|
@@ -83,6 +87,8 @@ module ApiRegulator
|
|
83
87
|
include ActiveModel::Attributes
|
84
88
|
include AttributeDefinitionMixin
|
85
89
|
|
90
|
+
attr_reader :raw_attributes
|
91
|
+
|
86
92
|
def initialize(attributes = {})
|
87
93
|
@raw_attributes = attributes
|
88
94
|
allowed_attributes = attributes.slice(*self.class.defined_attributes.map(&:to_sym))
|
@@ -91,7 +97,7 @@ module ApiRegulator
|
|
91
97
|
|
92
98
|
# Add child attributes and validations
|
93
99
|
children.each do |child|
|
94
|
-
define_attribute_and_validations(child)
|
100
|
+
define_attribute_and_validations(child, validation_context:)
|
95
101
|
end
|
96
102
|
end
|
97
103
|
|
@@ -123,7 +129,7 @@ module ApiRegulator
|
|
123
129
|
end
|
124
130
|
|
125
131
|
validator = item_validator_class.new(value)
|
126
|
-
unless validator.valid?
|
132
|
+
unless validator.valid?(validation_context)
|
127
133
|
validator.errors.each do |error|
|
128
134
|
nested_attr = "#{attribute}[#{index}].#{error.attribute}"
|
129
135
|
errors.add(nested_attr, error.message)
|
@@ -202,7 +208,7 @@ module ApiRegulator
|
|
202
208
|
validator = nested_validator_class.new(raw_value)
|
203
209
|
|
204
210
|
# If validation fails, propagate errors
|
205
|
-
unless validator.valid?
|
211
|
+
unless validator.valid?(validation_context)
|
206
212
|
validator.errors.each do |error|
|
207
213
|
nested_attr = "#{attribute}.#{error.attribute}"
|
208
214
|
errors.add(nested_attr, error.message)
|
@@ -242,7 +248,7 @@ module ApiRegulator
|
|
242
248
|
def self.build_all(api_definitions)
|
243
249
|
api_definitions.each do |api_definition|
|
244
250
|
class_name = "#{api_definition.controller_path}/#{api_definition.action_name}".gsub("/", "_").camelcase
|
245
|
-
validator_class = build_class(api_definition.params)
|
251
|
+
validator_class = build_class(api_definition.params, api_definition.action_name)
|
246
252
|
@validators[[api_definition.controller_path, api_definition.action_name]] = validator_class
|
247
253
|
Validator.const_set(class_name, validator_class)
|
248
254
|
end
|
@@ -253,7 +259,7 @@ module ApiRegulator
|
|
253
259
|
api_definition.responses.each do |code, params|
|
254
260
|
class_name = "#{api_definition.controller_path}/#{api_definition.action_name}/Response#{code}".gsub("/", "_").camelcase
|
255
261
|
|
256
|
-
validator_class = build_class(params.children)
|
262
|
+
validator_class = build_class(params.children, api_definition.action_name)
|
257
263
|
@validators[[api_definition.controller_path, api_definition.action_name, code]] = validator_class
|
258
264
|
Validator.const_set(class_name, validator_class)
|
259
265
|
end
|
@@ -264,21 +270,23 @@ module ApiRegulator
|
|
264
270
|
@validators[[controller.to_s, action.to_s, code].compact]
|
265
271
|
end
|
266
272
|
|
267
|
-
def self.build_class(params)
|
273
|
+
def self.build_class(params, validation_context)
|
268
274
|
Class.new do
|
269
275
|
include ActiveModel::Model
|
270
276
|
include ActiveModel::Attributes
|
271
277
|
include AttributeDefinitionMixin
|
272
278
|
|
279
|
+
attr_reader :raw_attributes
|
280
|
+
|
273
281
|
def initialize(attributes = {})
|
282
|
+
attributes = {} if attributes.blank?
|
274
283
|
@raw_attributes = attributes.deep_symbolize_keys
|
275
|
-
self.class.defined_attributes
|
276
284
|
allowed_attributes = attributes.slice(*self.class.defined_attributes.map(&:to_sym))
|
277
285
|
super(allowed_attributes)
|
278
286
|
end
|
279
287
|
|
280
288
|
params.each do |param|
|
281
|
-
define_attribute_and_validations(param)
|
289
|
+
define_attribute_and_validations(param, validation_context:)
|
282
290
|
end
|
283
291
|
end
|
284
292
|
end
|
@@ -287,11 +295,11 @@ module ApiRegulator
|
|
287
295
|
validator_class = get(controller, action, code)
|
288
296
|
|
289
297
|
unless validator_class
|
290
|
-
raise "No validator found"
|
298
|
+
raise "No validator found for controller: #{controller}, action: #{action}, code: #{code}"
|
291
299
|
end
|
292
300
|
|
293
301
|
validator = validator_class.new(body)
|
294
|
-
unless validator.valid?
|
302
|
+
unless validator.valid?(:response) # using :response for validation context
|
295
303
|
raise ApiRegulator::ValidationError.new(validator.errors)
|
296
304
|
end
|
297
305
|
end
|
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.13
|
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-01-
|
11
|
+
date: 2025-01-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|