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.
@@ -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'
@@ -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(::JSON.parse(Scorpio.root.join('documents/openapis.org/v3/schema.json').read))
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 Scorpio::OpenAPI::V3::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}')" }
17
- Info = openapi_class.call('definitions', 'info')
18
- Contact = openapi_class.call('definitions', 'contact')
19
- License = openapi_class.call('definitions', 'license')
20
- Server = openapi_class.call('definitions', 'server')
21
- ServerVariable = openapi_class.call('definitions', 'serverVariable')
22
- Components = openapi_class.call('definitions', 'components')
23
- Paths = openapi_class.call('definitions', 'paths')
24
- PathItem = openapi_class.call('definitions', 'pathItem')
25
- Operation = openapi_class.call('definitions', 'operation')
26
- ExternalDocs = openapi_class.call('definitions', 'externalDocs')
27
- Parameter = openapi_class.call('definitions', 'parameter')
28
- RequestBody = openapi_class.call('definitions', 'requestBody')
29
- MediaType = openapi_class.call('definitions', 'mediaType')
30
- Encoding = openapi_class.call('definitions', 'encoding')
31
- Responses = openapi_class.call('definitions', 'responses')
32
- Response = openapi_class.call('definitions', 'response')
33
- Callback = openapi_class.call('definitions', 'callback')
34
- Example = openapi_class.call('definitions', 'example')
35
- Link = openapi_class.call('definitions', 'link')
36
- Header = openapi_class.call('definitions', 'header')
37
- Tag = openapi_class.call('definitions', 'tag')
38
- Examples = openapi_class.call('definitions', 'examples')
39
- Reference = openapi_class.call('definitions', 'reference')
40
- Schema = openapi_class.call('definitions', 'schema')
41
- Discriminator = openapi_class.call('definitions', 'discriminator')
42
- Xml = openapi_class.call('definitions', 'xml')
43
- SecurityScheme = openapi_class.call('definitions', 'securityScheme')
44
- OauthFlows = openapi_class.call('definitions', 'oauthFlows')
45
- OauthFlow = openapi_class.call('definitions', 'oauthFlow')
46
- SecurityRequirement = openapi_class.call('definitions', 'securityRequirement')
47
- AnyOrExpression = openapi_class.call('definitions', 'anyOrExpression')
48
- CallbackOrReference = openapi_class.call('definitions', 'callbackOrReference')
49
- ExampleOrReference = openapi_class.call('definitions', 'exampleOrReference')
50
- HeaderOrReference = openapi_class.call('definitions', 'headerOrReference')
51
- LinkOrReference = openapi_class.call('definitions', 'linkOrReference')
52
- ParameterOrReference = openapi_class.call('definitions', 'parameterOrReference')
53
- RequestBodyOrReference = openapi_class.call('definitions', 'requestBodyOrReference')
54
- ResponseOrReference = openapi_class.call('definitions', 'responseOrReference')
55
- SchemaOrReference = openapi_class.call('definitions', 'schemaOrReference')
56
- SecuritySchemeOrReference = openapi_class.call('definitions', 'securitySchemeOrReference')
57
- AnysOrExpressions = openapi_class.call('definitions', 'anysOrExpressions')
58
- CallbacksOrReferences = openapi_class.call('definitions', 'callbacksOrReferences')
59
- Encodings = openapi_class.call('definitions', 'encodings')
60
- ExamplesOrReferences = openapi_class.call('definitions', 'examplesOrReferences')
61
- HeadersOrReferences = openapi_class.call('definitions', 'headersOrReferences')
62
- LinksOrReferences = openapi_class.call('definitions', 'linksOrReferences')
63
- MediaTypes = openapi_class.call('definitions', 'mediaTypes')
64
- ParametersOrReferences = openapi_class.call('definitions', 'parametersOrReferences')
65
- RequestBodiesOrReferences = openapi_class.call('definitions', 'requestBodiesOrReferences')
66
- ResponsesOrReferences = openapi_class.call('definitions', 'responsesOrReferences')
67
- SchemasOrReferences = openapi_class.call('definitions', 'schemasOrReferences')
68
- SecuritySchemesOrReferences = openapi_class.call('definitions', 'securitySchemesOrReferences')
69
- ServerVariables = openapi_class.call('definitions', 'serverVariables')
70
- Strings = openapi_class.call('definitions', 'strings')
71
- Object = openapi_class.call('definitions', 'object')
72
- Any = openapi_class.call('definitions', 'any')
73
- Expression = openapi_class.call('definitions', 'expression')
74
- SpecificationExtension = openapi_class.call('definitions', 'specificationExtension')
75
- DefaultType = openapi_class.call('definitions', 'defaultType')
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 path
48
- return @path if instance_variable_defined?(:@path)
49
- @path = begin
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
- def [](operationId_)
23
- memoize(:[], operationId_) do |operationId|
24
- detect { |operation| operation.operationId == operationId }
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|
@@ -110,45 +110,72 @@ module Scorpio
110
110
  end
111
111
  include Configurables
112
112
 
113
- def initialize(operation, **configuration, &b)
114
- configuration.each do |k, v|
115
- settername = "#{k}="
116
- if Configurables.public_method_defined?(settername)
117
- Configurables.instance_method(settername).bind(self).call(v)
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(ArgumentError, "unsupported configuration value passed: #{k.inspect} => #{v.inspect}")
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
- @operation = operation
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
- Addressable::Template.new(operation.path)
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.path} for operation #{operation.operationId} requires path_params " +
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.path} for operation #{operation.operationId} requires path_params " +
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
- url = File.join(base_url, path)
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