jsapi 0.7.0 → 0.7.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba5cf0667b03c271ef93b725cbcdcc25d51b2afaf33efe43372c66880ae46996
4
- data.tar.gz: 1826d5fbc22a972c8f38476a45eff4052930480d7b1a195255422de272d181a4
3
+ metadata.gz: e1726601746ec435ff292f43b0f9e415b9d75298c3ff767d38f32e7a80ec1089
4
+ data.tar.gz: 33d5995c7b8dda3b74c8ab01c4c75eb481666fce3c9a63d22ea662a2b1b3d297
5
5
  SHA512:
6
- metadata.gz: 63ca2ff516d31035884694dfb5a1969b1a697800f9afe694d30e6d250ca181bca0ae71d096d3e566d7de4a50f326233e9e5c9c3f199f38171d65bd27e36e6dec
7
- data.tar.gz: f1b25727248b4bad12b2d57d665f86ee2f75fe826de22cd1d86b4e435152e8c0d2cb07152666b23a61bfb14d3810d516f3eeee53369f6f837559efc29156e996
6
+ metadata.gz: cc78c78dd32ded46d63fb7d320378fa8b613d35a82e095473500e725bfbbe822097b0ee25d4376f9b240477181b4f00f0320ac1256515b8f24bdd025f62c2f91
7
+ data.tar.gz: eef5aac75b9c5094a5a9504eed38e1413fd38fd2becfc65b1830a604afb332609240a11c55db2d029c7ac0867a011a470dc4d46cf7f92e62a50a6b7a365aaf46
@@ -19,7 +19,7 @@ module Jsapi
19
19
  # Includes API definitions from +klasses+.
20
20
  def include(*klasses)
21
21
  klasses.each do |klass|
22
- _meta_model.add_included(klass.api_definitions)
22
+ _meta_model.include(klass.api_definitions)
23
23
  end
24
24
  end
25
25
 
@@ -6,6 +6,7 @@ module Jsapi
6
6
  module Attributes
7
7
  # Defines an attribute.
8
8
  def attribute(name, type = Object,
9
+ add_method: nil,
9
10
  default: nil,
10
11
  default_key: nil,
11
12
  keys: nil,
@@ -26,13 +27,18 @@ module Jsapi
26
27
  when Array
27
28
  unless read_only
28
29
  singular_name = name.to_s.singularize
29
- add_method = "add_#{singular_name}"
30
+ add_method = "add_#{singular_name}" if add_method.nil?
30
31
 
31
32
  type_caster = TypeCaster.new(type.first, values: values, name: singular_name)
32
33
 
33
34
  # Attribute writer
34
35
  define_method("#{name}=") do |argument|
35
- Array.wrap(argument).each { |element| send(add_method, element) }
36
+ if argument.nil?
37
+ instance_variable_set(instance_variable_name, nil)
38
+ else
39
+ instance_variable_set(instance_variable_name, [])
40
+ Array.wrap(argument).each { |element| send(add_method, element) }
41
+ end
36
42
  end
37
43
 
38
44
  # Add method
@@ -43,28 +49,35 @@ module Jsapi
43
49
  else
44
50
  instance_variable_set(instance_variable_name, [])
45
51
  end << casted_argument
52
+ attribute_changed(name)
46
53
  end
47
54
  end
48
55
  end
49
56
  when Hash
50
57
  singular_name = name.to_s.singularize
51
- add_method = "add_#{singular_name}"
52
58
 
53
59
  key_type, value_type = type.first
54
60
  key_type_caster = TypeCaster.new(key_type, values: keys, name: 'key')
55
61
 
56
- # Hash value reader
62
+ # Lookup method
57
63
  define_method(singular_name) do |key = nil|
58
64
  key = default_key if key.to_s.empty?
59
65
  send(name)&.[](key_type_caster.cast(key))
60
66
  end
61
67
 
62
68
  unless read_only
69
+ add_method = "add_#{singular_name}" if add_method.nil?
70
+
63
71
  value_type_caster = TypeCaster.new(value_type, values: values)
64
72
 
65
73
  # Attribute writer
66
74
  define_method("#{name}=") do |argument|
67
- argument.each { |key, value| send(add_method, key, value) }
75
+ if argument.nil?
76
+ instance_variable_set(instance_variable_name, nil)
77
+ else
78
+ instance_variable_set(instance_variable_name, {})
79
+ argument.each { |key, value| send(add_method, key, value) }
80
+ end
68
81
  end
69
82
 
70
83
  # Add method
@@ -86,6 +99,9 @@ module Jsapi
86
99
  else
87
100
  instance_variable_set(instance_variable_name, {})
88
101
  end[casted_key] = casted_value
102
+
103
+ attribute_changed(name)
104
+ casted_value
89
105
  end
90
106
  end
91
107
  else
@@ -100,9 +116,10 @@ module Jsapi
100
116
 
101
117
  # Attribute writer
102
118
  define_method("#{name}=") do |argument = nil|
103
- instance_variable_set(
104
- instance_variable_name, type_caster.cast(argument)
105
- )
119
+ type_caster.cast(argument).tap do |casted_value|
120
+ instance_variable_set(instance_variable_name, casted_value)
121
+ attribute_changed(name)
122
+ end
106
123
  end
107
124
  end
108
125
  end
@@ -39,6 +39,11 @@ module Jsapi
39
39
  def resolve(*)
40
40
  self
41
41
  end
42
+
43
+ protected
44
+
45
+ # Invoked whenever an attribute has been changed.
46
+ def attribute_changed(name); end
42
47
  end
43
48
  end
44
49
  end
@@ -24,7 +24,7 @@ module Jsapi
24
24
  #
25
25
  # Raises a ReferenceError if +ref+ could not be resolved.
26
26
  def resolve(definitions)
27
- object = definitions.find_component(self.class.component_type, ref)
27
+ object = definitions.send("find_#{self.class.component_type}", ref)
28
28
  raise ReferenceError, ref if object.nil?
29
29
 
30
30
  object.resolve(definitions)
@@ -3,19 +3,29 @@
3
3
  module Jsapi
4
4
  module Meta
5
5
  class Definitions < Base::Model
6
+ ##
7
+ # :attr_reader: children
8
+ # The +Definitions+ instances that directly inherit from this instance.
9
+ attribute :children, read_only: true, default: []
10
+
6
11
  ##
7
12
  # :attr: defaults
8
13
  # The Defaults.
9
14
  attribute :defaults, { String => Defaults }, keys: Schema::TYPES, default: {}
10
15
 
11
16
  ##
12
- # :attr: included
13
- # The Definitions included.
14
- attribute :included, [Definitions], default: []
17
+ # :attr: dependent_definitions
18
+ # The +Definitions+ instances that directly include this instance.
19
+ attribute :dependent_definitions, read_only: true, default: []
20
+
21
+ ##
22
+ # :attr: included_definitions
23
+ # The +Definitions+ instances included.
24
+ attribute :included_definitions, [Definitions], add_method: :include, default: []
15
25
 
16
26
  ##
17
27
  # :attr: on_rescues
18
- # The methods or procs to be called when rescuing an exception.
28
+ # The methods or procs to be called whenever an exception is rescued.
19
29
  attribute :on_rescues, [], default: []
20
30
 
21
31
  ##
@@ -30,7 +40,7 @@ module Jsapi
30
40
 
31
41
  ##
32
42
  # :attr_reader: owner
33
- # The class to which it is assigned.
43
+ # The class to which this instance is assigned.
34
44
  attribute :owner, read_only: true
35
45
 
36
46
  ##
@@ -40,7 +50,7 @@ module Jsapi
40
50
 
41
51
  ##
42
52
  # :attr_reader: parent
43
- # The Definitions from which it inherits.
53
+ # The +Definitions+ instance from which this instance inherits.
44
54
  attribute :parent, read_only: true
45
55
 
46
56
  ##
@@ -63,24 +73,14 @@ module Jsapi
63
73
  # The reusable Schema objects.
64
74
  attribute :schemas, { String => Schema }, default: {}
65
75
 
66
- undef add_included, add_operation, add_parameter
76
+ undef add_operation, add_parameter, include
67
77
 
68
78
  def initialize(keywords = {})
69
79
  @owner = keywords.delete(:owner)
70
80
  @parent = keywords.delete(:parent)
71
-
72
81
  super(keywords)
73
- end
74
-
75
- def add_included(definitions) # :nodoc:
76
- if circular_dependency?(definitions)
77
- raise ArgumentError, 'detected circular dependency between ' \
78
- "#{owner.inspect} and " \
79
- "#{definitions.owner.inspect}"
80
- end
81
82
 
82
- (@included ||= []) << definitions
83
- self
83
+ @parent&.inherited(self)
84
84
  end
85
85
 
86
86
  def add_operation(name = nil, keywords = {}) # :nodoc:
@@ -94,67 +94,79 @@ module Jsapi
94
94
  (@parameters ||= {})[name] = Parameter.new(name, keywords)
95
95
  end
96
96
 
97
- # Returns an array containing itself and all of the definitions inherited/included.
97
+ # Returns an array containing itself and all of the +Definitions+ instances
98
+ # inherited/included.
98
99
  def ancestors
99
- [self].tap do |ancestors|
100
- (included + Array(parent)).each do |included_or_parent|
101
- included_or_parent.ancestors.each do |definitions|
102
- ancestors << definitions
103
- end
100
+ @ancestors ||= [self].tap do |ancestors|
101
+ (included_definitions + Array(parent)).each do |definitions|
102
+ ancestors.push(*definitions.ancestors)
104
103
  end
105
104
  end.uniq
106
105
  end
107
106
 
108
107
  # Returns the default value for +type+ within +context+.
109
108
  def default_value(type, context: nil)
110
- return unless (type = type.to_s).present?
111
-
112
- ancestors.each do |definitions|
113
- default = definitions.default(type)
114
- return default.value(context: context) if default
115
- end
116
- nil
117
- end
118
-
119
- # Returns the component with the specified type and name.
120
- def find_component(type, name)
121
- return unless (name = name.to_s).present?
122
-
123
- ancestors.each do |definitions|
124
- component = definitions.send(type, name)
125
- return component if component.present?
126
- end
127
- nil
109
+ components.dig(:defaults, type.to_s)&.value(context: context)
128
110
  end
129
111
 
130
112
  # Returns the operation with the specified name.
131
113
  def find_operation(name = nil)
132
- return find_component(:operation, name) if name.present?
114
+ return components.dig(:operations, name.to_s) if name.present?
133
115
 
134
116
  # Return the one and only operation
135
117
  operations.values.first if operations.one?
136
118
  end
137
119
 
120
+ # Returns the reusable parameter with the specified name.
121
+ def find_parameter(name)
122
+ components.dig(:parameters, name&.to_s)
123
+ end
124
+
125
+ # Returns the reusable request body with the specified name.
126
+ def find_request_body(name)
127
+ components.dig(:request_bodies, name&.to_s)
128
+ end
129
+
130
+ # Returns the reusable response with the specified name.
131
+ def find_response(name)
132
+ components.dig(:responses, name&.to_s)
133
+ end
134
+
135
+ # Returns the reusable schema with the specified name.
136
+ def find_schema(name)
137
+ components.dig(:schemas, name&.to_s)
138
+ end
139
+
140
+ # Includes +definitions+.
141
+ def include(definitions)
142
+ if circular_dependency?(definitions)
143
+ raise ArgumentError, 'detected circular dependency between ' \
144
+ "#{owner.inspect} and " \
145
+ "#{definitions.owner.inspect}"
146
+ end
147
+
148
+ (@included_definitions ||= []) << definitions
149
+ definitions.included(self)
150
+ attribute_changed(:included_definitions)
151
+ self
152
+ end
153
+
138
154
  def inspect(*attributes) # :nodoc:
139
- super(*(attributes.presence || %i[owner parent included]))
155
+ super(*(attributes.presence || %i[owner]))
140
156
  end
141
157
 
142
158
  # Returns a hash representing the \JSON \Schema document for +name+.
143
159
  def json_schema_document(name)
144
- find_component(:schema, name)&.to_json_schema&.tap do |hash|
145
- definitions = ancestors
146
- .map(&:schemas)
147
- .reduce(&:merge)
148
- .except(name.to_s)
149
- .transform_values(&:to_json_schema)
150
-
151
- hash[:definitions] = definitions if definitions.any?
160
+ find_schema(name)&.to_json_schema&.tap do |json_schema_document|
161
+ if (schemas = components[:schemas].except(name.to_s)).any?
162
+ json_schema_document[:definitions] = schemas.transform_values(&:to_json_schema)
163
+ end
152
164
  end
153
165
  end
154
166
 
155
167
  # Returns the methods or procs to be called when rescuing an exception.
156
168
  def on_rescue_callbacks
157
- ancestors.flat_map(&:on_rescues)
169
+ components[:on_rescues]
158
170
  end
159
171
 
160
172
  # Returns a hash representing the \OpenAPI document for +version+.
@@ -162,79 +174,119 @@ module Jsapi
162
174
  # Raises an +ArgumentError+ if +version+ is not supported.
163
175
  def openapi_document(version = nil)
164
176
  version = OpenAPI::Version.from(version)
165
-
166
- operations = ancestors.map(&:operations).reduce(&:reverse_merge).values
167
-
168
- components = if version.major == 2
169
- {
170
- definitions: :schemas,
171
- parameters: :parameters,
172
- responses: :responses
173
- }
174
- else
175
- {
176
- schemas: :schemas,
177
- parameters: :parameters,
178
- requestBodies: :request_bodies,
179
- responses: :responses
180
- }
181
- end
182
-
183
- openapi_components = ancestors.map do |definitions|
184
- components.transform_values do |method|
185
- definitions.send(method).transform_values do |component|
186
- case method
187
- when :parameters
188
- component.to_openapi(version, self).first
189
- when :responses
190
- component.to_openapi(version, self)
191
- else
192
- component.to_openapi(version)
177
+ operations = components[:operations].values
178
+
179
+ component_keys = %i[parameters responses parameters schemas]
180
+ component_keys.insert(3, :request_bodies) if version.major > 2
181
+
182
+ openapi_components = component_keys.filter_map do |key|
183
+ value = components[key].transform_values do |component|
184
+ case key
185
+ when :parameters
186
+ component.to_openapi(version, self).first
187
+ when :responses
188
+ component.to_openapi(version, self)
189
+ else
190
+ component.to_openapi(version)
191
+ end
192
+ end.presence
193
+
194
+ [key, value] if value.present?
195
+ end.to_h
196
+
197
+ (openapi&.to_openapi(version, self) || {}).tap do |openapi_document|
198
+ openapi_document[:paths] =
199
+ operations
200
+ .group_by { |operation| operation.path || default_path }
201
+ .transform_values do |operations_by_path|
202
+ operations_by_path.index_by(&:method).transform_values do |operation|
203
+ operation.to_openapi(version, self)
193
204
  end
194
205
  end.presence
195
- end.compact
196
- end.reduce(&:reverse_merge)
197
-
198
- (openapi&.to_openapi(version, self) || {}).tap do |h|
199
- h[:paths] = operations
200
- .group_by { |operation| operation.path || default_path }
201
- .transform_values do |op|
202
- op.index_by(&:method).transform_values do |operation|
203
- operation.to_openapi(version, self)
204
- end
205
- end.presence
206
206
 
207
207
  if version.major == 2
208
208
  consumes = operations.filter_map { |operation| operation.consumes(self) }
209
- h[:consumes] = consumes.uniq.sort if consumes.present?
210
-
211
209
  produces = operations.flat_map { |operation| operation.produces(self) }
212
- h[:produces] = produces.uniq.sort if produces.present?
213
210
 
214
- h.merge!(openapi_components)
211
+ openapi_document.merge!(
212
+ consumes: (consumes.uniq.sort if consumes.present?),
213
+ produces: (produces.uniq.sort if produces.present?),
214
+ definitions: openapi_components[:schemas],
215
+ parameters: openapi_components[:parameters],
216
+ responses: openapi_components[:responses]
217
+ )
215
218
  elsif openapi_components.any?
216
- (h[:components] ||= {}).merge!(openapi_components)
219
+ (openapi_document[:components] ||= {}).reverse_merge!(
220
+ openapi_components.transform_keys do |key|
221
+ key.to_s.camelize(:lower).to_sym
222
+ end
223
+ )
217
224
  end
218
225
  end.compact
219
226
  end
220
227
 
221
228
  # Returns the first RescueHandler to handle +exception+, or nil if no one could be found.
222
229
  def rescue_handler_for(exception)
223
- ancestors.each do |definitions|
224
- definitions.rescue_handlers.each do |rescue_handler|
225
- return rescue_handler if rescue_handler.match?(exception)
226
- end
230
+ components[:rescue_handlers].find { |r| r.match?(exception) }
231
+ end
232
+
233
+ protected
234
+
235
+ def attribute_changed(name) # :nodoc:
236
+ return if name == :openapi
237
+
238
+ if name == :included_definitions
239
+ invalidate_ancestors
240
+ else
241
+ invalidate_components
227
242
  end
228
- nil
243
+ end
244
+
245
+ # Invoked whenever it is included in another +Definitions+ instance.
246
+ def included(definitions)
247
+ (@dependent_definitions ||= []) << definitions
248
+ end
249
+
250
+ # rubocop:disable Lint/MissingSuper
251
+
252
+ # Invoked whenever it is inherited by another +Definitions+ instance.
253
+ def inherited(definitions)
254
+ (@children ||= []) << definitions
255
+ end
256
+
257
+ # rubocop:enable Lint/MissingSuper
258
+
259
+ # Invalidates cached ancestors.
260
+ def invalidate_ancestors
261
+ @ancestors = nil
262
+ @components = nil
263
+ (children + dependent_definitions).each(&:invalidate_ancestors)
264
+ end
265
+
266
+ # Invalidates cached components.
267
+ def invalidate_components
268
+ @components = nil
269
+ (children + dependent_definitions).each(&:invalidate_components)
229
270
  end
230
271
 
231
272
  private
232
273
 
233
274
  def circular_dependency?(other)
234
275
  return true if other == self
235
- return false if other.included.none?
276
+ return false if other.included_definitions.none?
277
+
278
+ other.included_definitions.any? { |included| circular_dependency?(included) }
279
+ end
236
280
 
237
- other.included.any? { |included| circular_dependency?(included) }
281
+ def components
282
+ @components ||= ancestors.each_with_object({}) do |ancestor, components|
283
+ %i[defaults operations parameters request_bodies responses schemas].each do |type|
284
+ (components[type] ||= {}).reverse_merge!(ancestor.send(type))
285
+ end
286
+ %i[on_rescues rescue_handlers].each do |type|
287
+ (components[type] ||= []).push(*ancestor.send(type))
288
+ end
289
+ end
238
290
  end
239
291
 
240
292
  def default_operation_name
@@ -6,11 +6,8 @@ module Jsapi
6
6
  module Meta
7
7
  module Schema
8
8
  module Conversion
9
- extend ActiveSupport::Concern
10
-
11
- included do
12
- # The method or +Proc+ to convert objects by.
13
- attr_accessor :conversion
9
+ def self.included(mod)
10
+ mod.attr_accessor :conversion
14
11
  end
15
12
 
16
13
  def convert(object)
@@ -68,7 +68,7 @@ module Jsapi
68
68
  value = property.default_value(definitions, context: context) if value.nil?
69
69
  raise "#{discriminator.property_name} can't be nil" if value.nil?
70
70
 
71
- schema = definitions.find_component(:schema, discriminator.mapping(value) || value)
71
+ schema = definitions.find_schema(discriminator.mapping(value) || value)
72
72
  raise "inheriting schema couldn't be found: #{value.inspect}" if schema.nil?
73
73
 
74
74
  schema.resolve(definitions).resolve_schema(object, definitions, context: context)
@@ -3,10 +3,8 @@
3
3
  module Jsapi
4
4
  module Model
5
5
  module Attributes
6
- extend ActiveSupport::Concern
7
-
8
- included do
9
- delegate :[], :additional_attributes, :attribute?, :attributes, to: :nested
6
+ def self.included(mod)
7
+ mod.delegate :[], :additional_attributes, :attribute?, :attributes, to: :nested
10
8
  end
11
9
 
12
10
  def method_missing(*args) # :nodoc:
@@ -6,9 +6,10 @@ module Jsapi
6
6
  # parameters by default.
7
7
  class Base
8
8
  extend Naming
9
-
10
9
  include Attributes
11
- include Validations
10
+ include ActiveModel::Validations
11
+
12
+ validate :nested_validity
12
13
 
13
14
  def initialize(nested)
14
15
  @nested = nested
@@ -21,6 +22,12 @@ module Jsapi
21
22
  )
22
23
  end
23
24
 
25
+ # Overrides <code>ActiveModel::Validations#errors</code>
26
+ # to use Errors as error store.
27
+ def errors
28
+ @errors ||= Errors.new(self)
29
+ end
30
+
24
31
  def inspect # :nodoc:
25
32
  "#<#{self.class.name}#{' ' if attributes.any?}" \
26
33
  "#{attributes.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')}>"
@@ -29,6 +36,10 @@ module Jsapi
29
36
  private
30
37
 
31
38
  attr_reader :nested
39
+
40
+ def nested_validity
41
+ @nested.validate(errors)
42
+ end
32
43
  end
33
44
  end
34
45
  end
data/lib/jsapi/model.rb CHANGED
@@ -5,11 +5,10 @@ require_relative 'model/nested_error'
5
5
  require_relative 'model/errors'
6
6
  require_relative 'model/attributes'
7
7
  require_relative 'model/naming'
8
- require_relative 'model/validations'
9
8
  require_relative 'model/base'
10
9
  require_relative 'model/nestable'
11
10
 
12
11
  module Jsapi
13
- # Provides classes and modules to deal with API models.
12
+ # Provides classes to deal with API models.
14
13
  module Model end
15
14
  end
data/lib/jsapi/version.rb CHANGED
@@ -5,6 +5,6 @@ module Jsapi
5
5
  # NOTE: See https://bundler.io/guides/creating_gem.html
6
6
 
7
7
  # The current GEM version.
8
- VERSION = '0.7.0'
8
+ VERSION = '0.7.2'
9
9
  end
10
10
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jsapi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.7.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Denis Göller
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-09-21 00:00:00.000000000 Z
11
+ date: 2024-09-24 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Jsapi can be used to read requests, produce responses and create OpenAPI
14
14
  documents
@@ -145,7 +145,6 @@ files:
145
145
  - lib/jsapi/model/naming.rb
146
146
  - lib/jsapi/model/nestable.rb
147
147
  - lib/jsapi/model/nested_error.rb
148
- - lib/jsapi/model/validations.rb
149
148
  - lib/jsapi/version.rb
150
149
  homepage: https://github.com/dmgoeller/jsapi
151
150
  licenses:
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Jsapi
4
- module Model
5
- module Validations
6
- extend ActiveSupport::Concern
7
-
8
- include ActiveModel::Validations
9
-
10
- included do
11
- validate :nested_validity
12
- end
13
-
14
- # Overrides <code>ActiveModel::Validations#errors</code>
15
- # to use Errors as error store.
16
- def errors
17
- @errors ||= Errors.new(self)
18
- end
19
-
20
- private
21
-
22
- def nested_validity
23
- nested.validate(errors)
24
- end
25
- end
26
- end
27
- end