scorpio 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|