scorpio 0.3.1 → 0.4.0

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