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.
- checksums.yaml +4 -4
- data/.yardopts +6 -1
- data/CHANGELOG.md +10 -2
- data/LICENSE.md +2 -4
- data/README.md +81 -67
- data/documents/{github.com/OAI/OpenAPI-Specification/blob/oas3-schema/schemas/v3.0 → spec.openapis.org/oas/3.0}/schema.yaml +164 -628
- data/documents/spec.openapis.org/oas/3.1/dialect/base.schema.yaml +22 -0
- data/documents/spec.openapis.org/oas/3.1/meta/base.schema.yaml +71 -0
- data/documents/spec.openapis.org/oas/3.1/schema-base.yaml +21 -0
- data/documents/spec.openapis.org/oas/3.1/schema.yaml +980 -0
- data/documents/swagger.io/v2/schema.json +1 -1
- data/documents/www.googleapis.com/discovery/v1/apis/discovery/v1/rest.yml +44 -4
- data/lib/scorpio/google_api_document.rb +121 -193
- data/lib/scorpio/openapi/document.rb +63 -31
- data/lib/scorpio/openapi/operation.rb +114 -96
- data/lib/scorpio/openapi/operations_scope.rb +35 -19
- data/lib/scorpio/openapi/reference.rb +88 -23
- data/lib/scorpio/openapi/schema_elements/type_nullable.rb +38 -0
- data/lib/scorpio/openapi/schema_elements.rb +7 -0
- data/lib/scorpio/openapi/server.rb +34 -0
- data/lib/scorpio/openapi/tag.rb +19 -3
- data/lib/scorpio/openapi/v2/dialect.rb +66 -0
- data/lib/scorpio/openapi/v2.rb +124 -0
- data/lib/scorpio/openapi/v3_0/dialect.rb +76 -0
- data/lib/scorpio/openapi/v3_0.rb +130 -0
- data/lib/scorpio/openapi/v3_1.rb +243 -0
- data/lib/scorpio/openapi.rb +23 -203
- data/lib/scorpio/request.rb +67 -61
- data/lib/scorpio/resource_base.rb +57 -49
- data/lib/scorpio/response.rb +28 -10
- data/lib/scorpio/ur.rb +7 -3
- data/lib/scorpio/version.rb +1 -1
- data/lib/scorpio.rb +12 -6
- data/pages/Request_Configuration.md +69 -0
- data/pages/Security.md +50 -0
- data/scorpio.gemspec +6 -5
- metadata +28 -15
- data/documents/www.googleapis.com/discovery/v1/apis/discovery/v1/rest +0 -684
- data/lib/scorpio/openapi/v3/server.rb +0 -44
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Scorpio
|
|
4
|
-
class RequestSchemaFailure < Error
|
|
5
|
-
end
|
|
6
|
-
|
|
7
4
|
class ResourceBase
|
|
8
5
|
class << self
|
|
9
6
|
# ResourceBase.inheritable_accessor_defaults is a hash of accessor names (Symbol) mapped
|
|
@@ -22,23 +19,20 @@ module Scorpio
|
|
|
22
19
|
define_singleton_method(accessor, &default_getter)
|
|
23
20
|
inheritable_accessor_defaults[accessor] = singleton_class.instance_method(accessor)
|
|
24
21
|
# field setter method. redefines the getter, replacing the method with one that returns the
|
|
25
|
-
# setter's argument
|
|
22
|
+
# setter's argument `value`
|
|
26
23
|
define_singleton_method(:"#{accessor}=") do |value|
|
|
27
|
-
# the setter operates on the singleton class of the receiver (self)
|
|
28
|
-
singleton_class.instance_exec(value, self) do |value_, klass|
|
|
29
24
|
# remove a previous getter. NameError is raised if a getter is not defined on this class;
|
|
30
25
|
# this may be ignored.
|
|
31
26
|
begin
|
|
32
|
-
remove_method
|
|
27
|
+
singleton_class.send(:remove_method, accessor)
|
|
33
28
|
rescue NameError
|
|
34
29
|
end
|
|
35
30
|
# getter method
|
|
36
|
-
|
|
31
|
+
define_singleton_method(accessor) { value }
|
|
37
32
|
# invoke on_set callback defined on the class
|
|
38
33
|
if on_set
|
|
39
|
-
|
|
34
|
+
instance_exec(&on_set)
|
|
40
35
|
end
|
|
41
|
-
end
|
|
42
36
|
end
|
|
43
37
|
end
|
|
44
38
|
end
|
|
@@ -50,7 +44,7 @@ module Scorpio
|
|
|
50
44
|
end
|
|
51
45
|
update_dynamic_methods
|
|
52
46
|
else
|
|
53
|
-
self.represented_schemas = JSI::SchemaSet.
|
|
47
|
+
self.represented_schemas = JSI::SchemaSet.new(represented_schemas)
|
|
54
48
|
end
|
|
55
49
|
end)
|
|
56
50
|
define_inheritable_accessor(:models_by_schema, default_value: {}.freeze)
|
|
@@ -72,8 +66,8 @@ module Scorpio
|
|
|
72
66
|
if openapi_document && openapi_document.v2?
|
|
73
67
|
raise(ArgumentError, "servers are not supported for OpenAPI V2")
|
|
74
68
|
end
|
|
75
|
-
unless server.is_a?(
|
|
76
|
-
raise(TypeError, "server must be an #{
|
|
69
|
+
unless server.is_a?(OpenAPI::Server)
|
|
70
|
+
raise(TypeError, -"server must be an #{OpenAPI::Server}. received: #{server.pretty_inspect.chomp}")
|
|
77
71
|
end
|
|
78
72
|
})
|
|
79
73
|
|
|
@@ -91,7 +85,7 @@ module Scorpio
|
|
|
91
85
|
end
|
|
92
86
|
|
|
93
87
|
def openapi_document=(openapi_document)
|
|
94
|
-
openapi_document =
|
|
88
|
+
openapi_document = Scorpio.new_document(openapi_document)
|
|
95
89
|
|
|
96
90
|
begin
|
|
97
91
|
singleton_class.instance_exec { remove_method(:openapi_document) }
|
|
@@ -106,9 +100,9 @@ module Scorpio
|
|
|
106
100
|
define_singleton_method(:openapi_document_class) { openapi_document_class }
|
|
107
101
|
define_singleton_method(:openapi_document=) do |_|
|
|
108
102
|
if self == openapi_document_class
|
|
109
|
-
raise(ArgumentError, "openapi_document may only be set once on #{inspect}")
|
|
103
|
+
raise(ArgumentError, -"openapi_document may only be set once on #{inspect}")
|
|
110
104
|
else
|
|
111
|
-
raise(ArgumentError, "openapi_document may not be overridden on subclass #{inspect} after it was set on #{openapi_document_class.inspect}")
|
|
105
|
+
raise(ArgumentError, -"openapi_document may not be overridden on subclass #{inspect} after it was set on #{openapi_document_class.inspect}")
|
|
112
106
|
end
|
|
113
107
|
end
|
|
114
108
|
# TODO blame validate openapi_document
|
|
@@ -121,7 +115,7 @@ module Scorpio
|
|
|
121
115
|
|
|
122
116
|
def tag_name=(tag_name)
|
|
123
117
|
unless tag_name.respond_to?(:to_str)
|
|
124
|
-
raise(TypeError, "tag_name must be a string; got: #{tag_name.inspect}")
|
|
118
|
+
raise(TypeError, -"tag_name must be a string; got: #{tag_name.inspect}")
|
|
125
119
|
end
|
|
126
120
|
tag_name = tag_name.to_str
|
|
127
121
|
|
|
@@ -132,7 +126,7 @@ module Scorpio
|
|
|
132
126
|
define_singleton_method(:tag_name) { tag_name }
|
|
133
127
|
define_singleton_method(:tag_name=) do |tag_name|
|
|
134
128
|
unless tag_name == self.tag_name
|
|
135
|
-
raise(ArgumentError, "tag_name may not be overridden (to #{tag_name.inspect}). it is been set to #{self.tag_name.inspect}")
|
|
129
|
+
raise(ArgumentError, -"tag_name may not be overridden (to #{tag_name.inspect}). it is been set to #{self.tag_name.inspect}")
|
|
136
130
|
end
|
|
137
131
|
end
|
|
138
132
|
update_dynamic_methods
|
|
@@ -163,13 +157,16 @@ module Scorpio
|
|
|
163
157
|
end
|
|
164
158
|
|
|
165
159
|
def operation_for_resource_class?(operation)
|
|
166
|
-
return true if tag_name && operation.
|
|
160
|
+
return true if tag_name && operation.tagged?(tag_name)
|
|
167
161
|
|
|
168
162
|
request_response_schemas = operation.request_schemas | operation.response_schemas
|
|
169
163
|
# TODO/FIX nil instance is wrong. works for $ref and allOf, not for others.
|
|
170
164
|
# use all inplace applicators, not conditional on instance
|
|
171
|
-
|
|
172
|
-
|
|
165
|
+
request_response_schemas.each do |s|
|
|
166
|
+
s.each_inplace_applicator_schema(nil) do |ias|
|
|
167
|
+
return true if represented_schemas.include?(ias)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
173
170
|
|
|
174
171
|
return false
|
|
175
172
|
end
|
|
@@ -181,11 +178,14 @@ module Scorpio
|
|
|
181
178
|
#
|
|
182
179
|
# TODO/FIX nil instance is wrong. works for $ref and allOf, not for others.
|
|
183
180
|
# use all inplace applicators, not conditional on instance
|
|
184
|
-
|
|
185
|
-
|
|
181
|
+
operation.request_schemas.each do |s|
|
|
182
|
+
s.each_inplace_applicator_schema(nil) do |ias|
|
|
183
|
+
return true if represented_schemas.include?(ias)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
186
|
|
|
187
187
|
# the below only apply if the operation has this resource's tag
|
|
188
|
-
return false unless tag_name && operation.
|
|
188
|
+
return false unless tag_name && operation.tagged?(tag_name)
|
|
189
189
|
|
|
190
190
|
# define an instance method if path or query params can be filled in from
|
|
191
191
|
# property names described by represented_schemas
|
|
@@ -223,8 +223,7 @@ module Scorpio
|
|
|
223
223
|
# if the operationId is just "addPet"
|
|
224
224
|
# then the operation's method name on Pet will be "addPet".
|
|
225
225
|
tag_name_match = tag_name &&
|
|
226
|
-
operation.
|
|
227
|
-
operation.tags.include?(tag_name) &&
|
|
226
|
+
operation.tagged?(tag_name) &&
|
|
228
227
|
operation.operationId &&
|
|
229
228
|
operation.operationId.match(/\A#{Regexp.escape(tag_name)}[\.\/\:](\w+)\z/)
|
|
230
229
|
|
|
@@ -236,9 +235,7 @@ module Scorpio
|
|
|
236
235
|
end
|
|
237
236
|
|
|
238
237
|
def update_class_and_instance_api_methods
|
|
239
|
-
openapi_document.
|
|
240
|
-
path_item.each do |http_method, operation|
|
|
241
|
-
next unless operation.is_a?(Scorpio::OpenAPI::Operation)
|
|
238
|
+
openapi_document.operations.each do |operation|
|
|
242
239
|
method_name = api_method_name_by_operation(operation)
|
|
243
240
|
if method_name
|
|
244
241
|
# class method
|
|
@@ -255,11 +252,15 @@ module Scorpio
|
|
|
255
252
|
end
|
|
256
253
|
end
|
|
257
254
|
end
|
|
258
|
-
end
|
|
259
255
|
end
|
|
260
256
|
end
|
|
261
257
|
|
|
262
258
|
def call_operation(operation, call_params: nil, model_attributes: nil)
|
|
259
|
+
result, _ur = call_operation_ur(operation, call_params: call_params, model_attributes: model_attributes)
|
|
260
|
+
result
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def call_operation_ur(operation, call_params: nil, model_attributes: nil)
|
|
263
264
|
call_params = JSI::Util.stringify_symbol_keys(call_params) if call_params.respond_to?(:to_hash)
|
|
264
265
|
model_attributes = JSI::Util.stringify_symbol_keys(model_attributes || {})
|
|
265
266
|
|
|
@@ -311,18 +312,19 @@ module Scorpio
|
|
|
311
312
|
other_params = call_params
|
|
312
313
|
end
|
|
313
314
|
|
|
314
|
-
|
|
315
|
+
request_schema = request.request_schema
|
|
316
|
+
if request_schema
|
|
315
317
|
request_body_for_schema = -> (o) do
|
|
316
318
|
if o.is_a?(JSI::Base)
|
|
317
319
|
# TODO check indicated schemas
|
|
318
|
-
if o.jsi_schemas.include?(
|
|
320
|
+
if o.jsi_schemas.include?(request_schema)
|
|
319
321
|
jsi = o
|
|
320
322
|
else
|
|
321
323
|
# TODO maybe better way than reinstantiating another jsi as request_schema
|
|
322
|
-
jsi =
|
|
324
|
+
jsi = request_schema.new_jsi(o.jsi_node_content)
|
|
323
325
|
end
|
|
324
326
|
else
|
|
325
|
-
jsi =
|
|
327
|
+
jsi = request_schema.new_jsi(o)
|
|
326
328
|
end
|
|
327
329
|
jsi.jsi_select_descendents_leaf_first do |node|
|
|
328
330
|
# we want to specifically reject only nodes described (only) by a false schema.
|
|
@@ -342,12 +344,19 @@ module Scorpio
|
|
|
342
344
|
end
|
|
343
345
|
else
|
|
344
346
|
if other_params
|
|
345
|
-
if
|
|
347
|
+
if request.http_method_with_body?
|
|
346
348
|
request.body_object = other_params
|
|
347
349
|
else
|
|
348
350
|
if other_params.respond_to?(:to_hash)
|
|
349
|
-
|
|
350
|
-
|
|
351
|
+
other_params.to_hash.each_pair do |name, value|
|
|
352
|
+
param = request.param_for(name)
|
|
353
|
+
if param
|
|
354
|
+
request.set_param_from(param['in'], param['name'], value)
|
|
355
|
+
else
|
|
356
|
+
# set any params not described by operation params in the query
|
|
357
|
+
request.set_param_from('query', name, value)
|
|
358
|
+
end
|
|
359
|
+
end
|
|
351
360
|
else
|
|
352
361
|
raise
|
|
353
362
|
end
|
|
@@ -364,7 +373,7 @@ module Scorpio
|
|
|
364
373
|
'source' => {'operationId' => operation.operationId, 'call_params' => call_params, 'url' => ur.request.uri.to_s},
|
|
365
374
|
'ur' => ur,
|
|
366
375
|
}
|
|
367
|
-
response_object_to_instances(ur.response.body_object, initialize_options)
|
|
376
|
+
[response_object_to_instances(ur.response.body_object(mutable: true), initialize_options), ur]
|
|
368
377
|
end
|
|
369
378
|
|
|
370
379
|
def response_object_to_instances(object, initialize_options = {})
|
|
@@ -375,7 +384,7 @@ module Scorpio
|
|
|
375
384
|
elsif models.size == 1
|
|
376
385
|
model = models.first
|
|
377
386
|
else
|
|
378
|
-
raise(Scorpio::OpenAPI::Error, "multiple models indicated by response JSI. models: #{models.inspect}; object: #{object.pretty_inspect.chomp}")
|
|
387
|
+
raise(Scorpio::OpenAPI::Error, -"multiple models indicated by response JSI. models: #{models.inspect}; object: #{object.pretty_inspect.chomp}")
|
|
379
388
|
end
|
|
380
389
|
|
|
381
390
|
if model && object.respond_to?(:to_hash)
|
|
@@ -422,12 +431,12 @@ module Scorpio
|
|
|
422
431
|
end
|
|
423
432
|
|
|
424
433
|
def inspect
|
|
425
|
-
"\#<#{self.class.inspect} #{contained_object.inspect}>"
|
|
434
|
+
-"\#<#{self.class.inspect} #{contained_object.inspect}>"
|
|
426
435
|
end
|
|
427
436
|
|
|
428
437
|
def pretty_print(q)
|
|
429
438
|
q.instance_exec(self) do |obj|
|
|
430
|
-
text
|
|
439
|
+
text(-"\#<#{obj.class.inspect}")
|
|
431
440
|
group_sub {
|
|
432
441
|
nest(2) {
|
|
433
442
|
breakable ' '
|
|
@@ -474,15 +483,14 @@ module Scorpio
|
|
|
474
483
|
end
|
|
475
484
|
|
|
476
485
|
def call_operation(operation, call_params: nil)
|
|
477
|
-
response = self.class.
|
|
486
|
+
response, ur = self.class.call_operation_ur(operation, call_params: call_params, model_attributes: self.attributes)
|
|
478
487
|
|
|
479
488
|
# if we're making a POST or PUT and the request schema is this resource, we'll assume that
|
|
480
489
|
# the request is persisting this resource
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
response_resource_is_self = response_schema && self.class.represented_schemas.include?(response_schema)
|
|
490
|
+
request_body_object = ur.scorpio_request.body_object
|
|
491
|
+
request_resource_is_self = request_body_object.is_a?(JSI::Base) &&
|
|
492
|
+
request_body_object.jsi_schemas.any? { |s| self.class.represented_schemas.include?(s) }
|
|
493
|
+
response_resource_is_self = response.is_a?(self.class)
|
|
486
494
|
if request_resource_is_self && %w(put post).include?(operation.http_method.to_s.downcase)
|
|
487
495
|
@persisted = true
|
|
488
496
|
|
|
@@ -557,7 +565,7 @@ module Scorpio
|
|
|
557
565
|
ks = k.is_a?(String) ? k :
|
|
558
566
|
k.is_a?(Symbol) ? k.to_s :
|
|
559
567
|
k.respond_to?(:to_str) && (kstr = k.to_str).is_a?(String) ? kstr :
|
|
560
|
-
raise(TypeError, "JSON object (Hash) cannot be keyed with: #{k.pretty_inspect.chomp}")
|
|
568
|
+
raise(TypeError, -"JSON object (Hash) cannot be keyed with: #{k.pretty_inspect.chomp}")
|
|
561
569
|
hash[ks] = JSI::Util.as_json(self[k], **options)
|
|
562
570
|
end
|
|
563
571
|
hash
|
|
@@ -608,7 +616,7 @@ module Scorpio
|
|
|
608
616
|
if schema_names.empty?
|
|
609
617
|
[Container.to_s]
|
|
610
618
|
else
|
|
611
|
-
["#{Container} (#{schema_names.join(', ')})"]
|
|
619
|
+
[-"#{Container} (#{schema_names.join(', ')})"]
|
|
612
620
|
end
|
|
613
621
|
end
|
|
614
622
|
end
|
data/lib/scorpio/response.rb
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
module Scorpio
|
|
4
4
|
Response = Scorpio::Ur.properties['response']
|
|
5
5
|
|
|
6
|
+
# Scorpio::Response is a JSI schema module describing the same instances as [::Ur::Response](https://rubydoc.info/gems/ur/Ur/Response).
|
|
7
|
+
# It relies on methods of that module.
|
|
6
8
|
module Response
|
|
7
9
|
# the schema for this response according to its OpenAPI doc
|
|
8
10
|
# @return [::JSI::Schema]
|
|
@@ -10,32 +12,48 @@ module Scorpio
|
|
|
10
12
|
ur.scorpio_request.operation.response_schema(status: status, media_type: media_type)
|
|
11
13
|
end
|
|
12
14
|
|
|
13
|
-
#
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
# media types for which Scorpio has implemented parsing {Response#body_object} from `Response#body`
|
|
16
|
+
SUPPORTED_MEDIA_TYPES = %w(
|
|
17
|
+
application/json
|
|
18
|
+
).map(&:freeze).freeze
|
|
19
|
+
|
|
20
|
+
# the body (String) is parsed according to the response media type, if
|
|
21
|
+
# supported (see {Response::SUPPORTED_MEDIA_TYPES}), and instantiated
|
|
22
|
+
# as a JSI instance of {#response_schema} if that is defined.
|
|
23
|
+
#
|
|
24
|
+
# @param mutable [Boolean] instantiate the response body object as mutable?
|
|
25
|
+
def body_object(mutable: false)
|
|
26
|
+
k = [:body_object, mutable]
|
|
27
|
+
@memos.fetch(k) { @memos[k] = compute_body_object(mutable: mutable) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private def compute_body_object(mutable: false)
|
|
16
31
|
if json?
|
|
17
32
|
if body.empty?
|
|
18
33
|
# an empty body isn't valid json, of course, but we'll just return nil for it.
|
|
19
34
|
body_object = nil
|
|
20
35
|
else
|
|
21
36
|
begin
|
|
22
|
-
body_object =
|
|
37
|
+
body_object = JSON.parse(body, freeze: !mutable)
|
|
23
38
|
#rescue ::JSON::ParserError
|
|
24
39
|
# TODO
|
|
25
40
|
end
|
|
26
41
|
end
|
|
27
42
|
|
|
28
|
-
|
|
29
|
-
body_object = response_schema.new_jsi(body_object, mutable: true)
|
|
30
|
-
end
|
|
43
|
+
# NOTE: the supported media types above should correspond to Response::SUPPORTED_MEDIA_TYPES
|
|
31
44
|
|
|
32
|
-
body_object
|
|
33
45
|
elsif content_type && content_type.type_text? && content_type.subtype?('plain')
|
|
34
|
-
body
|
|
46
|
+
body_object = body
|
|
35
47
|
else
|
|
36
48
|
# we will return the body if we do not have a supported parsing. for now.
|
|
37
|
-
body
|
|
49
|
+
body_object = body
|
|
38
50
|
end
|
|
51
|
+
|
|
52
|
+
if response_schema && (body_object.respond_to?(:to_hash) || body_object.respond_to?(:to_ary))
|
|
53
|
+
body_object = response_schema.new_jsi(body_object, mutable: mutable)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
body_object
|
|
39
57
|
end
|
|
40
58
|
end
|
|
41
59
|
end
|
data/lib/scorpio/ur.rb
CHANGED
|
@@ -12,9 +12,13 @@ module Scorpio
|
|
|
12
12
|
}
|
|
13
13
|
})
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Scorpio::Response.itself # invoke autoload. this is Scorpio::Ur.properties['response']
|
|
16
16
|
|
|
17
17
|
module Ur
|
|
18
|
+
Request = Ur.properties["request"]
|
|
19
|
+
|
|
20
|
+
# The Scorpio::Request from which this Ur was run
|
|
21
|
+
# @return [Scorpio::Request]
|
|
18
22
|
attr_accessor :scorpio_request
|
|
19
23
|
|
|
20
24
|
# raises a subclass of Scorpio::HTTPError if the response has an error status.
|
|
@@ -24,7 +28,7 @@ module Scorpio
|
|
|
24
28
|
# raises a generic HTTPError otherwise.
|
|
25
29
|
#
|
|
26
30
|
# @raise [Scorpio::HTTPError]
|
|
27
|
-
# @return [
|
|
31
|
+
# @return [nil]
|
|
28
32
|
def raise_on_http_error
|
|
29
33
|
error_class = Scorpio.error_classes_by_status[response.status]
|
|
30
34
|
error_class ||= if (400..499).include?(response.status)
|
|
@@ -35,7 +39,7 @@ module Scorpio
|
|
|
35
39
|
HTTPError
|
|
36
40
|
end
|
|
37
41
|
if error_class
|
|
38
|
-
message = "Error calling operation #{scorpio_request.operation.human_id}:\n
|
|
42
|
+
message = -"Error calling operation #{scorpio_request.operation.human_id}:\n#{response.body}"
|
|
39
43
|
raise(error_class.new(message).tap do |e|
|
|
40
44
|
e.ur = self
|
|
41
45
|
e.response_object = response.body_object
|
data/lib/scorpio/version.rb
CHANGED
data/lib/scorpio.rb
CHANGED
|
@@ -9,6 +9,7 @@ require "pathname"
|
|
|
9
9
|
require "pp"
|
|
10
10
|
|
|
11
11
|
module Scorpio
|
|
12
|
+
# @private
|
|
12
13
|
def self.root
|
|
13
14
|
@root ||= Pathname.new(__FILE__).dirname.parent.expand_path
|
|
14
15
|
end
|
|
@@ -17,6 +18,7 @@ end
|
|
|
17
18
|
module Scorpio
|
|
18
19
|
# generally put in code paths that are not expected to be valid control flow paths.
|
|
19
20
|
# rather a NotImplementedCorrectlyError. but that's too long.
|
|
21
|
+
# @private
|
|
20
22
|
class Bug < NotImplementedError
|
|
21
23
|
end
|
|
22
24
|
|
|
@@ -31,12 +33,9 @@ module Scorpio
|
|
|
31
33
|
# @param status [Integer] if specified, sets the HTTP status the class represents
|
|
32
34
|
# @return [Integer] the HTTP status the class represents
|
|
33
35
|
def self.status(status = nil)
|
|
34
|
-
if status
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
else
|
|
38
|
-
@status
|
|
39
|
-
end
|
|
36
|
+
return @status if !status
|
|
37
|
+
Scorpio.error_classes_by_status[status] = self
|
|
38
|
+
@status = status
|
|
40
39
|
end
|
|
41
40
|
attr_accessor :ur, :response_object
|
|
42
41
|
end
|
|
@@ -107,4 +106,11 @@ module Scorpio
|
|
|
107
106
|
autoload :ResourceBase, 'scorpio/resource_base'
|
|
108
107
|
autoload :Request, 'scorpio/request'
|
|
109
108
|
autoload :Response, 'scorpio/response'
|
|
109
|
+
|
|
110
|
+
class << self
|
|
111
|
+
# (see OpenAPI::Document.new_document)
|
|
112
|
+
def new_document(instance, **new_param)
|
|
113
|
+
OpenAPI::Document.new_document(instance, **new_param)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
110
116
|
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Request Configuration
|
|
2
|
+
|
|
3
|
+
Scorpio aims to offer flexibility in how applications can configure requests. Requests are initiated from an Operation object (e.g. {Scorpio::OpenAPI::Operation#run}), utilizing the Operation and the OpenAPI document that contains it for configuration. Many configurable attributes can be set with varying granularity, on the document (applying to all requests from all operations, unless overridden), on the operation (applying to all requests from that operation), or on the request itself.
|
|
4
|
+
|
|
5
|
+
Configurable attributes are defined on several modules: for a request, {Scorpio::Request::Configurables}; for an operation, {Scorpio::OpenAPI::Operation::Configurables}; and for a document {Scorpio::OpenAPI::Document::Configurables} and {Scorpio::OpenAPI::Operation::V3Methods::Configurables} or {Scorpio::OpenAPI::Operation::V2Methods::Configurables}.
|
|
6
|
+
|
|
7
|
+
{Scorpio::Request#initialize} and methods that instantiate a request (such as {Scorpio::OpenAPI::Operation#run}) take a keyword hash of configuration, which may include attributes defined on the Configurables module, or parameters defined by the operation (except where parameter names conflict with configurable attributes, or are ambiguous).
|
|
8
|
+
|
|
9
|
+
So for example, given this OpenAPI document:
|
|
10
|
+
|
|
11
|
+
```yaml
|
|
12
|
+
openapi: 3.1.0
|
|
13
|
+
info: {title: example, version: '1'}
|
|
14
|
+
servers:
|
|
15
|
+
- url: "https://{subdomain}.example.com/v1"
|
|
16
|
+
variables:
|
|
17
|
+
subdomain:
|
|
18
|
+
default: api
|
|
19
|
+
paths:
|
|
20
|
+
'/foos/{id}':
|
|
21
|
+
patch:
|
|
22
|
+
operationId: foos.patch
|
|
23
|
+
parameters:
|
|
24
|
+
- name: id
|
|
25
|
+
in: path
|
|
26
|
+
required: true
|
|
27
|
+
schema:
|
|
28
|
+
type: string
|
|
29
|
+
'/bars/{id}':
|
|
30
|
+
patch:
|
|
31
|
+
operationId: bars.patch
|
|
32
|
+
parameters:
|
|
33
|
+
- name: id
|
|
34
|
+
in: path
|
|
35
|
+
required: true
|
|
36
|
+
schema:
|
|
37
|
+
type: string
|
|
38
|
+
- name: id
|
|
39
|
+
in: query
|
|
40
|
+
schema:
|
|
41
|
+
type: string
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
then the following Ruby code demonstrates how configuration is inherited from document to operation to request and how parameters are applied.
|
|
45
|
+
|
|
46
|
+
Note that `build_request` takes the same parameters as `run` or `run_ur` would; it is used since we are not hitting a real API.
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
# instantiate OAD whose content is the same as the yaml above
|
|
50
|
+
oad = Scorpio::OpenAPI::Document.from_instance({"openapi"=>"3.1.0", "info"=>{"title"=>"example", "version"=>"1"}, "servers"=>[{"url"=>"https://{subdomain}.example.com/v1", "variables"=>{"subdomain"=>{"default"=>"api"}}}], "paths"=>{"/foos/{id}"=>{"patch"=>{"operationId"=>"foos.patch", "parameters"=>[{"name"=>"id", "in"=>"path", "required"=>true, "schema"=>{"type"=>"string"}}]}}, "/bars/{id}"=>{"patch"=>{"operationId"=>"bars.patch", "parameters"=>[{"name"=>"id", "in"=>"path", "required"=>true, "schema"=>{"type"=>"string"}}, {"name"=>"id", "in"=>"query", "schema"=>{"type"=>"string"}}]}}}})
|
|
51
|
+
|
|
52
|
+
# configure document level server subdomain
|
|
53
|
+
oad.server_variables = {subdomain: 'east'}
|
|
54
|
+
# configure operation level server subdomain
|
|
55
|
+
oad.operations['bars.patch'].server_variables = {subdomain: 'north'}
|
|
56
|
+
|
|
57
|
+
# uses document server_variables and sets id as request path param
|
|
58
|
+
oad.operations['foos.patch'].build_request(id: 'C7').url
|
|
59
|
+
# https://east.example.com/v1/foos/C7
|
|
60
|
+
|
|
61
|
+
# sets id as request path param and sets server_variables request config
|
|
62
|
+
oad.operations['foos.patch'].build_request(id: 'C7', server_variables: {subdomain: 'west'}).url
|
|
63
|
+
# https://west.example.com/v1/foos/C7
|
|
64
|
+
|
|
65
|
+
# uses operation server_variables. since `id` is a param name in both path and query, `id` alone
|
|
66
|
+
# cannot be used as configuration; it must be set on the appropriate params' configurations
|
|
67
|
+
oad.operations['bars.patch'].build_request(path_params: {id: 'C7'}, query_params: {id: 'D7'}).url
|
|
68
|
+
# https://north.example.com/v1/bars/C7?id=D7
|
|
69
|
+
```
|
data/pages/Security.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# API Security
|
|
2
|
+
|
|
3
|
+
Scorpio does not currently implement an interface for any particular API security mechanism, which an OpenAPI description might specify using an operation's Security Requirement and corresponding Security Scheme. Scorpio offers flexibility in how applications may authenticate using common mechanisms - for more general guidance see {file:Request_Configuration Request Configuration}.
|
|
4
|
+
|
|
5
|
+
### Authorization header
|
|
6
|
+
|
|
7
|
+
An authorization header can be set using the `authorization` configuration on an OpenAPI document, operation, or request:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# applies to every request (unless overridden per operation or request)
|
|
11
|
+
my_openapi_document.authorization = "Basic bm90RXRoYW46cDRzc3cwcmQ"
|
|
12
|
+
|
|
13
|
+
# or, applying to every request from my_operation (unless overridden)
|
|
14
|
+
my_operation.authorization = "Basic bm90RXRoYW46cDRzc3cwcmQ"
|
|
15
|
+
|
|
16
|
+
# or, per request:
|
|
17
|
+
my_operation.run(authorization: "Basic bm90RXRoYW46cDRzc3cwcmQ")
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Another header
|
|
21
|
+
|
|
22
|
+
Any other request headers can be set similarly:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
# document
|
|
26
|
+
my_openapi_document.request_headers = {"Api-Key" => "bm90RXRoYW46cDRzc3cwcmQ"}
|
|
27
|
+
|
|
28
|
+
# operation
|
|
29
|
+
my_operation.request_headers = {"Api-Key" => "bm90RXRoYW46cDRzc3cwcmQ"}
|
|
30
|
+
|
|
31
|
+
# request (note: just `headers` not `request_headers` here)
|
|
32
|
+
my_operation.run(headers: {"Api-Key" => "bm90RXRoYW46cDRzc3cwcmQ"})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Faraday request middleware
|
|
36
|
+
|
|
37
|
+
Faraday middleware can be used to set headers or other components of the request, by configuring `faraday_builder`. If authentication must be computed for each request (e.g. by including a signature of the request), configured Faraday middleware is invoked for each request. Faraday middleware libraries exist for common authentication mechanisms - see [awesome-faraday](https://github.com/lostisland/awesome-faraday).
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
# document
|
|
41
|
+
my_openapi_document.faraday_builder = proc do |faraday_connection|
|
|
42
|
+
faraday_connection.request(:authorization, :basic, 'notEthan', 'p4ssw0rd')
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# operation
|
|
46
|
+
my_operation.faraday_builder = proc { |c| c.request(:authorization, :basic, 'notEthan', 'p4ssw0rd') }
|
|
47
|
+
|
|
48
|
+
# request
|
|
49
|
+
my_operation.run(faraday_builder: proc { |c| c.request(:authorization, :basic, 'notEthan', 'p4ssw0rd') })
|
|
50
|
+
```
|
data/scorpio.gemspec
CHANGED
|
@@ -6,10 +6,10 @@ Gem::Specification.new do |spec|
|
|
|
6
6
|
spec.authors = ["Ethan"]
|
|
7
7
|
spec.email = ["ethan@unth.net"]
|
|
8
8
|
|
|
9
|
-
spec.summary =
|
|
10
|
-
spec.description =
|
|
9
|
+
spec.summary = "Scorpio web API client"
|
|
10
|
+
spec.description = "Scorpio offers a client interface to any web API described by an OpenAPI description, following ORM conventions for RESTful resources."
|
|
11
11
|
spec.homepage = "https://github.com/notEthan/scorpio"
|
|
12
|
-
spec.license = "AGPL-3.0"
|
|
12
|
+
spec.license = "AGPL-3.0-only"
|
|
13
13
|
|
|
14
14
|
spec.files = [
|
|
15
15
|
'LICENSE.md',
|
|
@@ -19,12 +19,13 @@ Gem::Specification.new do |spec|
|
|
|
19
19
|
'scorpio.gemspec',
|
|
20
20
|
*Dir['lib/**/*'],
|
|
21
21
|
*Dir['documents/**/*'],
|
|
22
|
+
*Dir['pages/**/*'],
|
|
22
23
|
].reject { |f| File.lstat(f).ftype == 'directory' }
|
|
23
24
|
|
|
24
25
|
spec.require_paths = ["lib"]
|
|
25
26
|
|
|
26
|
-
spec.add_dependency
|
|
27
|
+
spec.add_dependency("jsi", "~> 0.9")
|
|
27
28
|
spec.add_dependency "ur", "~> 0.2.5"
|
|
28
|
-
spec.add_dependency "faraday"
|
|
29
|
+
spec.add_dependency "faraday"
|
|
29
30
|
spec.add_dependency "addressable", '~> 2.3'
|
|
30
31
|
end
|