scorpio 0.6.4 → 0.7.0

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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +6 -1
  3. data/CHANGELOG.md +10 -2
  4. data/LICENSE.md +2 -4
  5. data/README.md +81 -67
  6. data/documents/{github.com/OAI/OpenAPI-Specification/blob/oas3-schema/schemas/v3.0 → spec.openapis.org/oas/3.0}/schema.yaml +164 -628
  7. data/documents/spec.openapis.org/oas/3.1/dialect/base.schema.yaml +22 -0
  8. data/documents/spec.openapis.org/oas/3.1/meta/base.schema.yaml +71 -0
  9. data/documents/spec.openapis.org/oas/3.1/schema-base.yaml +21 -0
  10. data/documents/spec.openapis.org/oas/3.1/schema.yaml +980 -0
  11. data/documents/swagger.io/v2/schema.json +1 -1
  12. data/documents/www.googleapis.com/discovery/v1/apis/discovery/v1/rest.yml +44 -4
  13. data/lib/scorpio/google_api_document.rb +121 -193
  14. data/lib/scorpio/openapi/document.rb +63 -31
  15. data/lib/scorpio/openapi/operation.rb +114 -96
  16. data/lib/scorpio/openapi/operations_scope.rb +35 -19
  17. data/lib/scorpio/openapi/reference.rb +88 -23
  18. data/lib/scorpio/openapi/schema_elements/type_nullable.rb +38 -0
  19. data/lib/scorpio/openapi/schema_elements.rb +7 -0
  20. data/lib/scorpio/openapi/server.rb +34 -0
  21. data/lib/scorpio/openapi/tag.rb +19 -3
  22. data/lib/scorpio/openapi/v2/dialect.rb +66 -0
  23. data/lib/scorpio/openapi/v2.rb +124 -0
  24. data/lib/scorpio/openapi/v3_0/dialect.rb +76 -0
  25. data/lib/scorpio/openapi/v3_0.rb +130 -0
  26. data/lib/scorpio/openapi/v3_1.rb +243 -0
  27. data/lib/scorpio/openapi.rb +23 -203
  28. data/lib/scorpio/request.rb +67 -61
  29. data/lib/scorpio/resource_base.rb +57 -49
  30. data/lib/scorpio/response.rb +28 -10
  31. data/lib/scorpio/ur.rb +7 -3
  32. data/lib/scorpio/version.rb +1 -1
  33. data/lib/scorpio.rb +12 -6
  34. data/pages/Request_Configuration.md +69 -0
  35. data/pages/Security.md +50 -0
  36. data/scorpio.gemspec +6 -5
  37. metadata +28 -15
  38. data/documents/www.googleapis.com/discovery/v1/apis/discovery/v1/rest +0 -684
  39. data/lib/scorpio/openapi/v3/server.rb +0 -44
@@ -25,6 +25,18 @@ module Scorpio
25
25
  openapi_document.user_agent
26
26
  end
27
27
 
28
+ attr_writer(:accept)
29
+ def accept
30
+ return @accept if instance_variable_defined?(:@accept)
31
+ openapi_document.accept
32
+ end
33
+
34
+ attr_writer(:authorization)
35
+ def authorization
36
+ return @authorization if instance_variable_defined?(:@authorization)
37
+ openapi_document.authorization
38
+ end
39
+
28
40
  attr_writer :faraday_builder
29
41
  def faraday_builder
30
42
  return @faraday_builder if instance_variable_defined?(:@faraday_builder)
@@ -44,37 +56,32 @@ module Scorpio
44
56
  end
45
57
  end
46
58
  include Configurables
59
+ include(Document::Descendent)
47
60
 
48
61
  # openapi v3?
49
62
  # @return [Boolean]
50
63
  def v3?
51
- is_a?(V3::Operation)
64
+ is_a?(OpenAPI::Operation::V3Methods)
52
65
  end
53
66
 
54
67
  # openapi v2?
55
68
  # @return [Boolean]
56
69
  def v2?
57
- is_a?(V2::Operation)
58
- end
59
-
60
- # the document whence this operation came
61
- # @return [Scorpio::OpenAPI::Document]
62
- def openapi_document
63
- jsi_parent_nodes.detect { |p| p.is_a?(Scorpio::OpenAPI::Document) }
70
+ is_a?(OpenAPI::V2::Operation)
64
71
  end
65
72
 
66
73
  # @return [String]
67
74
  def path_template_str
68
75
  return @path_template_str if instance_variable_defined?(:@path_template_str)
69
- raise(Bug) unless jsi_parent_node.is_a?(Scorpio::OpenAPI::V2::PathItem) || jsi_parent_node.is_a?(Scorpio::OpenAPI::V3::PathItem)
70
- raise(Bug) unless jsi_parent_node.jsi_parent_node.is_a?(Scorpio::OpenAPI::V2::Paths) || jsi_parent_node.jsi_parent_node.is_a?(Scorpio::OpenAPI::V3::Paths)
71
- @path_template_str = jsi_parent_node.jsi_ptr.tokens.last
76
+ path_item = jsi_ancestor_nodes.detect { |n| n.is_a?(Scorpio::OpenAPI::PathItem) }
77
+ @path_template_str = path_item && path_item.jsi_ptr.tokens.last
72
78
  end
73
79
 
74
80
  # the path as an Addressable::Template
75
81
  # @return [Addressable::Template]
76
82
  def path_template
77
83
  return @path_template if instance_variable_defined?(:@path_template)
84
+ return(@path_template = nil) if !path_template_str
78
85
  @path_template = Addressable::Template.new(path_template_str)
79
86
  end
80
87
 
@@ -83,7 +90,7 @@ module Scorpio
83
90
  # @return [Addressable::Template]
84
91
  def uri_template(base_url: self.base_url)
85
92
  unless base_url
86
- raise(ArgumentError, "no base_url has been specified for operation #{self}")
93
+ raise(ArgumentError, -"no base_url has been specified for operation #{self}")
87
94
  end
88
95
  # we do not use Addressable::URI#join as the paths should just be concatenated, not resolved.
89
96
  # we use File.join just to deal with consecutive slashes.
@@ -95,38 +102,88 @@ module Scorpio
95
102
  # @return [String]
96
103
  def http_method
97
104
  return @http_method if instance_variable_defined?(:@http_method)
98
- raise(Bug) unless jsi_parent_node.is_a?(Scorpio::OpenAPI::V2::PathItem) || jsi_parent_node.is_a?(Scorpio::OpenAPI::V3::PathItem)
105
+ return(@http_method = nil) unless jsi_parent_node.is_a?(Scorpio::OpenAPI::PathItem)
99
106
  @http_method = jsi_ptr.tokens.last
100
107
  end
101
108
 
109
+ def get?
110
+ 'get'.casecmp?(http_method)
111
+ end
112
+
113
+ def put?
114
+ 'put'.casecmp?(http_method)
115
+ end
116
+
117
+ def post?
118
+ 'post'.casecmp?(http_method)
119
+ end
120
+
121
+ def delete?
122
+ 'delete'.casecmp?(http_method)
123
+ end
124
+
125
+ def options?
126
+ 'options'.casecmp?(http_method)
127
+ end
128
+
129
+ def head?
130
+ 'head'.casecmp?(http_method)
131
+ end
132
+
133
+ def patch?
134
+ 'patch'.casecmp?(http_method)
135
+ end
136
+
137
+ def trace?
138
+ 'trace'.casecmp?(http_method)
139
+ end
140
+
141
+ # @param tag_name [String]
142
+ # @return [Boolean]
143
+ def tagged?(tag_name)
144
+ tags.respond_to?(:to_ary) && tags.include?(tag_name)
145
+ end
146
+
102
147
  # a short identifier for this operation appropriate for an error message
103
148
  # @return [String]
104
149
  def human_id
105
- operationId || "path: #{path_template_str}, method: #{http_method}"
150
+ operationId || -"path: #{path_template_str}, method: #{http_method}"
106
151
  end
107
152
 
108
153
  # @param status [String, Integer]
109
- # @return [Scorpio::OpenAPI::V3::Response, Scorpio::OpenAPI::V2::Response]
154
+ # @return [OpenAPI::Response, nil]
110
155
  def oa_response(status: )
156
+ return nil if !responses
111
157
  status = status.to_s if status.is_a?(Numeric)
112
- if responses
113
- _, oa_response = responses.detect { |k, v| k.to_s == status }
114
- oa_response ||= responses['default']
158
+ responses.each do |k, v|
159
+ return v if k.to_s == status
115
160
  end
116
- oa_response
161
+ responses[-"#{status[0]}XX"] || responses['default']
117
162
  end
118
163
 
119
- # the parameters specified for this operation, plus any others scorpio considers to be parameters.
164
+ # operation parameters + path item parameters
120
165
  #
121
- # this method is not intended to be API-stable at the moment.
166
+ # @api private
167
+ # @return [#to_ary<#to_hash>]
168
+ def inherited_parameters
169
+ parameters = []
170
+ parameters.concat(self.parameters.to_ary) if self.parameters
171
+ path_item = jsi_ancestor_nodes.detect { |n| n.is_a?(OpenAPI::PathItem) }
172
+ parameters.concat((path_item && path_item.parameters || []).select do |pip|
173
+ parameters.none? { |p| p['in'] == pip['in'] && p['name'] == pip['name'] }
174
+ end)
175
+ parameters.freeze
176
+ end
177
+
178
+ # the parameters specified for this operation, plus any others scorpio considers to be parameters.
122
179
  #
123
180
  # @api private
124
- # @return [#to_ary<#to_h>]
181
+ # @return [#to_ary<#to_hash>]
125
182
  def inferred_parameters
126
- parameters = self.parameters ? self.parameters.to_a.dup : []
183
+ parameters = inherited_parameters.dup
127
184
  path_template.variables.each do |var|
128
185
  unless parameters.any? { |p| p['in'] == 'path' && p['name'] == var }
129
- # we could instantiate this as a V2::Parameter or a V3::Parameter
186
+ # we could instantiate this as a V2::Parameter or a V3_0::Parameter
130
187
  # or a ParameterWithContentInPath or whatever. but I can't be bothered.
131
188
  parameters << {
132
189
  'name' => var,
@@ -136,85 +193,47 @@ module Scorpio
136
193
  }
137
194
  end
138
195
  end
139
- parameters
140
- end
141
-
142
- # a module with accessor methods for unambiguously named parameters of this operation.
143
- # @return [Module]
144
- def request_accessor_module
145
- return @request_accessor_module if instance_variable_defined?(:@request_accessor_module)
146
- @request_accessor_module = begin
147
- params_by_name = inferred_parameters.group_by { |p| p['name'] }
148
- Module.new do
149
- instance_method_modules = [Request, Request::Configurables]
150
- instance_method_names = instance_method_modules.map do |mod|
151
- (mod.instance_methods + mod.private_instance_methods).map(&:to_s)
152
- end.inject(Set.new, &:merge)
153
- params_by_name.each do |name, params|
154
- next if instance_method_names.include?(name)
155
- if params.size == 1
156
- param = params.first
157
- define_method("#{name}=") { |value| set_param_from(param['in'], param['name'], value) }
158
- define_method(name) { get_param_from(param['in'], param['name']) }
159
- end
160
- end
161
- end
162
- end
196
+ parameters.freeze
163
197
  end
164
198
 
165
199
  # instantiates a {Scorpio::Request} for this operation.
166
- # parameters are all passed to {Scorpio::Request#initialize}.
200
+ # configuration is passed to {Scorpio::Request#initialize}.
167
201
  # @return [Scorpio::Request]
168
- def build_request(configuration = {}, &b)
169
- @request_class ||= Scorpio::Request.request_class_by_operation(self)
170
- @request_class.new(configuration, &b)
202
+ def build_request(**configuration, &b)
203
+ Scorpio::Request.new(self, **configuration, &b)
171
204
  end
172
205
 
173
206
  # runs a {Scorpio::Request} for this operation, returning a {Scorpio::Ur}.
174
- # parameters are all passed to {Scorpio::Request#initialize}.
207
+ # configuration is passed to {Scorpio::Request#initialize}.
175
208
  # @return [Scorpio::Ur] response ur
176
- def run_ur(configuration = {}, &b)
177
- build_request(configuration, &b).run_ur
209
+ def run_ur(**configuration, &b)
210
+ build_request(**configuration, &b).run_ur
178
211
  end
179
212
 
180
213
  # runs a {Scorpio::Request} for this operation - see {Scorpio::Request#run}.
181
- # parameters are all passed to {Scorpio::Request#initialize}.
214
+ # configuration is passed to {Scorpio::Request#initialize}.
182
215
  # @return response body object
183
- def run(configuration = {}, &b)
184
- build_request(configuration, &b).run
216
+ def run(mutable: false, **configuration, &b)
217
+ build_request(**configuration, &b).run(mutable: mutable)
185
218
  end
186
219
 
187
220
  # Runs this operation with the given request config, and yields the resulting {Scorpio::Ur}.
188
- # If the response contains a `Link` header with a `next` link (and that link's URL
189
- # corresponds to this operation), this operation is run again to that link's URL, that
190
- # request's Ur yielded, and a `next` link in that response is followed.
221
+ # If the response contains a `Link` header with a `next` link, this operation is run again to
222
+ # that link's URL, that request's Ur yielded, and a `next` link in that response is followed.
191
223
  # This repeats until a response does not contain a `Link` header with a `next` link.
192
224
  #
193
225
  # @param configuration (see Scorpio::Request#initialize)
194
226
  # @yield [Scorpio::Ur]
195
227
  # @return [Enumerator, nil]
196
- def each_link_page(configuration = {}, &block)
197
- init_request = build_request(configuration)
228
+ def each_link_page(**configuration, &block)
229
+ init_request = build_request(**configuration)
198
230
  next_page = proc do |last_page_ur|
199
231
  nextlinks = last_page_ur.response.links.select { |link| link.rel?('next') }
200
232
  if nextlinks.size == 0
201
233
  # no next link; we are at the end
202
234
  nil
203
235
  elsif nextlinks.size == 1
204
- nextlink = nextlinks.first
205
- # we do not use Addressable::URI#join as the paths should just be concatenated, not resolved.
206
- # we use File.join just to deal with consecutive slashes.
207
- template = Addressable::Template.new(File.join(init_request.base_url, path_template_str))
208
- target_uri = nextlink.absolute_target_uri
209
- path_params = template.extract(target_uri.merge(query: nil))
210
- unless path_params
211
- raise("the URI of the link to the next page did not match the URI of this operation")
212
- end
213
- query_params = target_uri.query_values
214
- run_ur(
215
- path_params: path_params,
216
- query_params: query_params,
217
- )
236
+ run_ur(**configuration, url: nextlinks.first.absolute_target_uri)
218
237
  else
219
238
  # TODO better error class / context / message
220
239
  raise("response included multiple links with rel=next")
@@ -226,17 +245,12 @@ module Scorpio
226
245
  private
227
246
 
228
247
  def jsi_object_group_text
229
- [*super, http_method, path_template_str].freeze
248
+ [*super, http_method, path_template_str].compact.freeze
230
249
  end
231
250
  end
232
251
 
233
- module V3
234
- raise(Bug, 'const_defined? Scorpio::OpenAPI::V3::Operation') unless const_defined?(:Operation)
235
-
236
- # Describes a single API operation on a path.
237
- #
238
- # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operationObject
239
- module Operation
252
+ module Operation
253
+ module V3Methods
240
254
  module Configurables
241
255
  def scheme
242
256
  # not applicable; for OpenAPI v3, scheme is specified by servers.
@@ -266,6 +280,7 @@ module Scorpio
266
280
  end
267
281
  end
268
282
  include Configurables
283
+ include(OpenAPI::Operation)
269
284
 
270
285
  # @return [JSI::Schema]
271
286
  def request_schema(media_type: self.request_media_type)
@@ -291,13 +306,15 @@ module Scorpio
291
306
  end
292
307
  end
293
308
 
294
- # @return [JSI::Schema]
309
+ # @return [JSI::Schema, nil]
295
310
  def response_schema(status: , media_type: )
296
- oa_response = self.oa_response(status: status)
297
- oa_media_types = oa_response ? oa_response['content'] : nil # Scorpio::OpenAPI::V3::MediaTypes
298
- oa_media_type = oa_media_types ? oa_media_types[media_type] : nil # Scorpio::OpenAPI::V3::MediaType
299
- oa_schema = oa_media_type ? oa_media_type['schema'] : nil # Scorpio::OpenAPI::V3::Schema
300
- oa_schema ? JSI::Schema.ensure_schema(oa_schema) : nil
311
+ oa_response = self.oa_response(status: status) || return
312
+ oa_media_types = oa_response['content'] || return # Scorpio::OpenAPI::V3_*::MediaTypes
313
+ oa_media_type = oa_media_types[media_type] # Scorpio::OpenAPI::V3_*::MediaType
314
+ oa_media_type ||= oa_media_types[-"#{::Ur::ContentType.new(media_type).type}/*"]
315
+ oa_media_type ||= oa_media_types['*/*'] || return
316
+ oa_schema = oa_media_type['schema'] || return # JSI::Schema, Scorpio::OpenAPI::V3_*::Schema
317
+ JSI::Schema.ensure_schema(oa_schema)
301
318
  end
302
319
 
303
320
  # @return [JSI::SchemaSet]
@@ -318,9 +335,9 @@ module Scorpio
318
335
  end
319
336
  end
320
337
  end
321
- module V2
322
- raise(Bug, 'const_defined? Scorpio::OpenAPI::V2::Operation') unless const_defined?(:Operation)
323
- module Operation
338
+
339
+ module Operation
340
+ module V2Methods
324
341
  module Configurables
325
342
  attr_writer :scheme
326
343
  def scheme
@@ -345,19 +362,20 @@ module Scorpio
345
362
  end
346
363
  end
347
364
  include Configurables
365
+ include(OpenAPI::Operation)
348
366
 
349
367
  # the body parameter
350
368
  # @return [#to_hash]
351
369
  # @raise [Scorpio::OpenAPI::SemanticError] if there's more than one body param
352
370
  def body_parameter
353
- body_parameters = (parameters || []).select { |parameter| parameter['in'] == 'body' }
371
+ body_parameters = inherited_parameters.select { |parameter| parameter['in'] == 'body' }
354
372
  if body_parameters.size == 0
355
373
  nil
356
374
  elsif body_parameters.size == 1
357
375
  body_parameters.first
358
376
  else
359
377
  # TODO blame
360
- raise(OpenAPI::SemanticError, "multiple body parameters on operation #{operation.pretty_inspect.chomp}")
378
+ raise(OpenAPI::SemanticError, -"multiple body parameters on operation #{operation.pretty_inspect.chomp}")
361
379
  end
362
380
  end
363
381
 
@@ -2,40 +2,56 @@
2
2
 
3
3
  module Scorpio
4
4
  module OpenAPI
5
- # OperationsScope acts as an Enumerable of the Operations for an openapi_document,
5
+ # OperationsScope is an Enumerable for a collection of Operations,
6
6
  # and offers subscripting by operationId.
7
7
  class OperationsScope
8
- # @param openapi_document [Scorpio::OpenAPI::Document]
9
- def initialize(openapi_document)
10
- @openapi_document = openapi_document
8
+ # @param enum [Enumerable]
9
+ def initialize(enum)
10
+ @enum = enum
11
11
  @operations_by_id = Hash.new do |h, operationId|
12
- op = detect { |operation| operation.operationId == operationId }
13
- unless op
14
- raise(::KeyError, "operationId not found: #{operationId.inspect}")
15
- end
16
- h[operationId] = op
12
+ h[operationId] = enum.detect { |operation| operation.operationId == operationId }
17
13
  end
18
14
  end
19
15
  attr_reader :openapi_document
20
16
 
21
17
  # @yield [Scorpio::OpenAPI::Operation]
22
- def each
23
- openapi_document.paths.each do |path, path_item|
24
- path_item.each do |http_method, operation|
25
- if operation.is_a?(Scorpio::OpenAPI::Operation)
26
- yield operation
27
- end
28
- end
29
- end
18
+ def each(&block)
19
+ @enum.each(&block)
30
20
  end
21
+
31
22
  include Enumerable
32
23
 
24
+ # @return [Scorpio::OpenAPI::Operation, nil]
25
+ def by_id(operationId)
26
+ @operations_by_id[operationId]
27
+ end
28
+
33
29
  # finds an operation with the given `operationId`
34
30
  # @param operationId [String] the operationId of the operation to find
35
31
  # @return [Scorpio::OpenAPI::Operation]
36
32
  # @raise [::KeyError] if the given operationId does not exist
37
- def [](operationId)
38
- @operations_by_id[operationId]
33
+ def by_id!(operationId)
34
+ @operations_by_id[operationId] || raise(::KeyError, -"operationId not found: #{operationId.inspect}")
35
+ end
36
+
37
+ alias_method(:[], :by_id!)
38
+
39
+ # @return [OperationsScope]
40
+ def select(&block)
41
+ OperationsScope.new(@enum.select(&block))
42
+ end
43
+
44
+ # @return [OperationsScope]
45
+ def reject(&block)
46
+ OperationsScope.new(@enum.reject(&block))
47
+ end
48
+
49
+ # Operations with the indicated tag
50
+ # @param tag [String, OpenAPI::Tag]
51
+ # @return [OperationsScope]
52
+ def tagged(tag)
53
+ tag_name = tag.is_a?(OpenAPI::Tag) ? tag.name : tag
54
+ select { |op| op.tagged?(tag_name) }
39
55
  end
40
56
  end
41
57
  end
@@ -3,41 +3,106 @@
3
3
  module Scorpio
4
4
  module OpenAPI
5
5
  module Reference
6
- # overrides JSI::Base#[] to implicitly dereference this Reference, except when
7
- # the given token is present in this Reference's instance (this should usually
8
- # only apply to the token '$ref')
6
+ # @private
7
+ module IncludeRecursive
8
+ def included(mod)
9
+ super
10
+ return if mod.is_a?(Class)
11
+ document_subschemas_include_derefable(mod)
12
+ mod.send(:extend, IncludeRecursive)
13
+ end
14
+
15
+ # - when a schema module includes OpenAPI::Reference
16
+ # - then we go to the schema's root node (which describes an OpenAPI Document)
17
+ # - and for each descendent schema, describing a part of an OAD
18
+ # - if that schema _could_ describe a reference, because its in-place applicators
19
+ # include the schema of the schema module including Reference
20
+ # (e.g. it has a oneOf with a $ref: '#/definitions/Reference')
21
+ # - then its schema module includes Derefable
22
+ def document_subschemas_include_derefable(mod)
23
+ return unless mod.is_a?(JSI::SchemaModule)
24
+
25
+ mod.schema.jsi_root_node.jsi_each_descendent_schema do |schema|
26
+ if to_enum(:schema_all_inplace_applicator_schemas, schema).include?(mod.schema)
27
+ schema.jsi_schema_module.include(Derefable)
28
+ end
29
+ end
30
+ end
31
+
32
+ # yield all of the schema's in-place applicator schemas, recursively, following $ref.
33
+ # TODO find a better home for this code (it is not particular to OpenAPI::Reference)
34
+ def schema_all_inplace_applicator_schemas(schema, &block)
35
+ yield schema
36
+ if schema.keyword?('$ref')
37
+ schema_all_inplace_applicator_schemas(schema.schema_ref.resolve, &block)
38
+ end
39
+ ia_elements = schema.dialect.elements.select { |element| element.invokes?(:inplace_applicate) }
40
+ cxt = JSI::Schema::Cxt::Block.new(
41
+ schema: schema,
42
+ abort: false,
43
+ block: proc { |ptr| schema_all_inplace_applicator_schemas(schema.subschema(ptr), &block) },
44
+ )
45
+ ia_elements.each do |element|
46
+ # getting subschemas yielded from action :subschema on elements that invoke :inplace_applicate
47
+ # is a kind of hacky way to get _all_ inplace applicators.
48
+ # getting applicator schemas normally comes from invoking :inplace_applicate, but that
49
+ # requires an instance and yields just the applicators that apply to that instance, not all.
50
+ #
51
+ # this does not work when the schemas an element yields from :subschema do not match its in-place
52
+ # applicator schemas. the only element that does that is for $ref which is handled above.
53
+ element.actions[:subschema].each do |action|
54
+ cxt.instance_exec(&action)
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ extend(IncludeRecursive)
61
+
62
+ # Derefable is included on the schema module of any schema that describes a reference or has an
63
+ # in-place applicator that describes a reference. You can call #deref regardless whether an object
64
+ # is of an expected type, or a reference to one, or a reference to a reference to one.
65
+ module Derefable
66
+ # resolves references ({Reference#resolve}) recursively.
67
+ def deref
68
+ return self unless is_a?(Reference) && has_ref?
69
+ resolved = resolve
70
+ return resolved if !resolved.is_a?(Reference)
71
+ resolved.deref
72
+ end
73
+ end
74
+
75
+ # overrides JSI::Base#[] to implicitly resolve this Reference, except when
76
+ # the given token is present in this Reference's instance.
77
+ # the token '$ref' will always come from this reference, not its resolution.
78
+ # tokens 'summary' and 'description' may also be in some references.
9
79
  def [](token, **kw)
10
80
  if respond_to?(:to_hash) && !key?(token)
11
- deref do |deref_jsi|
12
- return(deref_jsi[token, **kw])
81
+ resolve do |resolved|
82
+ return(resolved[token, **kw])
13
83
  end
14
84
  end
15
85
  return super
16
86
  end
17
87
 
18
- # yields or returns the target of this reference
19
- # @yield [JSI::Base] if a block is given
20
- # @return [JSI::Base]
21
- def deref
22
- return unless respond_to?(:to_hash) && self['$ref'].respond_to?(:to_str)
23
-
24
- ref_uri = Addressable::URI.parse(self['$ref'])
25
- ref_uri_nofrag = ref_uri.merge(fragment: nil)
88
+ # @return [Boolean]
89
+ def has_ref?
90
+ jsi_child_token_present?('$ref')
91
+ end
26
92
 
27
- if !ref_uri_nofrag.empty? || ref_uri.fragment.nil?
28
- raise(NotImplementedError,
29
- "Scorpio currently only supports fragment URIs as OpenAPI references. cannot find reference by uri: #{self['$ref']}"
30
- )
31
- end
93
+ # yields or returns the target of this reference. returns nil and does not yield if `$ref` is not present.
94
+ # @yield [JSI::Base] if a block is given
95
+ # @return [JSI::Base, nil]
96
+ def resolve
97
+ return unless has_ref?
32
98
 
33
- ptr = JSI::Ptr.from_fragment(ref_uri.fragment)
34
- deref_jsi = ptr.evaluate(jsi_root_node)
99
+ ref = @memos.fetch(:oa_ref) { @memos[:oa_ref] = JSI::Ref.new(jsi_node_content['$ref'], referrer: self) }
35
100
 
36
- # TODO type check deref_jsi
101
+ # TODO type check resolved
37
102
 
38
- yield deref_jsi if block_given?
103
+ yield ref.resolve if block_given?
39
104
 
40
- deref_jsi
105
+ ref.resolve
41
106
  end
42
107
  end
43
108
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Scorpio
4
+ module OpenAPI::SchemaElements
5
+ instance_types = {
6
+ 'boolean' => proc { instance == true || instance == false },
7
+ 'object' => proc { instance.respond_to?(:to_hash) },
8
+ 'array' => proc { instance.respond_to?(:to_ary) },
9
+ 'string' => proc { instance.respond_to?(:to_str) },
10
+ 'number' => proc { instance.is_a?(Numeric) },
11
+ 'integer' => proc { internal_integer?(instance) },
12
+ }.freeze
13
+
14
+ TYPE_NULLABLE = JSI::Schema::Element.new(keywords: ['type', 'nullable']) do |element|
15
+ element.add_action(:validate) do
16
+ next if !keyword?('type')
17
+ if instance.nil?
18
+ validate(
19
+ schema_content['nullable'] == true,
20
+ 'validation.keyword.type.not_nullable',
21
+ "instance is null without `nullable` = true",
22
+ keyword: 'nullable',
23
+ )
24
+ else
25
+ if !instance_types.key?(schema_content['type'])
26
+ next # schema error; skip validation
27
+ end
28
+ validate(
29
+ instance_exec(&instance_types[schema_content['type']]),
30
+ 'validation.keyword.type.not_match',
31
+ "instance type does not match `type` value",
32
+ keyword: 'type',
33
+ )
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Scorpio
4
+ module OpenAPI::SchemaElements
5
+ autoload(:TYPE_NULLABLE, 'scorpio/openapi/schema_elements/type_nullable')
6
+ end
7
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Scorpio
4
+ module OpenAPI
5
+ # An object representing a Server.
6
+ module Server
7
+ include(Document::Descendent)
8
+
9
+ # expands this server's #url template using the given_server_variables. any variables
10
+ # that are in the url but not in the given server variables are filled in
11
+ # using the default value for the variable.
12
+ #
13
+ # @param given_server_variables [Hash<String, String>]
14
+ # @return [Addressable::URI] the expanded url
15
+ def expanded_url(given_server_variables)
16
+ given_server_variables = JSI::Util.stringify_symbol_keys(given_server_variables)
17
+ if variables
18
+ server_variables = {}
19
+ (given_server_variables.keys | variables.keys).each do |key|
20
+ if given_server_variables.key?(key)
21
+ server_variables[key] = given_server_variables[key]
22
+ elsif variables[key].key?('default')
23
+ server_variables[key] = variables[key].default
24
+ end
25
+ end
26
+ else
27
+ server_variables = given_server_variables
28
+ end
29
+ template = Addressable::Template.new(url)
30
+ template.expand(server_variables).freeze
31
+ end
32
+ end
33
+ end
34
+ end