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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: beb1b2f02d84500f7a1075c188c72f140ae936ff5c8a0d8e2cf74564a33215b6
4
- data.tar.gz: b2c01f8fd7ae1c0dabe91777d408a0ea8680db49251c9c97471ddffa5c97e8de
3
+ metadata.gz: dd01d986f35199767ed09aeca6b5a5b0b19441113fa10b2fa5b34a511a4773ab
4
+ data.tar.gz: 02e722bd4b5df9a728a9ff1a339922cd891e9ae0eca9bf516b653bab3132c379
5
5
  SHA512:
6
- metadata.gz: 572709a0fe03ddef58b7e843143ba07e97cac8bb28019ea5c0ac56cdef3292868e7f25846408e019327be457a72d63e90f9302a41f37cb933b694f5030d06b27
7
- data.tar.gz: 201970e5d1eb3edfc12ef12f2c18ff8e45dd7a401cc4d7c8f59a47dd0eceeea80aabf268cf48fccd85fceb616645151bf57b1b59f2eecdb575ec6270f5a861ca
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.11)
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.8)
84
+ bigdecimal (3.1.9)
85
85
  builder (3.3.0)
86
86
  byebug (11.1.3)
87
- concurrent-ruby (1.3.4)
88
- connection_pool (2.4.1)
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.0)
93
+ erubi (1.13.1)
94
94
  globalid (1.2.1)
95
95
  activesupport (>= 6.1)
96
- i18n (1.14.6)
96
+ i18n (1.14.7)
97
97
  concurrent-ruby (~> 1.0)
98
- io-console (0.7.2)
99
- irb (1.14.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.3)
103
- loofah (2.23.1)
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.2)
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.17.2)
126
+ nokogiri (1.18.2)
126
127
  mini_portile2 (~> 2.8.2)
127
128
  racc (~> 1.4)
128
- nokogiri (1.17.2-arm64-darwin)
129
+ nokogiri (1.18.2-arm64-darwin)
129
130
  racc (~> 1.4)
130
- psych (5.2.0)
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.0.0)
139
+ rack-session (2.1.0)
140
+ base64 (>= 0.1.0)
135
141
  rack (>= 3.0.0)
136
- rack-test (2.1.0)
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.8.1)
176
+ rdoc (6.11.0)
171
177
  psych (>= 4.0.0)
172
- reline (0.5.11)
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.1)
195
- securerandom (0.4.0)
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.6)
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/).
@@ -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
 
@@ -8,7 +8,7 @@ module ApiRegulator
8
8
  end
9
9
 
10
10
  validator = validator_class.new(api_params)
11
- unless validator.valid?
11
+ unless validator.valid?(params[:action].to_sym)
12
12
  raise ApiRegulator::ValidationError.new(validator.errors)
13
13
  end
14
14
  end
@@ -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.options[:presence]
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.options[:presence] || false
110
+ required: p.location == :path || p.required? || false
111
111
  })
112
112
  end
113
113
  end
@@ -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 required?
41
- !!@options[:presence]
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 = nil)
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, allow_blank: !param.required?
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
- validate -> { validate_boolean(full_key) } if param.type == :boolean
38
- validate -> { validate_integer(full_key) } if param.type == :integer
39
- validate -> { validate_string(full_key) } if param.type == :string
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
@@ -1,3 +1,3 @@
1
1
  module ApiRegulator
2
- VERSION = "0.1.11"
2
+ VERSION = "0.1.13"
3
3
  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.11
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-15 00:00:00.000000000 Z
11
+ date: 2025-01-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport