• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

notEthan / scorpio / 13123791726

03 Feb 2025 09:01PM UTC coverage: 85.795% (+1.5%) from 84.292%
13123791726

push

github

notEthan
🔴LICENSE

1057 of 1232 relevant lines covered (85.8%)

321.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.77
/lib/scorpio/openapi/operation.rb
1
# frozen_string_literal: true
2

3
module Scorpio
14✔
4
  module OpenAPI
14✔
5
    # An OpenAPI operation
6
    #
7
    # Scorpio::OpenAPI::Operation is a module common to V2 and V3 operations.
8
    module Operation
14✔
9
      module Configurables
14✔
10
        attr_writer :base_url
14✔
11
        def base_url(scheme: self.scheme, server: self.server, server_variables: self.server_variables)
14✔
12
          return @base_url if instance_variable_defined?(:@base_url)
1,512✔
13
          openapi_document.base_url(scheme: scheme, server: server, server_variables: server_variables)
1,512✔
14
        end
15

16
        attr_writer :request_headers
14✔
17
        def request_headers
14✔
18
          return @request_headers if instance_variable_defined?(:@request_headers)
3,990✔
19
          openapi_document.request_headers
3,990✔
20
        end
21

22
        attr_writer :user_agent
14✔
23
        def user_agent
14✔
24
          return @user_agent if instance_variable_defined?(:@user_agent)
1,484✔
25
          openapi_document.user_agent
1,484✔
26
        end
27

28
        attr_writer :faraday_builder
14✔
29
        def faraday_builder
14✔
30
          return @faraday_builder if instance_variable_defined?(:@faraday_builder)
70✔
31
          openapi_document.faraday_builder
70✔
32
        end
33

34
        attr_writer :faraday_adapter
14✔
35
        def faraday_adapter
14✔
36
          return @faraday_adapter if instance_variable_defined?(:@faraday_adapter)
742✔
37
          openapi_document.faraday_adapter
742✔
38
        end
39

40
        attr_writer :logger
14✔
41
        def logger
14✔
42
          return @logger if instance_variable_defined?(:@logger)
742✔
43
          openapi_document.logger
742✔
44
        end
45
      end
46
      include Configurables
14✔
47

48
      # openapi v3?
49
      # @return [Boolean]
50
      def v3?
14✔
51
        is_a?(V3::Operation)
×
52
      end
53

54
      # openapi v2?
55
      # @return [Boolean]
56
      def v2?
14✔
57
        is_a?(V2::Operation)
×
58
      end
59

60
      # the document whence this operation came
61
      # @return [Scorpio::OpenAPI::Document]
62
      def openapi_document
14✔
63
        jsi_parent_nodes.detect { |p| p.is_a?(Scorpio::OpenAPI::Document) }
49,896✔
64
      end
65

66
      # @return [String]
67
      def path_template_str
14✔
68
        return @path_template_str if instance_variable_defined?(:@path_template_str)
140✔
69
        raise(Bug) unless jsi_parent_node.is_a?(Scorpio::OpenAPI::V2::PathItem) || jsi_parent_node.is_a?(Scorpio::OpenAPI::V3::PathItem)
84✔
70
        raise(Bug) unless jsi_parent_node.jsi_parent_node.is_a?(Scorpio::OpenAPI::V2::Paths) || jsi_parent_node.jsi_parent_node.is_a?(Scorpio::OpenAPI::V3::Paths)
84✔
71
        @path_template_str = jsi_parent_node.jsi_ptr.tokens.last
84✔
72
      end
73

74
      # the path as an Addressable::Template
75
      # @return [Addressable::Template]
76
      def path_template
14✔
77
        return @path_template if instance_variable_defined?(:@path_template)
3,486✔
78
        @path_template = Addressable::Template.new(path_template_str)
84✔
79
      end
80

81
      # the URI template, consisting of the base_url concatenated with the path template
82
      # @param base_url [#to_str] the base URL to which the path template is appended
83
      # @return [Addressable::Template]
84
      def uri_template(base_url: self.base_url)
14✔
85
        unless base_url
×
86
          raise(ArgumentError, "no base_url has been specified for operation #{self}")
×
87
        end
88
        # we do not use Addressable::URI#join as the paths should just be concatenated, not resolved.
89
        # we use File.join just to deal with consecutive slashes.
90
        Addressable::Template.new(File.join(base_url, path_template_str))
×
91
      end
92

93
      # the HTTP method of this operation as indicated by the attribute name for this operation
94
      # from the parent PathItem
95
      # @return [String]
96
      def http_method
14✔
97
        return @http_method if instance_variable_defined?(:@http_method)
1,232✔
98
        raise(Bug) unless jsi_parent_node.is_a?(Scorpio::OpenAPI::V2::PathItem) || jsi_parent_node.is_a?(Scorpio::OpenAPI::V3::PathItem)
84✔
99
        @http_method = jsi_ptr.tokens.last
84✔
100
      end
101

102
      # a short identifier for this operation appropriate for an error message
103
      # @return [String]
104
      def human_id
14✔
105
        operationId || "path: #{path_template_str}, method: #{http_method}"
70✔
106
      end
107

108
      # @param status [String, Integer]
109
      # @return [Scorpio::OpenAPI::V3::Response, Scorpio::OpenAPI::V2::Response]
110
      def oa_response(status: )
14✔
111
        status = status.to_s if status.is_a?(Numeric)
1,218✔
112
        if responses
1,218✔
113
          _, oa_response = responses.detect { |k, v| k.to_s == status }
2,436✔
114
          oa_response ||= responses['default']
1,218✔
115
        end
116
        oa_response
1,218✔
117
      end
118

119
      # the parameters specified for this operation, plus any others scorpio considers to be parameters.
120
      #
121
      # this method is not intended to be API-stable at the moment.
122
      #
123
      # @api private
124
      # @return [#to_ary<#to_h>]
125
      def inferred_parameters
14✔
126
        parameters = self.parameters ? self.parameters.to_a.dup : []
210✔
127
        path_template.variables.each do |var|
210✔
128
          unless parameters.any? { |p| p['in'] == 'path' && p['name'] == var }
140✔
129
            # we could instantiate this as a V2::Parameter or a V3::Parameter
130
            # or a ParameterWithContentInPath or whatever. but I can't be bothered.
131
            parameters << {
×
132
              'name' => var,
133
              'in' => 'path',
134
              'required' => true,
135
              'type' => 'string',
136
            }
137
          end
138
        end
139
        parameters
210✔
140
      end
141

142
      # a module with accessor methods for unambiguously named parameters of this operation.
143
      # @return [Module]
144
      def request_accessor_module
14✔
145
        return @request_accessor_module if instance_variable_defined?(:@request_accessor_module)
84✔
146
        @request_accessor_module = begin
36✔
147
          params_by_name = inferred_parameters.group_by { |p| p['name'] }
112✔
148
          Module.new do
84✔
149
            instance_method_modules = [Request]
84✔
150
            instance_method_names = instance_method_modules.map do |mod|
84✔
151
              (mod.instance_methods + mod.private_instance_methods).map(&:to_s)
84✔
152
            end.inject(Set.new, &:merge)
153
            params_by_name.each do |name, params|
84✔
154
              next if instance_method_names.include?(name)
28✔
155
              if params.size == 1
28✔
156
                param = params.first
28✔
157
                define_method("#{name}=") { |value| set_param_from(param['in'], param['name'], value) }
28✔
158
                define_method(name) { get_param_from(param['in'], param['name']) }
28✔
159
              end
160
            end
161
          end
162
        end
163
      end
164

165
      # instantiates a {Scorpio::Request} for this operation.
166
      # parameters are all passed to {Scorpio::Request#initialize}.
167
      # @return [Scorpio::Request]
168
      def build_request(configuration = {}, &b)
14✔
169
        @request_class ||= Scorpio::Request.request_class_by_operation(self)
742✔
170
        @request_class.new(configuration, &b)
742✔
171
      end
172

173
      # runs a {Scorpio::Request} for this operation, returning a {Scorpio::Ur}.
174
      # parameters are all passed to {Scorpio::Request#initialize}.
175
      # @return [Scorpio::Ur] response ur
176
      def run_ur(configuration = {}, &b)
14✔
177
        build_request(configuration, &b).run_ur
28✔
178
      end
179

180
      # runs a {Scorpio::Request} for this operation - see {Scorpio::Request#run}.
181
      # parameters are all passed to {Scorpio::Request#initialize}.
182
      # @return response body object
183
      def run(configuration = {}, &b)
14✔
184
        build_request(configuration, &b).run
×
185
      end
186

187
      # Runs this operation with the given request config, and yields the resulting {Scorpio::Ur}.
188
      # If the response contains a `Link` header with a `next` link (and that link's URL
189
      # corresponds to this operation), this operation is run again to that link's URL, that
190
      # request's Ur yielded, and a `next` link in that response is followed.
191
      # This repeats until a response does not contain a `Link` header with a `next` link.
192
      #
193
      # @param configuration (see Scorpio::Request#initialize)
194
      # @yield [Scorpio::Ur]
195
      # @return [Enumerator, nil]
196
      def each_link_page(configuration = {}, &block)
14✔
197
        init_request = build_request(configuration)
42✔
198
        next_page = proc do |last_page_ur|
42✔
199
          nextlinks = last_page_ur.response.links.select { |link| link.rel?('next') }
98✔
200
          if nextlinks.size == 0
70✔
201
            # no next link; we are at the end
202
            nil
42✔
203
          elsif nextlinks.size == 1
26✔
204
            nextlink = nextlinks.first
28✔
205
            # we do not use Addressable::URI#join as the paths should just be concatenated, not resolved.
206
            # we use File.join just to deal with consecutive slashes.
207
            template = Addressable::Template.new(File.join(init_request.base_url, path_template_str))
28✔
208
            target_uri = nextlink.absolute_target_uri
28✔
209
            path_params = template.extract(target_uri.merge(query: nil))
28✔
210
            unless path_params
28✔
211
              raise("the URI of the link to the next page did not match the URI of this operation")
×
212
            end
213
            query_params = target_uri.query_values
28✔
214
            run_ur(
28✔
215
              path_params: path_params,
216
              query_params: query_params,
217
            )
218
          else
219
            # TODO better error class / context / message
220
            raise("response included multiple links with rel=next")
×
221
          end
222
        end
223
        init_request.each_page_ur(next_page: next_page, &block)
42✔
224
      end
225

226
      private
14✔
227

228
      def jsi_object_group_text
14✔
229
        [*super, http_method, path_template_str].freeze
×
230
      end
231
    end
232

233
    module V3
14✔
234
      raise(Bug, 'const_defined? Scorpio::OpenAPI::V3::Operation') unless const_defined?(:Operation)
14✔
235

236
      # Describes a single API operation on a path.
237
      #
238
      # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operationObject
239
      module Operation
14✔
240
        module Configurables
14✔
241
          def scheme
14✔
242
            # not applicable; for OpenAPI v3, scheme is specified by servers.
243
            nil
864✔
244
          end
245

246
          attr_writer :server
14✔
247
          def server
14✔
248
            return @server if instance_variable_defined?(:@server)
1,512✔
249
            openapi_document.server
1,512✔
250
          end
251

252
          attr_writer :server_variables
14✔
253
          def server_variables
14✔
254
            return @server_variables if instance_variable_defined?(:@server_variables)
1,512✔
255
            openapi_document.server_variables
1,512✔
256
          end
257

258
          attr_writer :request_media_type
14✔
259
          def request_media_type
14✔
260
            return @request_media_type if instance_variable_defined?(:@request_media_type)
2,968✔
261
            if requestBody && requestBody['content']
2,968✔
262
              Request.best_media_type(requestBody['content'].keys)
2,058✔
263
            else
264
              openapi_document.request_media_type
910✔
265
            end
266
          end
267
        end
268
        include Configurables
14✔
269

270
        # @return [JSI::Schema]
271
        def request_schema(media_type: self.request_media_type)
14✔
272
          # TODO typechecking on requestBody & children
273
          request_content = requestBody && requestBody['content']
966✔
274
          return nil unless request_content
966✔
275
          raise(ArgumentError, "please specify media_type for request_schema") unless media_type
546✔
276
          schema = request_content[media_type] && request_content[media_type]['schema']
546✔
277
          return nil unless schema
546✔
278
          JSI::Schema.ensure_schema(schema)
546✔
279
        end
280

281
        # @return [JSI::SchemaSet]
282
        def request_schemas
14✔
283
          JSI::SchemaSet.build do |schemas|
518✔
284
            if requestBody && requestBody['content']
518✔
285
              requestBody['content'].each_value do |oa_media_type|
168✔
286
                if oa_media_type['schema']
168✔
287
                  schemas << oa_media_type['schema']
168✔
288
                end
289
              end
290
            end
291
          end
292
        end
293

294
        # @return [JSI::Schema]
295
        def response_schema(status: , media_type: )
14✔
296
          oa_response = self.oa_response(status: status)
1,218✔
297
          oa_media_types = oa_response ? oa_response['content'] : nil # Scorpio::OpenAPI::V3::MediaTypes
1,218✔
298
          oa_media_type = oa_media_types ? oa_media_types[media_type] : nil # Scorpio::OpenAPI::V3::MediaType
1,218✔
299
          oa_schema = oa_media_type ? oa_media_type['schema'] : nil # Scorpio::OpenAPI::V3::Schema
1,218✔
300
          oa_schema ? JSI::Schema.ensure_schema(oa_schema) : nil
1,218✔
301
        end
302

303
        # @return [JSI::SchemaSet]
304
        def response_schemas
14✔
305
          JSI::SchemaSet.build do |schemas|
364✔
306
            if responses
364✔
307
              responses.each_value do |oa_response|
364✔
308
                if oa_response['content']
364✔
309
                  oa_response['content'].each_value do |oa_media_type|
280✔
310
                    if oa_media_type['schema']
280✔
311
                      schemas << oa_media_type['schema']
280✔
312
                    end
313
                  end
314
                end
315
              end
316
            end
317
          end
318
        end
319
      end
320
    end
321
    module V2
14✔
322
      raise(Bug, 'const_defined? Scorpio::OpenAPI::V2::Operation') unless const_defined?(:Operation)
14✔
323
      module Operation
14✔
324
        module Configurables
14✔
325
          attr_writer :scheme
14✔
326
          def scheme
14✔
327
            return @scheme if instance_variable_defined?(:@scheme)
×
328
            openapi_document.scheme
×
329
          end
330
          def server
14✔
331
            nil
332
          end
333
          def server_variables
14✔
334
            nil
335
          end
336

337
          attr_writer :request_media_type
14✔
338
          def request_media_type
14✔
339
            return @request_media_type if instance_variable_defined?(:@request_media_type)
×
340
            if key?('consumes')
×
341
              Request.best_media_type(consumes)
×
342
            else
343
              openapi_document.request_media_type
×
344
            end
345
          end
346
        end
347
        include Configurables
14✔
348

349
        # the body parameter
350
        # @return [#to_hash]
351
        # @raise [Scorpio::OpenAPI::SemanticError] if there's more than one body param
352
        def body_parameter
14✔
353
          body_parameters = (parameters || []).select { |parameter| parameter['in'] == 'body' }
×
354
          if body_parameters.size == 0
×
355
            nil
356
          elsif body_parameters.size == 1
357
            body_parameters.first
×
358
          else
359
            # TODO blame
360
            raise(OpenAPI::SemanticError, "multiple body parameters on operation #{operation.pretty_inspect.chomp}")
×
361
          end
362
        end
363

364
        # request schema for the given media_type
365
        # @param media_type unused
366
        # @return [JSI::Schema]
367
        def request_schema(media_type: nil)
14✔
368
          if body_parameter && body_parameter['schema']
×
369
            JSI::Schema.ensure_schema(body_parameter['schema'])
×
370
          else
371
            nil
372
          end
373
        end
374

375
        # @return [JSI::SchemaSet]
376
        def request_schemas
14✔
377
          request_schema ? JSI::SchemaSet[request_schema] : JSI::SchemaSet[]
×
378
        end
379

380
        # @param status [Integer, String] response status
381
        # @param media_type unused
382
        # @return [JSI::Schema]
383
        def response_schema(status: , media_type: nil)
14✔
384
          oa_response = self.oa_response(status: status)
×
385
          oa_response_schema = oa_response ? oa_response['schema'] : nil # Scorpio::OpenAPI::V2::Schema
×
386
          oa_response_schema ? JSI::Schema.ensure_schema(oa_response_schema) : nil
×
387
        end
388

389
        # @return [JSI::SchemaSet]
390
        def response_schemas
14✔
391
          JSI::SchemaSet.build do |schemas|
×
392
            if responses
×
393
              responses.each_value do |oa_response|
×
394
                if oa_response['schema']
×
395
                  schemas << oa_response['schema']
×
396
                end
397
              end
398
            end
399
          end
400
        end
401
      end
402
    end
403
  end
404
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc