scorpio 0.4.2 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,7 @@
1
1
  module Scorpio
2
2
  module OpenAPI
3
3
  # A document that defines or describes an API.
4
- # An OpenAPI definition uses and conforms to the OpenAPI Specification.
4
+ # An OpenAPI description document uses and conforms to the OpenAPI Specification.
5
5
  #
6
6
  # Scorpio::OpenAPI::Document is a module common to V2 and V3 documents.
7
7
  module Document
@@ -12,24 +12,18 @@ module Scorpio
12
12
  # @param instance [#to_hash] the document to represent as a Scorpio OpenAPI Document
13
13
  # @return [Scorpio::OpenAPI::V2::Document, Scorpio::OpenAPI::V3::Document]
14
14
  def from_instance(instance)
15
- if instance.is_a?(Hash)
16
- instance = JSI::JSON::Node.new_doc(instance)
17
- end
18
- if instance.is_a?(JSI::JSON::Node)
19
- if instance['swagger'] =~ /\A2(\.|\z)/
20
- instance = Scorpio::OpenAPI::V2::Document.new(instance)
21
- elsif instance['openapi'] =~ /\A3(\.|\z)/
22
- instance = Scorpio::OpenAPI::V3::Document.new(instance)
23
- else
24
- raise(ArgumentError, "instance does not look like a recognized openapi document")
25
- end
26
- end
27
15
  if instance.is_a?(Scorpio::OpenAPI::Document)
28
16
  instance
29
17
  elsif instance.is_a?(JSI::Base)
30
18
  raise(TypeError, "instance is unexpected JSI type: #{instance.class.inspect}")
31
19
  elsif instance.respond_to?(:to_hash)
32
- from_instance(instance.to_hash)
20
+ if instance['swagger'] =~ /\A2(\.|\z)/
21
+ instance = Scorpio::OpenAPI::V2::Document.new_jsi(instance)
22
+ elsif instance['openapi'] =~ /\A3(\.|\z)/
23
+ instance = Scorpio::OpenAPI::V3::Document.new_jsi(instance)
24
+ else
25
+ raise(ArgumentError, "instance does not look like a recognized openapi document")
26
+ end
33
27
  else
34
28
  raise(TypeError, "instance does not look like a hash (json object)")
35
29
  end
@@ -89,7 +83,7 @@ module Scorpio
89
83
  # A document that defines or describes an API conforming to the OpenAPI Specification v3.
90
84
  #
91
85
  # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#oasObject
92
- class Document
86
+ module Document
93
87
  module Configurables
94
88
  def scheme
95
89
  nil
@@ -132,7 +126,7 @@ module Scorpio
132
126
  # A document that defines or describes an API conforming to the OpenAPI Specification v2 (aka Swagger).
133
127
  #
134
128
  # The root document is known as the Swagger Object.
135
- class Document
129
+ module Document
136
130
  module Configurables
137
131
  attr_writer :scheme
138
132
  def scheme
@@ -55,18 +55,14 @@ module Scorpio
55
55
 
56
56
  # @return [Scorpio::OpenAPI::Document] the document whence this operation came
57
57
  def openapi_document
58
- parents.detect { |p| p.is_a?(Scorpio::OpenAPI::Document) }
58
+ parent_jsis.detect { |p| p.is_a?(Scorpio::OpenAPI::Document) }
59
59
  end
60
60
 
61
61
  def path_template_str
62
62
  return @path_template_str if instance_variable_defined?(:@path_template_str)
63
- @path_template_str = begin
64
- parent_is_pathitem = parent.is_a?(Scorpio::OpenAPI::V2::PathItem) || parent.is_a?(Scorpio::OpenAPI::V3::PathItem)
65
- parent_parent_is_paths = parent.parent.is_a?(Scorpio::OpenAPI::V2::Paths) || parent.parent.is_a?(Scorpio::OpenAPI::V3::Paths)
66
- if parent_is_pathitem && parent_parent_is_paths
67
- parent.instance.path.last
68
- end
69
- end
63
+ raise(Bug) unless parent_jsi.is_a?(Scorpio::OpenAPI::V2::PathItem) || parent_jsi.is_a?(Scorpio::OpenAPI::V3::PathItem)
64
+ raise(Bug) unless parent_jsi.parent_jsi.is_a?(Scorpio::OpenAPI::V2::Paths) || parent_jsi.parent_jsi.is_a?(Scorpio::OpenAPI::V3::Paths)
65
+ @path_template_str = parent_jsi.jsi_ptr.reference_tokens.last
70
66
  end
71
67
 
72
68
  # @return [Addressable::Template] the path as an Addressable::Template
@@ -91,12 +87,8 @@ module Scorpio
91
87
  # for this operation from the parent PathItem
92
88
  def http_method
93
89
  return @http_method if instance_variable_defined?(:@http_method)
94
- @http_method = begin
95
- parent_is_pathitem = parent.is_a?(Scorpio::OpenAPI::V2::PathItem) || parent.is_a?(Scorpio::OpenAPI::V3::PathItem)
96
- if parent_is_pathitem
97
- instance.path.last
98
- end
99
- end
90
+ raise(Bug) unless parent_jsi.is_a?(Scorpio::OpenAPI::V2::PathItem) || parent_jsi.is_a?(Scorpio::OpenAPI::V3::PathItem)
91
+ @http_method = jsi_ptr.reference_tokens.last
100
92
  end
101
93
 
102
94
  # @return [String] a short identifier for this operation appropriate for an error message
@@ -182,7 +174,7 @@ module Scorpio
182
174
  # Describes a single API operation on a path.
183
175
  #
184
176
  # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operationObject
185
- class Operation
177
+ module Operation
186
178
  module Configurables
187
179
  def scheme
188
180
  # not applicable; for OpenAPI v3, scheme is specified by servers.
@@ -246,7 +238,7 @@ module Scorpio
246
238
  end
247
239
  module V2
248
240
  raise(Bug, 'const_defined? Scorpio::OpenAPI::V2::Operation') unless const_defined?(:Operation)
249
- class Operation
241
+ module Operation
250
242
  module Configurables
251
243
  attr_writer :scheme
252
244
  def scheme
@@ -272,7 +264,8 @@ module Scorpio
272
264
  end
273
265
  include Configurables
274
266
 
275
- # there should only be one body parameter; this returns it
267
+ # @return [#to_hash] the body parameter
268
+ # @raise [Scorpio::OpenAPI::SemanticError] if there's more than one body param
276
269
  def body_parameter
277
270
  body_parameters = (parameters || []).select { |parameter| parameter['in'] == 'body' }
278
271
  if body_parameters.size == 0
@@ -3,7 +3,7 @@ module Scorpio
3
3
  # OperationsScope acts as an Enumerable of the Operations for an openapi_document,
4
4
  # and offers subscripting by operationId.
5
5
  class OperationsScope
6
- include JSI::Memoize
6
+ include JSI::Util::Memoize
7
7
 
8
8
  # @param openapi_document [Scorpio::OpenAPI::Document]
9
9
  def initialize(openapi_document)
@@ -25,9 +25,14 @@ module Scorpio
25
25
 
26
26
  # @param operationId
27
27
  # @return [Scorpio::OpenAPI::Operation] the operation with the given operationId
28
+ # @raise [::KeyError] if the given operationId does not exist
28
29
  def [](operationId)
29
- memoize(:[], operationId) do |operationId_|
30
- detect { |operation| operation.operationId == operationId_ }
30
+ jsi_memoize(:[], operationId) do |operationId_|
31
+ detect { |operation| operation.operationId == operationId_ }.tap do |op|
32
+ unless op
33
+ raise(::KeyError, "operationId not found: #{operationId_.inspect}")
34
+ end
35
+ end
31
36
  end
32
37
  end
33
38
  end
@@ -0,0 +1,19 @@
1
+ module Scorpio
2
+ module OpenAPI
3
+ module Reference
4
+ # overrides JSI::Base#[] to implicitly dereference this Reference, except when
5
+ # the given token is present in this Reference's instance (this should usually
6
+ # only apply to the token '$ref')
7
+ #
8
+ # see JSI::Base#initialize documentation at https://www.rubydoc.info/gems/jsi/JSI/Base
9
+ def [](token, *a, &b)
10
+ if respond_to?(:to_hash) && !key?(token)
11
+ deref do |deref_jsi|
12
+ return deref_jsi[token]
13
+ end
14
+ end
15
+ return super
16
+ end
17
+ end
18
+ end
19
+ end
@@ -6,7 +6,7 @@ module Scorpio
6
6
  # An object representing a Server.
7
7
  #
8
8
  # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#serverObject
9
- class Server
9
+ module Server
10
10
  # expands this server's #url using the given_server_variables. any variables
11
11
  # that are in the url but not in the given server variables are filled in
12
12
  # using the default value for the variable.
@@ -1,6 +1,8 @@
1
1
  module Scorpio
2
2
  class Request
3
3
  SUPPORTED_REQUEST_MEDIA_TYPES = ['application/json', 'application/x-www-form-urlencoded']
4
+ FALLBACK_CONTENT_TYPE = 'application/x-www-form-urlencoded'
5
+
4
6
  def self.best_media_type(media_types)
5
7
  if media_types.size == 1
6
8
  media_types.first
@@ -50,10 +52,9 @@ module Scorpio
50
52
  def body
51
53
  return @body if instance_variable_defined?(:@body)
52
54
  if instance_variable_defined?(:@body_object)
53
- # TODO handle media types like `application/schema-instance+json`
54
- if media_type == 'application/json'
55
+ if content_type && content_type.json?
55
56
  JSON.pretty_generate(JSI::Typelike.as_json(body_object))
56
- elsif media_type == "application/x-www-form-urlencoded"
57
+ elsif content_type && content_type.form_urlencoded?
57
58
  URI.encode_www_form(body_object)
58
59
 
59
60
  # NOTE: the supported media types above should correspond to Request::SUPPORTED_REQUEST_MEDIA_TYPES
@@ -62,7 +63,7 @@ module Scorpio
62
63
  if body_object.respond_to?(:to_str)
63
64
  body_object
64
65
  else
65
- raise(NotImplementedError, "Scorpio does not know how to generate the request body with media_type = #{media_type.respond_to?(:to_str) ? media_type : media_type.inspect} for operation: #{operation.human_id}. Scorpio supports media types: #{SUPPORTED_REQUEST_MEDIA_TYPES.join(', ')}. body_object was: #{body_object.pretty_inspect.chomp}")
66
+ raise(NotImplementedError, "Scorpio does not know how to generate the request body with content_type = #{content_type.respond_to?(:to_str) ? content_type : content_type.inspect} for operation: #{operation.human_id}. Scorpio supports media types: #{SUPPORTED_REQUEST_MEDIA_TYPES.join(', ')}. body_object was: #{body_object.pretty_inspect.chomp}")
66
67
  end
67
68
  end
68
69
  else
@@ -81,7 +82,7 @@ module Scorpio
81
82
  attr_writer :media_type
82
83
  def media_type
83
84
  return @media_type if instance_variable_defined?(:@media_type)
84
- content_type_header ? content_type_attrs.media_type : operation.request_media_type
85
+ content_type_header ? content_type_header.media_type : operation.request_media_type
85
86
  end
86
87
 
87
88
  attr_writer :user_agent
@@ -190,23 +191,18 @@ module Scorpio
190
191
  Addressable::URI.parse(File.join(base_url, path))
191
192
  end
192
193
 
193
- # @return [::Ur::ContentTypeAttrs] content type attributes for this request's Content-Type
194
- def content_type_attrs
195
- Ur::ContentTypeAttrs.new(content_type)
196
- end
197
-
198
- # @return [String] the value of the request Content-Type header
194
+ # @return [::Ur::ContentType] the value of the request Content-Type header
199
195
  def content_type_header
200
196
  headers.each do |k, v|
201
- return v if k =~ /\Acontent[-_]type\z/i
197
+ return ::Ur::ContentType.new(v) if k =~ /\Acontent[-_]type\z/i
202
198
  end
203
199
  nil
204
200
  end
205
201
 
206
- # @return [String] Content-Type for this request, taken from request headers if
202
+ # @return [::Ur::ContentType] Content-Type for this request, taken from request headers if
207
203
  # present, or the request media_type.
208
204
  def content_type
209
- content_type_header || media_type
205
+ content_type_header || (media_type ? ::Ur::ContentType.new(media_type) : nil)
210
206
  end
211
207
 
212
208
  # @return [::JSI::Schema]
@@ -214,11 +210,6 @@ module Scorpio
214
210
  operation.request_schema(media_type: media_type)
215
211
  end
216
212
 
217
- # @return [Class subclassing JSI::Base]
218
- def request_schema_class(media_type: self.media_type)
219
- JSI.class_for_schema(request_schema(media_type: media_type))
220
- end
221
-
222
213
  # builds a Faraday connection with this Request's faraday_builder and faraday_adapter.
223
214
  # passes a given proc yield_ur to middleware to yield an Ur for requests made with the connection.
224
215
  #
@@ -228,8 +219,9 @@ module Scorpio
228
219
  Faraday.new do |faraday_connection|
229
220
  faraday_builder.call(faraday_connection)
230
221
  if yield_ur
231
- ::Ur::Faraday # autoload trigger
232
- faraday_connection.response(:yield_ur, ur_class: Scorpio::Ur, logger: self.logger, &yield_ur)
222
+ -> { ::Ur::Faraday }.() # autoload trigger
223
+
224
+ faraday_connection.response(:yield_ur, schemas: Set[Scorpio::Ur.schema], logger: self.logger, &yield_ur)
233
225
  end
234
226
  faraday_connection.adapter(*faraday_adapter)
235
227
  end
@@ -328,8 +320,14 @@ module Scorpio
328
320
  if user_agent
329
321
  headers['User-Agent'] = user_agent
330
322
  end
331
- if media_type && !content_type_header
332
- headers['Content-Type'] = media_type
323
+ if !content_type_header
324
+ if media_type
325
+ headers['Content-Type'] = media_type
326
+ else
327
+ # I'd rather not have a default content-type, but if none is set then the HTTP adapter sets this to
328
+ # application/x-www-form-urlencoded and issues a warning about it.
329
+ headers['Content-Type'] = FALLBACK_CONTENT_TYPE
330
+ end
333
331
  end
334
332
  if self.headers
335
333
  headers.update(self.headers)
@@ -6,18 +6,20 @@ module Scorpio
6
6
 
7
7
  class ResourceBase
8
8
  class << self
9
- # a hash of accessor names (Symbol) to default getter methods (UnboundMethod), used to determine
10
- # what accessors have been overridden from their defaults.
9
+ # ResourceBase.inheritable_accessor_defaults is a hash of accessor names (Symbol) mapped
10
+ # to default getter methods (UnboundMethod), used to determine what accessors have been
11
+ # overridden from their defaults.
11
12
  (-> (x) { define_method(:inheritable_accessor_defaults) { x } }).({})
12
- def define_inheritable_accessor(accessor, options = {})
13
- if options[:default_getter]
14
- # the value before the field is set (overwritten) is the result of the default_getter proc
15
- define_singleton_method(accessor, &options[:default_getter])
16
- else
17
- # the value before the field is set (overwritten) is the default_value (which is nil if not specified)
18
- default_value = options[:default_value]
19
- define_singleton_method(accessor) { default_value }
20
- end
13
+
14
+ # @param accessor [String, Symbol] the name of the accessor
15
+ # @param default_getter [#to_proc] a proc to provide a default value when no value
16
+ # has been explicitly set
17
+ # @param default_value [Object] a default value to return when no value has been
18
+ # explicitly set. do not pass both :default_getter and :default_value.
19
+ # @param on_set [#to_proc] callback proc, invoked when a value is assigned
20
+ def define_inheritable_accessor(accessor, default_value: nil, default_getter: -> { default_value }, on_set: nil)
21
+ # the value before the field is set (overwritten) is the result of the default_getter proc
22
+ define_singleton_method(accessor, &default_getter)
21
23
  inheritable_accessor_defaults[accessor] = self.singleton_class.instance_method(accessor)
22
24
  # field setter method. redefines the getter, replacing the method with one that returns the
23
25
  # setter's argument (that being inherited to the scope of the define_method(accessor) block
@@ -33,8 +35,8 @@ module Scorpio
33
35
  # getter method
34
36
  define_method(accessor) { value_ }
35
37
  # invoke on_set callback defined on the class
36
- if options[:on_set]
37
- klass.instance_exec(&options[:on_set])
38
+ if on_set
39
+ klass.instance_exec(&on_set)
38
40
  end
39
41
  end
40
42
  end
@@ -257,7 +259,7 @@ module Scorpio
257
259
  end
258
260
 
259
261
  def call_operation(operation, call_params: nil, model_attributes: nil)
260
- call_params = JSI.stringify_symbol_keys(call_params) if call_params.is_a?(Hash)
262
+ call_params = JSI.stringify_symbol_keys(call_params) if call_params.respond_to?(:to_hash)
261
263
  model_attributes = JSI.stringify_symbol_keys(model_attributes || {})
262
264
 
263
265
  request = Scorpio::Request.new(operation)
@@ -312,7 +314,7 @@ module Scorpio
312
314
  # TODO deal with model_attributes / call_params better in nested whatever
313
315
  if call_params.nil?
314
316
  request.body_object = request_body_for_schema(model_attributes, operation.request_schema)
315
- elsif call_params.is_a?(Hash)
317
+ elsif call_params.respond_to?(:to_hash)
316
318
  body = request_body_for_schema(model_attributes.merge(call_params), operation.request_schema)
317
319
  request.body_object = body.merge(call_params) # TODO
318
320
  else
@@ -323,7 +325,7 @@ module Scorpio
323
325
  if METHODS_WITH_BODIES.any? { |m| m.to_s == operation.http_method.downcase.to_s }
324
326
  request.body_object = other_params
325
327
  else
326
- if other_params.is_a?(Hash)
328
+ if other_params.respond_to?(:to_hash)
327
329
  # TODO pay more attention to 'parameters' api method attribute
328
330
  request.query_params = other_params
329
331
  else
@@ -349,17 +351,15 @@ module Scorpio
349
351
  if object.is_a?(Scorpio::ResourceBase)
350
352
  # TODO request_schema_fail unless schema is for given model type
351
353
  request_body_for_schema(object.attributes, schema)
352
- elsif object.is_a?(JSI::Base)
353
- request_body_for_schema(object.instance, schema)
354
- elsif object.is_a?(JSI::JSON::Node)
355
- request_body_for_schema(object.content, schema)
354
+ elsif object.is_a?(JSI::PathedNode)
355
+ request_body_for_schema(object.node_content, schema)
356
356
  else
357
- if object.is_a?(Hash)
357
+ if object.respond_to?(:to_hash)
358
358
  object.map do |key, value|
359
359
  if schema
360
360
  if schema['type'] == 'object'
361
361
  # TODO code dup with response_object_to_instances
362
- if schema['properties'].respond_to?(:to_hash) && schema['properties'][key]
362
+ if schema['properties'].respond_to?(:to_hash) && schema['properties'].key?(key)
363
363
  subschema = schema['properties'][key]
364
364
  include_pair = true
365
365
  else
@@ -397,7 +397,7 @@ module Scorpio
397
397
  {}
398
398
  end
399
399
  end.inject({}, &:update)
400
- elsif object.is_a?(Array) || object.is_a?(Set)
400
+ elsif object.respond_to?(:to_ary) || object.is_a?(Set)
401
401
  object.map do |el|
402
402
  if schema
403
403
  if schema['type'] == 'array'
@@ -423,7 +423,14 @@ module Scorpio
423
423
 
424
424
  def response_object_to_instances(object, initialize_options = {})
425
425
  if object.is_a?(JSI::Base)
426
- model = models_by_schema[object.schema]
426
+ models = object.jsi_schemas.map { |schema| models_by_schema[schema] }
427
+ if models.size == 0
428
+ model = nil
429
+ elsif models.size == 1
430
+ model = models.first
431
+ else
432
+ raise(Scorpio::OpenAPI::Error, "multiple models indicated by response JSI. models: #{models.inspect}; jsi: #{jsi.pretty_inspect.chomp}")
433
+ end
427
434
  end
428
435
 
429
436
  if object.respond_to?(:to_hash)
@@ -431,8 +438,7 @@ module Scorpio
431
438
  mod = object.map do |key, value|
432
439
  {key => response_object_to_instances(value, initialize_options)}
433
440
  end.inject({}, &:update)
434
- mod = mod.instance if mod.is_a?(JSI::Base)
435
- mod = mod.content if mod.is_a?(JSI::JSON::Node)
441
+ mod = mod.node_content if mod.is_a?(JSI::PathedNode)
436
442
  mod
437
443
  end
438
444
  if model
@@ -520,9 +526,9 @@ module Scorpio
520
526
  end
521
527
  end
522
528
 
523
- def fingerprint
529
+ def jsi_fingerprint
524
530
  {class: self.class, attributes: JSI::Typelike.as_json(@attributes)}
525
531
  end
526
- include JSI::FingerprintHash
532
+ include JSI::Util::FingerprintHash
527
533
  end
528
534
  end
@@ -1,5 +1,7 @@
1
1
  module Scorpio
2
- class Response < ::Ur::Response
2
+ Response = Scorpio::Ur.properties['response']
3
+
4
+ module Response
3
5
  # @return [::JSI::Schema] the schema for this response according to its OpenAPI doc
4
6
  def response_schema
5
7
  ur.scorpio_request.operation.response_schema(status: status, media_type: media_type)
@@ -9,8 +11,7 @@ module Scorpio
9
11
  # if supported (only application/json is currently supported) instantiated according to
10
12
  # #response_schema
11
13
  def body_object
12
- # TODO handle media types like `application/schema-instance+json` or vendor things like github's
13
- if media_type == 'application/json'
14
+ if json?
14
15
  if body.empty?
15
16
  # an empty body isn't valid json, of course, but we'll just return nil for it.
16
17
  body_object = nil
@@ -23,11 +24,11 @@ module Scorpio
23
24
  end
24
25
 
25
26
  if response_schema && (body_object.respond_to?(:to_hash) || body_object.respond_to?(:to_ary))
26
- body_object = JSI.class_for_schema(response_schema).new(JSI::JSON::Node.new_doc(body_object))
27
+ body_object = response_schema.new_jsi(body_object)
27
28
  end
28
29
 
29
30
  body_object
30
- elsif media_type == 'text/plain'
31
+ elsif content_type && content_type.type_text? && content_type.subtype?('plain')
31
32
  body
32
33
  else
33
34
  # we will return the body if we do not have a supported parsing. for now.