scorpio 0.2.3 → 0.3.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.
@@ -0,0 +1,167 @@
1
+ module Scorpio
2
+ module OpenAPI
3
+ module Document
4
+ class << self
5
+ def from_instance(instance)
6
+ if instance.is_a?(Hash)
7
+ instance = JSI::JSON::Node.new_doc(instance)
8
+ end
9
+ if instance.is_a?(JSI::JSON::Node)
10
+ if instance['swagger'] =~ /\A2(\.|\z)/
11
+ instance = Scorpio::OpenAPI::V2::Document.new(instance)
12
+ elsif instance['openapi'] =~ /\A3(\.|\z)/
13
+ instance = Scorpio::OpenAPI::V3::Document.new(instance)
14
+ else
15
+ raise(ArgumentError, "instance does not look like a recognized openapi document")
16
+ end
17
+ end
18
+ if instance.is_a?(Scorpio::OpenAPI::Document)
19
+ instance
20
+ elsif instance.is_a?(JSI::Base)
21
+ raise(TypeError, "instance is unexpected JSI type: #{instance.class.inspect}")
22
+ elsif instance.respond_to?(:to_hash)
23
+ from_instance(instance.to_hash)
24
+ else
25
+ raise(TypeError, "instance does not look like a hash (json object)")
26
+ end
27
+ end
28
+ end
29
+
30
+ module Configurables
31
+ attr_writer :request_headers
32
+ def request_headers
33
+ return @request_headers if instance_variable_defined?(:@request_headers)
34
+ {}.freeze
35
+ end
36
+
37
+ attr_writer :user_agent
38
+ def user_agent
39
+ return @user_agent if instance_variable_defined?(:@user_agent)
40
+ "Scorpio/#{Scorpio::VERSION} (https://github.com/notEthan/scorpio) Faraday/#{Faraday::VERSION} Ruby/#{RUBY_VERSION}"
41
+ end
42
+
43
+ attr_writer :faraday_builder
44
+ def faraday_builder
45
+ return @faraday_builder if instance_variable_defined?(:@faraday_builder)
46
+ -> (_) { }
47
+ end
48
+
49
+ attr_writer :faraday_adapter
50
+ def faraday_adapter
51
+ return @faraday_adapter if instance_variable_defined?(:@faraday_adapter)
52
+ [Faraday.default_adapter]
53
+ end
54
+
55
+ attr_writer :logger
56
+ def logger
57
+ return @logger if instance_variable_defined?(:@logger)
58
+ (Object.const_defined?(:Rails) && ::Rails.respond_to?(:logger) ? ::Rails.logger : nil)
59
+ end
60
+ end
61
+ include Configurables
62
+
63
+ def v2?
64
+ is_a?(V2::Document)
65
+ end
66
+
67
+ def v3?
68
+ is_a?(V3::Document)
69
+ end
70
+
71
+ def operations
72
+ return @operations if instance_variable_defined?(:@operations)
73
+ @operations = OperationsScope.new(self)
74
+ end
75
+ end
76
+
77
+ module V3
78
+ raise(Bug) unless const_defined?(:Document)
79
+ class Document
80
+ module Configurables
81
+ def scheme
82
+ nil
83
+ end
84
+ attr_writer :server
85
+ def server
86
+ return @server if instance_variable_defined?(:@server)
87
+ if servers.respond_to?(:to_ary) && servers.size == 1
88
+ servers.first
89
+ else
90
+ nil
91
+ end
92
+ end
93
+ attr_writer :server_variables
94
+ def server_variables
95
+ return @server_variables if instance_variable_defined?(:@server_variables)
96
+ {}.freeze
97
+ end
98
+ attr_writer :base_url
99
+ def base_url(scheme: nil, server: self.server, server_variables: self.server_variables)
100
+ return @base_url if instance_variable_defined?(:@base_url)
101
+ if server
102
+ server.expanded_url(server_variables)
103
+ end
104
+ end
105
+
106
+ attr_writer :request_media_type
107
+ def request_media_type
108
+ return @request_media_type if instance_variable_defined?(:@request_media_type)
109
+ nil
110
+ end
111
+ end
112
+ include Configurables
113
+ end
114
+ end
115
+
116
+ module V2
117
+ raise(Bug) unless const_defined?(:Document)
118
+ class Document
119
+ module Configurables
120
+ attr_writer :scheme
121
+ def scheme
122
+ return @scheme if instance_variable_defined?(:@scheme)
123
+ if schemes.nil?
124
+ 'https'
125
+ elsif schemes.respond_to?(:to_ary)
126
+ # prefer https, then http, then anything else since we probably don't support.
127
+ schemes.sort_by { |s| ['https', 'http'].index(s) || (1.0 / 0) }.first
128
+ end
129
+ end
130
+
131
+ def server
132
+ nil
133
+ end
134
+ def server_variables
135
+ nil
136
+ end
137
+
138
+ attr_writer :base_url
139
+ # the base url to which paths are appended.
140
+ # by default this looks at the openapi document's schemes, picking https or http first.
141
+ # it looks at the openapi_document's host and basePath.
142
+ def base_url(scheme: self.scheme, server: nil, server_variables: nil)
143
+ return @base_url if instance_variable_defined?(:@base_url)
144
+ if host && scheme
145
+ Addressable::URI.new(
146
+ scheme: scheme,
147
+ host: host,
148
+ path: basePath,
149
+ ).to_s
150
+ end
151
+ end
152
+
153
+ attr_writer :request_media_type
154
+ def request_media_type
155
+ return @request_media_type if instance_variable_defined?(:@request_media_type)
156
+ if consumes.respond_to?(:to_ary)
157
+ Request.best_media_type(consumes)
158
+ else
159
+ nil
160
+ end
161
+ end
162
+ end
163
+ include Configurables
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,208 @@
1
+ module Scorpio
2
+ module OpenAPI
3
+ module Operation
4
+ module Configurables
5
+ attr_writer :base_url
6
+ def base_url(scheme: self.scheme, server: self.server, server_variables: self.server_variables)
7
+ return @base_url if instance_variable_defined?(:@base_url)
8
+ openapi_document.base_url(scheme: scheme, server: server, server_variables: server_variables)
9
+ end
10
+
11
+ attr_writer :request_headers
12
+ def request_headers
13
+ return @request_headers if instance_variable_defined?(:@request_headers)
14
+ openapi_document.request_headers
15
+ end
16
+
17
+ attr_writer :user_agent
18
+ def user_agent
19
+ return @user_agent if instance_variable_defined?(:@user_agent)
20
+ openapi_document.user_agent
21
+ end
22
+
23
+ attr_writer :faraday_builder
24
+ def faraday_builder
25
+ return @faraday_builder if instance_variable_defined?(:@faraday_builder)
26
+ openapi_document.faraday_builder
27
+ end
28
+
29
+ attr_writer :faraday_adapter
30
+ def faraday_adapter
31
+ return @faraday_adapter if instance_variable_defined?(:@faraday_adapter)
32
+ openapi_document.faraday_adapter
33
+ end
34
+
35
+ attr_writer :logger
36
+ def logger
37
+ return @logger if instance_variable_defined?(:@logger)
38
+ openapi_document.logger
39
+ end
40
+ end
41
+ include Configurables
42
+
43
+ def openapi_document
44
+ parents.detect { |p| p.is_a?(Scorpio::OpenAPI::Document) }
45
+ end
46
+
47
+ def path
48
+ return @path if instance_variable_defined?(:@path)
49
+ @path = begin
50
+ parent_is_pathitem = parent.is_a?(Scorpio::OpenAPI::V2::PathItem) || parent.is_a?(Scorpio::OpenAPI::V3::PathItem)
51
+ parent_parent_is_paths = parent.parent.is_a?(Scorpio::OpenAPI::V2::Paths) || parent.parent.is_a?(Scorpio::OpenAPI::V3::Paths)
52
+ if parent_is_pathitem && parent_parent_is_paths
53
+ parent.instance.path.last
54
+ end
55
+ end
56
+ end
57
+
58
+ def http_method
59
+ return @http_method if instance_variable_defined?(:@http_method)
60
+ @http_method = begin
61
+ parent_is_pathitem = parent.is_a?(Scorpio::OpenAPI::V2::PathItem) || parent.is_a?(Scorpio::OpenAPI::V3::PathItem)
62
+ if parent_is_pathitem
63
+ instance.path.last
64
+ end
65
+ end
66
+ end
67
+
68
+ def build_request(*a, &b)
69
+ request = Scorpio::Request.new(self, *a, &b)
70
+ end
71
+
72
+ def run_ur(*a, &b)
73
+ build_request(*a, &b).run_ur
74
+ end
75
+
76
+ def run(*a, &b)
77
+ build_request(*a, &b).run
78
+ end
79
+ end
80
+
81
+ module V3
82
+ raise(Bug) unless const_defined?(:Operation)
83
+ class Operation
84
+ module Configurables
85
+ def scheme
86
+ nil
87
+ end
88
+
89
+ attr_writer :server
90
+ def server
91
+ return @server if instance_variable_defined?(:@server)
92
+ openapi_document.server
93
+ end
94
+
95
+ attr_writer :server_variables
96
+ def server_variables
97
+ return @server_variables if instance_variable_defined?(:@server_variables)
98
+ openapi_document.server_variables
99
+ end
100
+
101
+ attr_writer :request_media_type
102
+ def request_media_type
103
+ return @request_media_type if instance_variable_defined?(:@request_media_type)
104
+ if requestBody && requestBody['content']
105
+ Request.best_media_type(requestBody['content'].keys)
106
+ else
107
+ openapi_document.request_media_type
108
+ end
109
+ end
110
+ end
111
+ include Configurables
112
+
113
+ def request_schema(media_type: self.request_media_type)
114
+ # TODO typechecking on requestBody & children
115
+ requestBody &&
116
+ requestBody['content'] &&
117
+ requestBody['content'][media_type] &&
118
+ requestBody['content'][media_type]['schema'] &&
119
+ requestBody['content'][media_type]['schema'].deref
120
+ end
121
+
122
+ def request_schemas
123
+ if requestBody && requestBody['content']
124
+ # oamt is for Scorpio::OpenAPI::V3::MediaType
125
+ requestBody['content'].values.map { |oamt| oamt['schema'] }.compact.map(&:deref)
126
+ end
127
+ end
128
+
129
+ # @return JSI::Schema
130
+ def response_schema(status: , media_type: )
131
+ status = status.to_s if status.is_a?(Numeric)
132
+ if self.responses
133
+ # Scorpio::OpenAPI::V3::Response
134
+ _, oa_response = self.responses.detect { |k, v| k.to_s == status }
135
+ oa_response ||= self.responses['default']
136
+ end
137
+ oa_media_types = oa_response ? oa_response['content'] : nil # Scorpio::OpenAPI::V3::MediaTypes
138
+ oa_media_type = oa_media_types ? oa_media_types[media_type] : nil # Scorpio::OpenAPI::V3::MediaType
139
+ oa_schema = oa_media_type ? oa_media_type['schema'] : nil # Scorpio::OpenAPI::V3::Schema
140
+ oa_schema ? JSI::Schema.new(oa_schema) : nil
141
+ end
142
+ end
143
+ end
144
+ module V2
145
+ raise(Bug) unless const_defined?(:Operation)
146
+ class Operation
147
+ module Configurables
148
+ attr_writer :scheme
149
+ def scheme
150
+ return @scheme if instance_variable_defined?(:@scheme)
151
+ openapi_document.scheme
152
+ end
153
+ def server
154
+ nil
155
+ end
156
+ def server_variables
157
+ nil
158
+ end
159
+
160
+ attr_writer :request_media_type
161
+ def request_media_type
162
+ return @request_media_type if instance_variable_defined?(:@request_media_type)
163
+ if key?('consumes')
164
+ Request.best_media_type(consumes)
165
+ else
166
+ openapi_document.request_media_type
167
+ end
168
+ end
169
+ end
170
+ include Configurables
171
+
172
+ # there should only be one body parameter; this returns it
173
+ def body_parameter
174
+ body_parameters = (parameters || []).select { |parameter| parameter['in'] == 'body' }
175
+ if body_parameters.size == 0
176
+ nil
177
+ elsif body_parameters.size == 1
178
+ body_parameters.first
179
+ else
180
+ raise(Bug) # TODO BLAME
181
+ end
182
+ end
183
+
184
+ def request_schema(media_type: nil)
185
+ if body_parameter && body_parameter['schema']
186
+ JSI::Schema.new(body_parameter['schema'])
187
+ end
188
+ end
189
+
190
+ def request_schemas
191
+ [request_schema]
192
+ end
193
+
194
+ # @return JSI::Schema
195
+ def response_schema(status: , media_type: nil)
196
+ status = status.to_s if status.is_a?(Numeric)
197
+ if self.responses
198
+ # Scorpio::OpenAPI::V2::Response
199
+ _, oa_response = self.responses.detect { |k, v| k.to_s == status }
200
+ oa_response ||= self.responses['default']
201
+ end
202
+ oa_response_schema = oa_response ? oa_response['schema'] : nil # Scorpio::OpenAPI::V2::Schema
203
+ oa_response_schema ? JSI::Schema.new(oa_response_schema) : nil
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,29 @@
1
+ module Scorpio
2
+ module OpenAPI
3
+ class OperationsScope
4
+ include JSI::Memoize
5
+
6
+ def initialize(openapi_document)
7
+ @openapi_document = openapi_document
8
+ end
9
+ attr_reader :openapi_document
10
+
11
+ def each
12
+ openapi_document.paths.each do |path, path_item|
13
+ path_item.each do |http_method, operation|
14
+ if operation.is_a?(Scorpio::OpenAPI::Operation)
15
+ yield operation
16
+ end
17
+ end
18
+ end
19
+ end
20
+ include Enumerable
21
+
22
+ def [](operationId_)
23
+ memoize(:[], operationId_) do |operationId|
24
+ detect { |operation| operation.operationId == operationId }
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,32 @@
1
+ module Scorpio
2
+ module OpenAPI
3
+ module V3
4
+ raise(Bug) unless const_defined?(:Server)
5
+ class Server
6
+ def expanded_url(given_server_variables)
7
+ if variables
8
+ server_variables = (given_server_variables.keys | variables.keys).map do |key|
9
+ server_variable = variables[key]
10
+ if server_variable && server_variable.enum
11
+ unless server_variable.enum.include?(given_server_variables[key])
12
+ warn # TODO BLAME
13
+ end
14
+ end
15
+ if given_server_variables.key?(key)
16
+ {key => given_server_variables[key]}
17
+ elsif server_variable.key?('default')
18
+ {key => server_variable.default}
19
+ else
20
+ {}
21
+ end
22
+ end.inject({}, &:update)
23
+ else
24
+ server_variables = given_server_variables
25
+ end
26
+ template = Addressable::Template.new(url)
27
+ template.expand(server_variables)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,227 @@
1
+ module Scorpio
2
+ class Request
3
+ SUPPORTED_REQUEST_MEDIA_TYPES = ['application/json', 'application/x-www-form-urlencoded']
4
+ def self.best_media_type(media_types)
5
+ if media_types.size == 1
6
+ media_types.first
7
+ else
8
+ SUPPORTED_REQUEST_MEDIA_TYPES.detect { |mt| media_types.include?(mt) }
9
+ end
10
+ end
11
+
12
+ module Configurables
13
+ attr_writer :path_params
14
+ def path_params
15
+ return @path_params if instance_variable_defined?(:@path_params)
16
+ {}.freeze
17
+ end
18
+
19
+ attr_writer :query_params
20
+ def query_params
21
+ return @query_params if instance_variable_defined?(:@query_params)
22
+ nil
23
+ end
24
+
25
+ attr_writer :scheme
26
+ def scheme
27
+ return @scheme if instance_variable_defined?(:@scheme)
28
+ operation.scheme
29
+ end
30
+
31
+ attr_writer :server
32
+ def server
33
+ return @server if instance_variable_defined?(:@server)
34
+ operation.server
35
+ end
36
+
37
+ attr_writer :server_variables
38
+ def server_variables
39
+ return @server_variables if instance_variable_defined?(:@server_variables)
40
+ operation.server_variables
41
+ end
42
+
43
+ attr_writer :base_url
44
+ def base_url
45
+ return @base_url if instance_variable_defined?(:@base_url)
46
+ operation.base_url(scheme: scheme, server: server, server_variables: server_variables)
47
+ end
48
+
49
+ attr_writer :body
50
+ def body
51
+ return @body if instance_variable_defined?(:@body)
52
+ if instance_variable_defined?(:@body_object)
53
+ # TODO handle media types like `application/schema-instance+json`
54
+ if media_type == 'application/json'
55
+ JSON.pretty_generate(JSI::Typelike.as_json(body_object))
56
+ elsif media_type == "application/x-www-form-urlencoded"
57
+ URI.encode_www_form(body_object)
58
+
59
+ # NOTE: the supported media types above should correspond to Request::SUPPORTED_REQUEST_MEDIA_TYPES
60
+
61
+ else
62
+ if body_object.respond_to?(:to_str)
63
+ body_object
64
+ else
65
+ raise(NotImplementedError)
66
+ end
67
+ end
68
+ else
69
+ nil
70
+ end
71
+ end
72
+
73
+ attr_accessor :body_object
74
+
75
+ attr_writer :headers
76
+ def headers
77
+ return @headers if instance_variable_defined?(:@headers)
78
+ operation.request_headers
79
+ end
80
+
81
+ attr_writer :media_type
82
+ def media_type
83
+ return @media_type if instance_variable_defined?(:@media_type)
84
+ content_type_header ? content_type_attrs.media_type : operation.request_media_type
85
+ end
86
+
87
+ attr_writer :user_agent
88
+ def user_agent
89
+ return @user_agent if instance_variable_defined?(:@user_agent)
90
+ operation.user_agent
91
+ end
92
+
93
+ attr_writer :faraday_builder
94
+ def faraday_builder
95
+ return @faraday_builder if instance_variable_defined?(:@faraday_builder)
96
+ operation.faraday_builder
97
+ end
98
+
99
+ attr_writer :faraday_adapter
100
+ def faraday_adapter
101
+ return @faraday_adapter if instance_variable_defined?(:@faraday_adapter)
102
+ operation.faraday_adapter
103
+ end
104
+
105
+ attr_writer :logger
106
+ def logger
107
+ return @logger if instance_variable_defined?(:@logger)
108
+ operation.logger
109
+ end
110
+ end
111
+ include Configurables
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)
118
+ else
119
+ raise(ArgumentError, "unsupported configuration value passed: #{k.inspect} => #{v.inspect}")
120
+ end
121
+ end
122
+
123
+ @operation = operation
124
+ if block_given?
125
+ yield self
126
+ end
127
+ end
128
+
129
+ attr_reader :operation
130
+
131
+ def openapi_document
132
+ operation.openapi_document
133
+ end
134
+
135
+ def http_method
136
+ operation.http_method.downcase.to_sym
137
+ end
138
+
139
+ def path_template
140
+ Addressable::Template.new(operation.path)
141
+ end
142
+
143
+ def path
144
+ missing_variables = path_template.variables - path_params.keys
145
+ if missing_variables.any?
146
+ raise(ArgumentError, "path #{operation.path} for operation #{operation.operationId} requires path_params " +
147
+ "which were missing: #{missing_variables.inspect}")
148
+ end
149
+ empty_variables = path_template.variables.select { |v| path_params[v].to_s.empty? }
150
+ if empty_variables.any?
151
+ raise(ArgumentError, "path #{operation.path} for operation #{operation.operationId} requires path_params " +
152
+ "which were empty: #{empty_variables.inspect}")
153
+ end
154
+
155
+ path_template.expand(path_params).tap do |path|
156
+ if query_params
157
+ path.query_values = query_params
158
+ end
159
+ end
160
+ end
161
+
162
+ def url
163
+ unless base_url
164
+ raise(ArgumentError, "no base_url has been specified for request")
165
+ end
166
+ # we do not use Addressable::URI#join as the paths should just be concatenated, not resolved.
167
+ # we use File.join just to deal with consecutive slashes.
168
+ url = File.join(base_url, path)
169
+ url = Addressable::URI.parse(url)
170
+ end
171
+
172
+ def content_type_attrs
173
+ Ur::ContentTypeAttrs.new(content_type)
174
+ end
175
+
176
+ def content_type_header
177
+ headers.each do |k, v|
178
+ return v if k =~ /\Acontent[-_]type\z/i
179
+ end
180
+ nil
181
+ end
182
+
183
+ def content_type
184
+ content_type_header || media_type
185
+ end
186
+
187
+ def request_schema(media_type: self.media_type)
188
+ operation.request_schema(media_type: media_type)
189
+ end
190
+
191
+ def request_schema_class(media_type: self.media_type)
192
+ JSI.class_for_schema(request_schema(media_type: media_type))
193
+ end
194
+
195
+ def faraday_connection(yield_ur)
196
+ Faraday.new do |faraday_connection|
197
+ faraday_builder.call(faraday_connection)
198
+ ::Ur::Faraday # autoload trigger
199
+ faraday_connection.response(:yield_ur, ur_class: Scorpio::Ur, logger: self.logger, &yield_ur)
200
+ faraday_connection.adapter(*faraday_adapter)
201
+ end
202
+ end
203
+
204
+ def run_ur
205
+ headers = {}
206
+ if user_agent
207
+ headers['User-Agent'] = user_agent
208
+ end
209
+ if media_type && !content_type_header
210
+ headers['Content-Type'] = media_type
211
+ end
212
+ if self.headers
213
+ headers.update(self.headers)
214
+ end
215
+ ur = nil
216
+ faraday_connection(-> (yur) { ur = yur }).run_request(http_method, url, body, headers)
217
+ ur.scorpio_request = self
218
+ ur
219
+ end
220
+
221
+ def run
222
+ ur = run_ur
223
+ ur.raise_on_http_error
224
+ ur.response.body_object
225
+ end
226
+ end
227
+ end