scorpio 0.3.1 → 0.4.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 +1 -0
- data/CHANGELOG.md +7 -0
- data/README.md +95 -2
- data/documents/github.com/OAI/OpenAPI-Specification/blob/oas3-schema/schemas/v3.0/schema.yaml +1495 -0
- data/lib/scorpio.rb +18 -0
- data/lib/scorpio/openapi.rb +63 -62
- data/lib/scorpio/openapi/document.rb +17 -0
- data/lib/scorpio/openapi/operation.rb +87 -5
- data/lib/scorpio/openapi/operations_scope.rb +9 -3
- data/lib/scorpio/openapi/v3/server.rb +10 -0
- data/lib/scorpio/request.rb +148 -12
- data/lib/scorpio/resource_base.rb +15 -12
- data/lib/scorpio/response.rb +4 -0
- data/lib/scorpio/ur.rb +12 -0
- data/lib/scorpio/version.rb +1 -1
- data/scorpio.gemspec +1 -0
- metadata +18 -4
- data/documents/openapis.org/v3/schema.json +0 -1239
- data/documents/openapis.org/v3/schema.yml +0 -794
data/lib/scorpio.rb
CHANGED
@@ -19,8 +19,18 @@ module Scorpio
|
|
19
19
|
end
|
20
20
|
|
21
21
|
proc { |v| define_singleton_method(:error_classes_by_status) { v } }.call({})
|
22
|
+
# Scorpio::Error encompasses certain Scorpio-defined errors encountered in using Scorpio.
|
23
|
+
# at the moment this only includes HTTPError[^1], but will likely cover errors in schemas and
|
24
|
+
# other things in the future.
|
25
|
+
#
|
26
|
+
# [^1]: unless I have, since writing this, implemented other things but forgotten to update this
|
27
|
+
# comment, which does seem likely enough.
|
22
28
|
class Error < StandardError; end
|
23
29
|
class HTTPError < Error
|
30
|
+
# for HTTPError subclasses representing a single status, sets and/or returns the represented status.
|
31
|
+
#
|
32
|
+
# @param status [Integer] if specified, sets the HTTP status the class represents
|
33
|
+
# @return [Integer] the HTTP status the class represents
|
24
34
|
def self.status(status = nil)
|
25
35
|
if status
|
26
36
|
@status = status
|
@@ -35,7 +45,9 @@ module Scorpio
|
|
35
45
|
# be referred to like Scorpio::BadRequest400Error. this is just to avoid clutter in the Scorpio
|
36
46
|
# namespace in yardoc.
|
37
47
|
module HTTPErrors
|
48
|
+
# an HTTP response with a status of 400-499
|
38
49
|
class ClientError < HTTPError; end
|
50
|
+
# an HTTP response with a status of 500-599
|
39
51
|
class ServerError < HTTPError; end
|
40
52
|
|
41
53
|
class BadRequest400Error < ClientError; status(400); end
|
@@ -82,6 +94,12 @@ module Scorpio
|
|
82
94
|
include HTTPErrors
|
83
95
|
error_classes_by_status.freeze
|
84
96
|
|
97
|
+
class ConfigError < Error
|
98
|
+
attr_accessor :name
|
99
|
+
end
|
100
|
+
class AmbiguousParameter < ConfigError
|
101
|
+
end
|
102
|
+
|
85
103
|
autoload :Google, 'scorpio/google_api_document'
|
86
104
|
autoload :OpenAPI, 'scorpio/openapi'
|
87
105
|
autoload :Ur, 'scorpio/ur'
|
data/lib/scorpio/openapi.rb
CHANGED
@@ -5,7 +5,7 @@ module Scorpio
|
|
5
5
|
autoload :OperationsScope, 'scorpio/openapi/operations_scope'
|
6
6
|
|
7
7
|
module V3
|
8
|
-
openapi_schema = JSI::Schema.new(::
|
8
|
+
openapi_schema = JSI::Schema.new(::YAML.load_file(Scorpio.root.join('documents/github.com/OAI/OpenAPI-Specification/blob/oas3-schema/schemas/v3.0/schema.yaml')))
|
9
9
|
openapi_class = proc do |*key|
|
10
10
|
JSI.class_for_schema(key.inject(openapi_schema, &:[]))
|
11
11
|
end
|
@@ -13,66 +13,67 @@ module Scorpio
|
|
13
13
|
Document = openapi_class.call()
|
14
14
|
|
15
15
|
# naming these is not strictly necessary, but is nice to have.
|
16
|
-
# generated: puts
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
16
|
+
# generated: `puts JSI::Schema.new(::YAML.load_file(Scorpio.root.join('documents/github.com/OAI/OpenAPI-Specification/blob/oas3-schema/schemas/v3.0/schema.yaml')))['definitions'].select { |k,v| ['object', nil].include?(v['type']) }.keys.map { |k| "#{k[0].upcase}#{k[1..-1]} = openapi_class.call('definitions', '#{k}')" }`
|
17
|
+
Reference = openapi_class.call('definitions', 'Reference')
|
18
|
+
Info = openapi_class.call('definitions', 'Info')
|
19
|
+
Contact = openapi_class.call('definitions', 'Contact')
|
20
|
+
License = openapi_class.call('definitions', 'License')
|
21
|
+
Server = openapi_class.call('definitions', 'Server')
|
22
|
+
ServerVariable = openapi_class.call('definitions', 'ServerVariable')
|
23
|
+
Components = openapi_class.call('definitions', 'Components')
|
24
|
+
Schema = openapi_class.call('definitions', 'Schema')
|
25
|
+
Discriminator = openapi_class.call('definitions', 'Discriminator')
|
26
|
+
XML = openapi_class.call('definitions', 'XML')
|
27
|
+
Response = openapi_class.call('definitions', 'Response')
|
28
|
+
MediaType = openapi_class.call('definitions', 'MediaType')
|
29
|
+
MediaTypeWithExample = openapi_class.call('definitions', 'MediaTypeWithExample')
|
30
|
+
MediaTypeWithExamples = openapi_class.call('definitions', 'MediaTypeWithExamples')
|
31
|
+
Example = openapi_class.call('definitions', 'Example')
|
32
|
+
Header = openapi_class.call('definitions', 'Header')
|
33
|
+
HeaderWithSchema = openapi_class.call('definitions', 'HeaderWithSchema')
|
34
|
+
HeaderWithSchemaWithExample = openapi_class.call('definitions', 'HeaderWithSchemaWithExample')
|
35
|
+
HeaderWithSchemaWithExamples = openapi_class.call('definitions', 'HeaderWithSchemaWithExamples')
|
36
|
+
HeaderWithContent = openapi_class.call('definitions', 'HeaderWithContent')
|
37
|
+
Paths = openapi_class.call('definitions', 'Paths')
|
38
|
+
PathItem = openapi_class.call('definitions', 'PathItem')
|
39
|
+
Operation = openapi_class.call('definitions', 'Operation')
|
40
|
+
Responses = openapi_class.call('definitions', 'Responses')
|
41
|
+
SecurityRequirement = openapi_class.call('definitions', 'SecurityRequirement')
|
42
|
+
Tag = openapi_class.call('definitions', 'Tag')
|
43
|
+
ExternalDocumentation = openapi_class.call('definitions', 'ExternalDocumentation')
|
44
|
+
Parameter = openapi_class.call('definitions', 'Parameter')
|
45
|
+
ParameterWithSchema = openapi_class.call('definitions', 'ParameterWithSchema')
|
46
|
+
ParameterWithSchemaWithExample = openapi_class.call('definitions', 'ParameterWithSchemaWithExample')
|
47
|
+
ParameterWithSchemaWithExampleInPath = openapi_class.call('definitions', 'ParameterWithSchemaWithExampleInPath')
|
48
|
+
ParameterWithSchemaWithExampleInQuery = openapi_class.call('definitions', 'ParameterWithSchemaWithExampleInQuery')
|
49
|
+
ParameterWithSchemaWithExampleInHeader = openapi_class.call('definitions', 'ParameterWithSchemaWithExampleInHeader')
|
50
|
+
ParameterWithSchemaWithExampleInCookie = openapi_class.call('definitions', 'ParameterWithSchemaWithExampleInCookie')
|
51
|
+
ParameterWithSchemaWithExamples = openapi_class.call('definitions', 'ParameterWithSchemaWithExamples')
|
52
|
+
ParameterWithSchemaWithExamplesInPath = openapi_class.call('definitions', 'ParameterWithSchemaWithExamplesInPath')
|
53
|
+
ParameterWithSchemaWithExamplesInQuery = openapi_class.call('definitions', 'ParameterWithSchemaWithExamplesInQuery')
|
54
|
+
ParameterWithSchemaWithExamplesInHeader = openapi_class.call('definitions', 'ParameterWithSchemaWithExamplesInHeader')
|
55
|
+
ParameterWithSchemaWithExamplesInCookie = openapi_class.call('definitions', 'ParameterWithSchemaWithExamplesInCookie')
|
56
|
+
ParameterWithContent = openapi_class.call('definitions', 'ParameterWithContent')
|
57
|
+
ParameterWithContentInPath = openapi_class.call('definitions', 'ParameterWithContentInPath')
|
58
|
+
ParameterWithContentNotInPath = openapi_class.call('definitions', 'ParameterWithContentNotInPath')
|
59
|
+
RequestBody = openapi_class.call('definitions', 'RequestBody')
|
60
|
+
SecurityScheme = openapi_class.call('definitions', 'SecurityScheme')
|
61
|
+
APIKeySecurityScheme = openapi_class.call('definitions', 'APIKeySecurityScheme')
|
62
|
+
HTTPSecurityScheme = openapi_class.call('definitions', 'HTTPSecurityScheme')
|
63
|
+
NonBearerHTTPSecurityScheme = openapi_class.call('definitions', 'NonBearerHTTPSecurityScheme')
|
64
|
+
BearerHTTPSecurityScheme = openapi_class.call('definitions', 'BearerHTTPSecurityScheme')
|
65
|
+
OAuth2SecurityScheme = openapi_class.call('definitions', 'OAuth2SecurityScheme')
|
66
|
+
OpenIdConnectSecurityScheme = openapi_class.call('definitions', 'OpenIdConnectSecurityScheme')
|
67
|
+
OAuthFlows = openapi_class.call('definitions', 'OAuthFlows')
|
68
|
+
ImplicitOAuthFlow = openapi_class.call('definitions', 'ImplicitOAuthFlow')
|
69
|
+
PasswordOAuthFlow = openapi_class.call('definitions', 'PasswordOAuthFlow')
|
70
|
+
ClientCredentialsFlow = openapi_class.call('definitions', 'ClientCredentialsFlow')
|
71
|
+
AuthorizationCodeOAuthFlow = openapi_class.call('definitions', 'AuthorizationCodeOAuthFlow')
|
72
|
+
Link = openapi_class.call('definitions', 'Link')
|
73
|
+
LinkWithOperationRef = openapi_class.call('definitions', 'LinkWithOperationRef')
|
74
|
+
LinkWithOperationId = openapi_class.call('definitions', 'LinkWithOperationId')
|
75
|
+
Callback = openapi_class.call('definitions', 'Callback')
|
76
|
+
Encoding = openapi_class.call('definitions', 'Encoding')
|
76
77
|
end
|
77
78
|
module V2
|
78
79
|
openapi_schema = JSI::Schema.new(::JSON.parse(Scorpio.root.join('documents/swagger.io/v2/schema.json').read))
|
@@ -83,7 +84,7 @@ module Scorpio
|
|
83
84
|
Document = openapi_class.call()
|
84
85
|
|
85
86
|
# naming these is not strictly necessary, but is nice to have.
|
86
|
-
# generated: puts Scorpio::OpenAPI::V2::Document.schema['definitions'].select { |k,v| ['object', nil].include?(v['type']) }.keys.map { |k| "#{k[0].upcase}#{k[1..-1]} = openapi_class.call('definitions', '#{k}')" }
|
87
|
+
# generated: `puts Scorpio::OpenAPI::V2::Document.schema['definitions'].select { |k,v| ['object', nil].include?(v['type']) }.keys.map { |k| "#{k[0].upcase}#{k[1..-1]} = openapi_class.call('definitions', '#{k}')" }`
|
87
88
|
Info = openapi_class.call('definitions', 'info')
|
88
89
|
Contact = openapi_class.call('definitions', 'contact')
|
89
90
|
License = openapi_class.call('definitions', 'license')
|
@@ -1,7 +1,16 @@
|
|
1
1
|
module Scorpio
|
2
2
|
module OpenAPI
|
3
|
+
# A document that defines or describes an API.
|
4
|
+
# An OpenAPI definition uses and conforms to the OpenAPI Specification.
|
5
|
+
#
|
6
|
+
# Scorpio::OpenAPI::Document is a module common to V2 and V3 documents.
|
3
7
|
module Document
|
4
8
|
class << self
|
9
|
+
# takes a document, generally a Hash, and returns a Scorpio OpenAPI Document
|
10
|
+
# instantiating it.
|
11
|
+
#
|
12
|
+
# @param instance [#to_hash] the document to represent as a Scorpio OpenAPI Document
|
13
|
+
# @return [Scorpio::OpenAPI::V2::Document, Scorpio::OpenAPI::V3::Document]
|
5
14
|
def from_instance(instance)
|
6
15
|
if instance.is_a?(Hash)
|
7
16
|
instance = JSI::JSON::Node.new_doc(instance)
|
@@ -76,6 +85,10 @@ module Scorpio
|
|
76
85
|
|
77
86
|
module V3
|
78
87
|
raise(Bug) unless const_defined?(:Document)
|
88
|
+
|
89
|
+
# A document that defines or describes an API conforming to the OpenAPI Specification v3.
|
90
|
+
#
|
91
|
+
# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#oasObject
|
79
92
|
class Document
|
80
93
|
module Configurables
|
81
94
|
def scheme
|
@@ -115,6 +128,10 @@ module Scorpio
|
|
115
128
|
|
116
129
|
module V2
|
117
130
|
raise(Bug) unless const_defined?(:Document)
|
131
|
+
|
132
|
+
# A document that defines or describes an API conforming to the OpenAPI Specification v2 (aka Swagger).
|
133
|
+
#
|
134
|
+
# The root document is known as the Swagger Object.
|
118
135
|
class Document
|
119
136
|
module Configurables
|
120
137
|
attr_writer :scheme
|
@@ -1,5 +1,8 @@
|
|
1
1
|
module Scorpio
|
2
2
|
module OpenAPI
|
3
|
+
# An OpenAPI operation
|
4
|
+
#
|
5
|
+
# Scorpio::OpenAPI::Operation is a module common to V2 and V3 operations.
|
3
6
|
module Operation
|
4
7
|
module Configurables
|
5
8
|
attr_writer :base_url
|
@@ -40,13 +43,24 @@ module Scorpio
|
|
40
43
|
end
|
41
44
|
include Configurables
|
42
45
|
|
46
|
+
# @return [Boolean] v3?
|
47
|
+
def v3?
|
48
|
+
is_a?(V3::Operation)
|
49
|
+
end
|
50
|
+
|
51
|
+
# @return [Boolean] v2?
|
52
|
+
def v2?
|
53
|
+
is_a?(V2::Operation)
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [Scorpio::OpenAPI::Document] the document whence this operation came
|
43
57
|
def openapi_document
|
44
58
|
parents.detect { |p| p.is_a?(Scorpio::OpenAPI::Document) }
|
45
59
|
end
|
46
60
|
|
47
|
-
def
|
48
|
-
return @
|
49
|
-
@
|
61
|
+
def path_template_str
|
62
|
+
return @path_template_str if instance_variable_defined?(:@path_template_str)
|
63
|
+
@path_template_str = begin
|
50
64
|
parent_is_pathitem = parent.is_a?(Scorpio::OpenAPI::V2::PathItem) || parent.is_a?(Scorpio::OpenAPI::V3::PathItem)
|
51
65
|
parent_parent_is_paths = parent.parent.is_a?(Scorpio::OpenAPI::V2::Paths) || parent.parent.is_a?(Scorpio::OpenAPI::V3::Paths)
|
52
66
|
if parent_is_pathitem && parent_parent_is_paths
|
@@ -55,6 +69,24 @@ module Scorpio
|
|
55
69
|
end
|
56
70
|
end
|
57
71
|
|
72
|
+
# @return [Addressable::Template] the path as an Addressable::Template
|
73
|
+
def path_template
|
74
|
+
return @path_template if instance_variable_defined?(:@path_template)
|
75
|
+
@path_template = Addressable::Template.new(path_template_str)
|
76
|
+
end
|
77
|
+
|
78
|
+
# @param base_url [#to_str] the base URL to which the path template is appended
|
79
|
+
# @return [Addressable::Template] the URI template, consisting of the base_url
|
80
|
+
# concatenated with the path template
|
81
|
+
def uri_template(base_url: self.base_url)
|
82
|
+
unless base_url
|
83
|
+
raise(ArgumentError, "no base_url has been specified for operation #{self}")
|
84
|
+
end
|
85
|
+
# we do not use Addressable::URI#join as the paths should just be concatenated, not resolved.
|
86
|
+
# we use File.join just to deal with consecutive slashes.
|
87
|
+
Addressable::Template.new(File.join(base_url, path_template_str))
|
88
|
+
end
|
89
|
+
|
58
90
|
def http_method
|
59
91
|
return @http_method if instance_variable_defined?(:@http_method)
|
60
92
|
@http_method = begin
|
@@ -65,6 +97,49 @@ module Scorpio
|
|
65
97
|
end
|
66
98
|
end
|
67
99
|
|
100
|
+
# this method is not intended to be API-stable at the moment.
|
101
|
+
#
|
102
|
+
# @return [#to_ary<#to_h>] the parameters specified for this operation, plus any others
|
103
|
+
# scorpio considers to be parameters
|
104
|
+
def inferred_parameters
|
105
|
+
parameters = self.parameters ? self.parameters.to_a.dup : []
|
106
|
+
path_template.variables.each do |var|
|
107
|
+
unless parameters.any? { |p| p['in'] == 'path' && p['name'] == var }
|
108
|
+
# we could instantiate this as a V2::Parameter or a V3::Parameter
|
109
|
+
# or a ParameterWithContentInPath or whatever. but I can't be bothered.
|
110
|
+
parameters << {
|
111
|
+
'name' => var,
|
112
|
+
'in' => 'path',
|
113
|
+
'required' => true,
|
114
|
+
'type' => 'string',
|
115
|
+
}
|
116
|
+
end
|
117
|
+
end
|
118
|
+
parameters
|
119
|
+
end
|
120
|
+
|
121
|
+
# @return [Module] a module with accessor methods for unambiguously named parameters of this operation.
|
122
|
+
def request_accessor_module
|
123
|
+
return @request_accessor_module if instance_variable_defined?(:@request_accessor_module)
|
124
|
+
@request_accessor_module = begin
|
125
|
+
params_by_name = inferred_parameters.group_by { |p| p['name'] }
|
126
|
+
Module.new do
|
127
|
+
instance_method_modules = [Request, Request::Configurables]
|
128
|
+
instance_method_names = instance_method_modules.map do |mod|
|
129
|
+
(mod.instance_methods + mod.private_instance_methods).map(&:to_s)
|
130
|
+
end.inject(Set.new, &:|)
|
131
|
+
params_by_name.each do |name, params|
|
132
|
+
next if instance_method_names.include?(name)
|
133
|
+
if params.size == 1
|
134
|
+
param = params.first
|
135
|
+
define_method("#{name}=") { |value| set_param_from(param['in'], param['name'], value) }
|
136
|
+
define_method(name) { get_param_from(param['in'], param['name']) }
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
68
143
|
def build_request(*a, &b)
|
69
144
|
request = Scorpio::Request.new(self, *a, &b)
|
70
145
|
end
|
@@ -80,9 +155,14 @@ module Scorpio
|
|
80
155
|
|
81
156
|
module V3
|
82
157
|
raise(Bug) unless const_defined?(:Operation)
|
158
|
+
|
159
|
+
# Describes a single API operation on a path.
|
160
|
+
#
|
161
|
+
# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operationObject
|
83
162
|
class Operation
|
84
163
|
module Configurables
|
85
164
|
def scheme
|
165
|
+
# not applicable; for OpenAPI v3, scheme is specified by servers.
|
86
166
|
nil
|
87
167
|
end
|
88
168
|
|
@@ -177,18 +257,20 @@ module Scorpio
|
|
177
257
|
elsif body_parameters.size == 1
|
178
258
|
body_parameters.first
|
179
259
|
else
|
180
|
-
raise(Bug) # TODO BLAME
|
260
|
+
raise(Bug, "multiple body parameters on operation #{operation.pretty_inspect.chomp}") # TODO BLAME
|
181
261
|
end
|
182
262
|
end
|
183
263
|
|
184
264
|
def request_schema(media_type: nil)
|
185
265
|
if body_parameter && body_parameter['schema']
|
186
266
|
JSI::Schema.new(body_parameter['schema'])
|
267
|
+
else
|
268
|
+
nil
|
187
269
|
end
|
188
270
|
end
|
189
271
|
|
190
272
|
def request_schemas
|
191
|
-
[request_schema]
|
273
|
+
request_schema ? [request_schema] : []
|
192
274
|
end
|
193
275
|
|
194
276
|
# @return JSI::Schema
|
@@ -1,13 +1,17 @@
|
|
1
1
|
module Scorpio
|
2
2
|
module OpenAPI
|
3
|
+
# OperationsScope acts as an Enumerable of the Operations for an openapi_document,
|
4
|
+
# and offers subscripting by operationId.
|
3
5
|
class OperationsScope
|
4
6
|
include JSI::Memoize
|
5
7
|
|
8
|
+
# @param openapi_document [Scorpio::OpenAPI::Document]
|
6
9
|
def initialize(openapi_document)
|
7
10
|
@openapi_document = openapi_document
|
8
11
|
end
|
9
12
|
attr_reader :openapi_document
|
10
13
|
|
14
|
+
# @yield [Scorpio::OpenAPI::Operation]
|
11
15
|
def each
|
12
16
|
openapi_document.paths.each do |path, path_item|
|
13
17
|
path_item.each do |http_method, operation|
|
@@ -19,9 +23,11 @@ module Scorpio
|
|
19
23
|
end
|
20
24
|
include Enumerable
|
21
25
|
|
22
|
-
|
23
|
-
|
24
|
-
|
26
|
+
# @param operationId
|
27
|
+
# @return [Scorpio::OpenAPI::Operation] the operation with the given operationId
|
28
|
+
def [](operationId)
|
29
|
+
memoize(:[], operationId) do |operationId_|
|
30
|
+
detect { |operation| operation.operationId == operationId_ }
|
25
31
|
end
|
26
32
|
end
|
27
33
|
end
|
@@ -2,7 +2,17 @@ module Scorpio
|
|
2
2
|
module OpenAPI
|
3
3
|
module V3
|
4
4
|
raise(Bug) unless const_defined?(:Server)
|
5
|
+
|
6
|
+
# An object representing a Server.
|
7
|
+
#
|
8
|
+
# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#serverObject
|
5
9
|
class Server
|
10
|
+
# expands this server's #url using the given_server_variables. any variables
|
11
|
+
# that are in the url but not in the given server variables are filled in
|
12
|
+
# using the default value for the variable.
|
13
|
+
#
|
14
|
+
# @param given_server_variables [Hash<String, String>]
|
15
|
+
# @return [Addressable::URI] the expanded url
|
6
16
|
def expanded_url(given_server_variables)
|
7
17
|
if variables
|
8
18
|
server_variables = (given_server_variables.keys | variables.keys).map do |key|
|
data/lib/scorpio/request.rb
CHANGED
@@ -110,45 +110,72 @@ module Scorpio
|
|
110
110
|
end
|
111
111
|
include Configurables
|
112
112
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
113
|
+
# @param operation [Scorpio::OpenAPI::Operation]
|
114
|
+
# @param configuration [#to_hash] a hash keyed with configurable attributes for
|
115
|
+
# the request - instance methods of Scorpio::Request::Configurables, whose values
|
116
|
+
# will be assigned for those attributes.
|
117
|
+
def initialize(operation, configuration = {}, &b)
|
118
|
+
@operation = operation
|
119
|
+
|
120
|
+
configuration = JSI.stringify_symbol_keys(configuration)
|
121
|
+
params_set = Set.new # the set of params that have been set
|
122
|
+
# do the Configurables first
|
123
|
+
configuration.each do |name, value|
|
124
|
+
if Configurables.public_method_defined?("#{name}=")
|
125
|
+
Configurables.instance_method("#{name}=").bind(self).call(value)
|
126
|
+
params_set << name
|
127
|
+
end
|
128
|
+
end
|
129
|
+
# then do other top-level params
|
130
|
+
configuration.reject { |name, _| params_set.include?(name) }.each do |name, value|
|
131
|
+
params = operation.inferred_parameters.select { |p| p['name'] == name }
|
132
|
+
if params.size == 1
|
133
|
+
set_param_from(params.first['in'], name, value)
|
134
|
+
elsif params.size == 0
|
135
|
+
raise(ArgumentError, "unrecognized configuration value passed: #{name.inspect}")
|
118
136
|
else
|
119
|
-
raise(
|
137
|
+
raise(AmbiguousParameter.new("There are multiple parameters named #{name.inspect} - cannot use it as a configuration key").tap { |e| e.name = name })
|
120
138
|
end
|
121
139
|
end
|
122
140
|
|
123
|
-
|
141
|
+
extend operation.request_accessor_module
|
142
|
+
|
124
143
|
if block_given?
|
125
144
|
yield self
|
126
145
|
end
|
127
146
|
end
|
128
147
|
|
148
|
+
# @return [Scorpio::OpenAPI::Operation]
|
129
149
|
attr_reader :operation
|
130
150
|
|
151
|
+
# @return [Scorpio::OpenAPI::Document]
|
131
152
|
def openapi_document
|
132
153
|
operation.openapi_document
|
133
154
|
end
|
134
155
|
|
156
|
+
# @return [Symbol] the http method for this request - :get, :post, etc.
|
135
157
|
def http_method
|
136
158
|
operation.http_method.downcase.to_sym
|
137
159
|
end
|
138
160
|
|
161
|
+
# @return [Addressable::Template] the template for the request's path, to be expanded
|
162
|
+
# with path_params and appended to the request's base_url
|
139
163
|
def path_template
|
140
|
-
|
164
|
+
operation.path_template
|
141
165
|
end
|
142
166
|
|
167
|
+
# @return [Addressable::URI] an Addressable::URI containing only the path to append to
|
168
|
+
# the base_url for this request
|
143
169
|
def path
|
170
|
+
path_params = JSI.stringify_symbol_keys(self.path_params)
|
144
171
|
missing_variables = path_template.variables - path_params.keys
|
145
172
|
if missing_variables.any?
|
146
|
-
raise(ArgumentError, "path #{operation.
|
173
|
+
raise(ArgumentError, "path #{operation.path_template_str} for operation #{operation.operationId} requires path_params " +
|
147
174
|
"which were missing: #{missing_variables.inspect}")
|
148
175
|
end
|
149
176
|
empty_variables = path_template.variables.select { |v| path_params[v].to_s.empty? }
|
150
177
|
if empty_variables.any?
|
151
|
-
raise(ArgumentError, "path #{operation.
|
178
|
+
raise(ArgumentError, "path #{operation.path_template_str} for operation #{operation.operationId} requires path_params " +
|
152
179
|
"which were empty: #{empty_variables.inspect}")
|
153
180
|
end
|
154
181
|
|
@@ -159,20 +186,22 @@ module Scorpio
|
|
159
186
|
end
|
160
187
|
end
|
161
188
|
|
189
|
+
# @return [Addressable::URI] the full URL for this request
|
162
190
|
def url
|
163
191
|
unless base_url
|
164
192
|
raise(ArgumentError, "no base_url has been specified for request")
|
165
193
|
end
|
166
194
|
# we do not use Addressable::URI#join as the paths should just be concatenated, not resolved.
|
167
195
|
# we use File.join just to deal with consecutive slashes.
|
168
|
-
|
169
|
-
url = Addressable::URI.parse(url)
|
196
|
+
Addressable::URI.parse(File.join(base_url, path))
|
170
197
|
end
|
171
198
|
|
199
|
+
# @return [::Ur::ContentTypeAttrs] content type attributes for this request's Content-Type
|
172
200
|
def content_type_attrs
|
173
201
|
Ur::ContentTypeAttrs.new(content_type)
|
174
202
|
end
|
175
203
|
|
204
|
+
# @return [String] the value of the request Content-Type header
|
176
205
|
def content_type_header
|
177
206
|
headers.each do |k, v|
|
178
207
|
return v if k =~ /\Acontent[-_]type\z/i
|
@@ -180,18 +209,27 @@ module Scorpio
|
|
180
209
|
nil
|
181
210
|
end
|
182
211
|
|
212
|
+
# @return [String] Content-Type for this request, taken from request headers if
|
213
|
+
# present, or the request media_type.
|
183
214
|
def content_type
|
184
215
|
content_type_header || media_type
|
185
216
|
end
|
186
217
|
|
218
|
+
# @return [::JSI::Schema]
|
187
219
|
def request_schema(media_type: self.media_type)
|
188
220
|
operation.request_schema(media_type: media_type)
|
189
221
|
end
|
190
222
|
|
223
|
+
# @return [Class subclassing JSI::Base]
|
191
224
|
def request_schema_class(media_type: self.media_type)
|
192
225
|
JSI.class_for_schema(request_schema(media_type: media_type))
|
193
226
|
end
|
194
227
|
|
228
|
+
# builds a Faraday connection with this Request's faraday_builder and faraday_adapter.
|
229
|
+
# passes a given proc yield_ur to middleware to yield an Ur for requests made with the connection.
|
230
|
+
#
|
231
|
+
# @param yield_ur [Proc]
|
232
|
+
# @return [::Faraday::Connection]
|
195
233
|
def faraday_connection(yield_ur = nil)
|
196
234
|
Faraday.new do |faraday_connection|
|
197
235
|
faraday_builder.call(faraday_connection)
|
@@ -203,6 +241,81 @@ module Scorpio
|
|
203
241
|
end
|
204
242
|
end
|
205
243
|
|
244
|
+
# if there is only one parameter with the given name, of any sort, this will set it.
|
245
|
+
#
|
246
|
+
# @param name [String, Symbol] the 'name' property of one applicable parameter
|
247
|
+
# @param value [Object] the applicable parameter will be applied to the request with the given value.
|
248
|
+
# @return [Object] echoes the value param
|
249
|
+
# @raise [Scorpio::AmbiguousParameter] if more than one paramater has the given name
|
250
|
+
def set_param(name, value)
|
251
|
+
name = name.to_s if name.is_a?(Symbol)
|
252
|
+
params = operation.inferred_parameters.select { |p| p['name'] == name }
|
253
|
+
if params.size == 1
|
254
|
+
set_param_from(params.first['in'], name, value)
|
255
|
+
else
|
256
|
+
raise(AmbiguousParameter.new("There are multiple parameters named #{name}; cannot use #set_param").tap { |e| e.name = name })
|
257
|
+
end
|
258
|
+
value
|
259
|
+
end
|
260
|
+
|
261
|
+
# @param name [String, Symbol] the 'name' property of one applicable parameter
|
262
|
+
# @return [Object] the value of the named parameter on this request
|
263
|
+
# @raise [Scorpio::AmbiguousParameter] if more than one paramater has the given name
|
264
|
+
def get_param(name)
|
265
|
+
name = name.to_s if name.is_a?(Symbol)
|
266
|
+
params = operation.inferred_parameters.select { |p| p['name'] == name }
|
267
|
+
if params.size == 1
|
268
|
+
get_param_from(params.first['in'], name)
|
269
|
+
else
|
270
|
+
raise(AmbiguousParameter.new("There are multiple parameters named #{name}; cannot use #get_param").tap { |e| e.name = name })
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
# @param in [String, Symbol] one of 'path', 'query', 'header', or 'cookie' - where to apply the named value
|
275
|
+
# @param name [String, Symbol] the parameter name to apply the value to
|
276
|
+
# @param value [Object] the value
|
277
|
+
# @return [Object] echoes the value param
|
278
|
+
# @raise [ArgumentError] invalid 'in' parameter
|
279
|
+
# @raise [NotImplementedError] cookies aren't implemented
|
280
|
+
def set_param_from(param_in, name, value)
|
281
|
+
param_in = param_in.to_s if param_in.is_a?(Symbol)
|
282
|
+
name = name.to_s if name.is_a?(Symbol)
|
283
|
+
if param_in == 'path'
|
284
|
+
self.path_params = self.path_params.merge(name => value)
|
285
|
+
elsif param_in == 'query'
|
286
|
+
self.query_params = (self.query_params || {}).merge(name => value)
|
287
|
+
elsif param_in == 'header'
|
288
|
+
self.headers = self.headers.merge(name => value)
|
289
|
+
elsif param_in == 'cookie'
|
290
|
+
raise(NotImplementedError, "cookies not implemented: #{name.inspect} => #{value.inspect}")
|
291
|
+
else
|
292
|
+
raise(ArgumentError, "cannot set param from param_in = #{param_in.inspect} (name: #{name.pretty_inspect.chomp}, value: #{value.pretty_inspect.chomp})")
|
293
|
+
end
|
294
|
+
value
|
295
|
+
end
|
296
|
+
|
297
|
+
# @param in [String, Symbol] one of 'path', 'query', 'header', or 'cookie' - where to apply the named value
|
298
|
+
# @param name [String, Symbol] the parameter name
|
299
|
+
# @return [Object] the value of the named parameter on this request
|
300
|
+
# @raise [ArgumentError] invalid 'in' parameter
|
301
|
+
# @raise [NotImplementedError] cookies aren't implemented
|
302
|
+
def get_param_from(param_in, name)
|
303
|
+
if param_in == 'path'
|
304
|
+
path_params[name]
|
305
|
+
elsif param_in == 'query'
|
306
|
+
query_params ? query_params[name] : nil
|
307
|
+
elsif param_in == 'header'
|
308
|
+
headers[name]
|
309
|
+
elsif param_in == 'cookie'
|
310
|
+
raise(NotImplementedError, "cookies not implemented: #{name.inspect}")
|
311
|
+
else
|
312
|
+
raise(ArgumentError, "cannot get param from param_in = #{param_in.inspect} (name: #{name.pretty_inspect.chomp})")
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
# runs this request and returns the full representation of the request that was run and its response.
|
317
|
+
#
|
318
|
+
# @return [Scorpio::Ur]
|
206
319
|
def run_ur
|
207
320
|
headers = {}
|
208
321
|
if user_agent
|
@@ -220,10 +333,33 @@ module Scorpio
|
|
220
333
|
ur
|
221
334
|
end
|
222
335
|
|
336
|
+
# runs this request. returns the response body object - that is, the response body
|
337
|
+
# parsed according to an understood media type, and instantiated with the applicable
|
338
|
+
# response schema if one is specified. see Scorpio::Response#body_object for more detail.
|
339
|
+
#
|
340
|
+
# @raise [Scorpio::HTTPError] if the request returns a 4xx or 5xx status, the appropriate
|
341
|
+
# error is raised - see Scorpio::HTTPErrors
|
223
342
|
def run
|
224
343
|
ur = run_ur
|
225
344
|
ur.raise_on_http_error
|
226
345
|
ur.response.body_object
|
227
346
|
end
|
347
|
+
|
348
|
+
# todo make a proper iterator interface
|
349
|
+
# @param next_page [#call] a callable which will take a parameter `page_ur`, which is a {Scorpio::Ur},
|
350
|
+
# and must result in an Ur representing the next page, which will be yielded to the block.
|
351
|
+
# @yield [Scorpio::Ur] yields the first page, and each subsequent result of calls to `next_page` until
|
352
|
+
# that results in nil
|
353
|
+
# @return [void]
|
354
|
+
def each_page_ur(next_page: , raise_on_http_error: true)
|
355
|
+
return to_enum(__method__, next_page: next_page, raise_on_http_error: raise_on_http_error) unless block_given?
|
356
|
+
page_ur = run_ur
|
357
|
+
while page_ur
|
358
|
+
page_ur.raise_on_http_error if raise_on_http_error
|
359
|
+
yield page_ur
|
360
|
+
page_ur = next_page.call(page_ur)
|
361
|
+
end
|
362
|
+
nil
|
363
|
+
end
|
228
364
|
end
|
229
365
|
end
|