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

notEthan / scorpio / 22243088469

20 Feb 2026 10:07PM UTC coverage: 86.948% (-0.6%) from 87.5%
22243088469

push

github

notEthan
jsi ~> 0.9

1259 of 1448 relevant lines covered (86.95%)

385.58 hits per line

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

81.86
/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,680✔
13
          openapi_document.base_url(scheme: scheme, server: server, server_variables: server_variables)
1,680✔
14
        end
15

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

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

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

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

40
        attr_writer :logger
14✔
41
        def logger
14✔
42
          return @logger if instance_variable_defined?(:@logger)
1,050✔
43
          openapi_document.logger
1,050✔
44
        end
45
      end
46
      include Configurables
14✔
47
      include(Document::Descendent)
14✔
48

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

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

61
      # @return [String]
62
      def path_template_str
14✔
63
        return @path_template_str if instance_variable_defined?(:@path_template_str)
490✔
64
        return(@path_template_str = nil) unless jsi_parent_node.is_a?(Scorpio::OpenAPI::PathItem)
224✔
65
        return(@path_template_str = nil) unless jsi_parent_node.jsi_parent_node.is_a?(Scorpio::OpenAPI::Paths)
224✔
66
        @path_template_str = jsi_parent_node.jsi_ptr.tokens.last
224✔
67
      end
68

69
      # the path as an Addressable::Template
70
      # @return [Addressable::Template]
71
      def path_template
14✔
72
        return @path_template if instance_variable_defined?(:@path_template)
4,914✔
73
        return(@path_template = nil) if !path_template_str
224✔
74
        @path_template = Addressable::Template.new(path_template_str)
224✔
75
      end
76

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

89
      # the HTTP method of this operation as indicated by the attribute name for this operation
90
      # from the parent PathItem
91
      # @return [String]
92
      def http_method
14✔
93
        return @http_method if instance_variable_defined?(:@http_method)
1,862✔
94
        return(@http_method = nil) unless jsi_parent_node.is_a?(Scorpio::OpenAPI::PathItem)
210✔
95
        @http_method = jsi_ptr.tokens.last
210✔
96
      end
97

98
      def get?
14✔
99
        'get'.casecmp?(http_method)
×
100
      end
101

102
      def put?
14✔
103
        'put'.casecmp?(http_method)
×
104
      end
105

106
      def post?
14✔
107
        'post'.casecmp?(http_method)
×
108
      end
109

110
      def delete?
14✔
111
        'delete'.casecmp?(http_method)
×
112
      end
113

114
      def options?
14✔
115
        'options'.casecmp?(http_method)
×
116
      end
117

118
      def head?
14✔
119
        'head'.casecmp?(http_method)
×
120
      end
121

122
      def patch?
14✔
123
        'patch'.casecmp?(http_method)
×
124
      end
125

126
      def trace?
14✔
127
        'trace'.casecmp?(http_method)
×
128
      end
129

130
      # @param tag_name [String]
131
      # @return [Boolean]
132
      def tagged?(tag_name)
14✔
133
        tags.respond_to?(:to_ary) && tags.include?(tag_name)
1,554✔
134
      end
135

136
      # a short identifier for this operation appropriate for an error message
137
      # @return [String]
138
      def human_id
14✔
139
        operationId || "path: #{path_template_str}, method: #{http_method}"
70✔
140
      end
141

142
      # @param status [String, Integer]
143
      # @return [Scorpio::OpenAPI::V3_0::Response, Scorpio::OpenAPI::V2::Response]
144
      def oa_response(status: )
14✔
145
        status = status.to_s if status.is_a?(Numeric)
1,568✔
146
        if responses
1,568✔
147
          _, oa_response = responses.detect { |k, v| k.to_s == status }
2,884✔
148
          oa_response ||= responses['default']
1,442✔
149
        end
150
        oa_response
1,568✔
151
      end
152

153
      # the parameters specified for this operation, plus any others scorpio considers to be parameters.
154
      #
155
      # @api private
156
      # @return [#to_ary<#to_h>]
157
      def inferred_parameters
14✔
158
        parameters = self.parameters ? self.parameters.to_a.dup : []
462✔
159
        path_template.variables.each do |var|
462✔
160
          unless parameters.any? { |p| p['in'] == 'path' && p['name'] == var }
224✔
161
            # we could instantiate this as a V2::Parameter or a V3_0::Parameter
162
            # or a ParameterWithContentInPath or whatever. but I can't be bothered.
163
            parameters << {
84✔
164
              'name' => var,
165
              'in' => 'path',
166
              'required' => true,
167
              'type' => 'string',
168
            }
169
          end
170
        end
171
        parameters
462✔
172
      end
173

174
      # a module with accessor methods for unambiguously named parameters of this operation.
175
      # @return [Module]
176
      def request_accessor_module
14✔
177
        return @request_accessor_module if instance_variable_defined?(:@request_accessor_module)
210✔
178
        @request_accessor_module = begin
90✔
179
          operation = self
210✔
180
          params_by_name = inferred_parameters.group_by { |p| p['name'] }
280✔
181
          Module.new do
210✔
182
            define_singleton_method(:inspect) { "(Scorpio param module for operation: #{operation.human_id})" }
210✔
183
            instance_method_modules = [Request]
210✔
184
            instance_method_names = instance_method_modules.map do |mod|
210✔
185
              (mod.instance_methods + mod.private_instance_methods).map(&:to_s)
210✔
186
            end.inject(Set.new, &:merge)
187
            params_by_name.each do |name, params|
210✔
188
              next if instance_method_names.include?(name)
70✔
189
              if params.size == 1
70✔
190
                param = params.first
70✔
191
                define_method("#{name}=") { |value| set_param_from(param['in'], param['name'], value) }
70✔
192
                define_method(name) { get_param_from(param['in'], param['name']) }
70✔
193
              end
194
            end
195
          end
196
        end
197
      end
198

199
      # instantiates a {Scorpio::Request} for this operation.
200
      # parameters are all passed to {Scorpio::Request#initialize}.
201
      # @return [Scorpio::Request]
202
      def build_request(**configuration, &b)
14✔
203
        @request_class ||= Scorpio::Request.request_class_by_operation(self)
1,050✔
204
        @request_class.new(**configuration, &b)
1,050✔
205
      end
206

207
      # runs a {Scorpio::Request} for this operation, returning a {Scorpio::Ur}.
208
      # parameters are all passed to {Scorpio::Request#initialize}.
209
      # @return [Scorpio::Ur] response ur
210
      def run_ur(**configuration, &b)
14✔
211
        build_request(**configuration, &b).run_ur
28✔
212
      end
213

214
      # runs a {Scorpio::Request} for this operation - see {Scorpio::Request#run}.
215
      # parameters are all passed to {Scorpio::Request#initialize}.
216
      # @return response body object
217
      def run(mutable: false, **configuration, &b)
14✔
218
        build_request(**configuration, &b).run(mutable: mutable)
×
219
      end
220

221
      # Runs this operation with the given request config, and yields the resulting {Scorpio::Ur}.
222
      # If the response contains a `Link` header with a `next` link (and that link's URL
223
      # corresponds to this operation), this operation is run again to that link's URL, that
224
      # request's Ur yielded, and a `next` link in that response is followed.
225
      # This repeats until a response does not contain a `Link` header with a `next` link.
226
      #
227
      # @param configuration (see Scorpio::Request#initialize)
228
      # @yield [Scorpio::Ur]
229
      # @return [Enumerator, nil]
230
      def each_link_page(**configuration, &block)
14✔
231
        init_request = build_request(**configuration)
42✔
232
        next_page = proc do |last_page_ur|
42✔
233
          nextlinks = last_page_ur.response.links.select { |link| link.rel?('next') }
98✔
234
          if nextlinks.size == 0
70✔
235
            # no next link; we are at the end
236
            nil
42✔
237
          elsif nextlinks.size == 1
26✔
238
            run_ur(url: nextlinks.first.absolute_target_uri)
28✔
239
          else
240
            # TODO better error class / context / message
241
            raise("response included multiple links with rel=next")
×
242
          end
243
        end
244
        init_request.each_page_ur(next_page: next_page, &block)
42✔
245
      end
246

247
      private
14✔
248

249
      def jsi_object_group_text
14✔
250
        [*super, http_method, path_template_str].compact.freeze
×
251
      end
252
    end
253

254
    module Operation
14✔
255
      module V3Methods
14✔
256
        module Configurables
14✔
257
          def scheme
14✔
258
            # not applicable; for OpenAPI v3, scheme is specified by servers.
259
            nil
960✔
260
          end
261

262
          attr_writer :server
14✔
263
          def server
14✔
264
            return @server if instance_variable_defined?(:@server)
1,680✔
265
            openapi_document.server
1,680✔
266
          end
267

268
          attr_writer :server_variables
14✔
269
          def server_variables
14✔
270
            return @server_variables if instance_variable_defined?(:@server_variables)
1,680✔
271
            openapi_document.server_variables
1,680✔
272
          end
273

274
          attr_writer :request_media_type
14✔
275
          def request_media_type
14✔
276
            return @request_media_type if instance_variable_defined?(:@request_media_type)
3,570✔
277
            if requestBody && requestBody['content']
3,570✔
278
              Request.best_media_type(requestBody['content'].keys)
2,156✔
279
            else
280
              openapi_document.request_media_type
1,414✔
281
            end
282
          end
283
        end
284
        include Configurables
14✔
285
        include(OpenAPI::Operation)
14✔
286

287
        # @return [JSI::Schema]
288
        def request_schema(media_type: self.request_media_type)
14✔
289
          # TODO typechecking on requestBody & children
290
          request_content = requestBody && requestBody['content']
980✔
291
          return nil unless request_content
980✔
292
          raise(ArgumentError, "please specify media_type for request_schema") unless media_type
308✔
293
          schema = request_content[media_type] && request_content[media_type]['schema']
308✔
294
          return nil unless schema
308✔
295
          JSI::Schema.ensure_schema(schema)
308✔
296
        end
297

298
        # @return [JSI::SchemaSet]
299
        def request_schemas
14✔
300
          JSI::SchemaSet.build do |schemas|
1,484✔
301
            if requestBody && requestBody['content']
1,484✔
302
              requestBody['content'].each_value do |oa_media_type|
322✔
303
                if oa_media_type['schema']
322✔
304
                  schemas << oa_media_type['schema']
322✔
305
                end
306
              end
307
            end
308
          end
309
        end
310

311
        # @return [JSI::Schema]
312
        def response_schema(status: , media_type: )
14✔
313
          oa_response = self.oa_response(status: status)
1,568✔
314
          oa_media_types = oa_response ? oa_response['content'] : nil # Scorpio::OpenAPI::V3_0::MediaTypes
1,568✔
315
          oa_media_type = oa_media_types ? oa_media_types[media_type] : nil # Scorpio::OpenAPI::V3_0::MediaType
1,568✔
316
          oa_schema = oa_media_type ? oa_media_type['schema'] : nil # Scorpio::OpenAPI::V3_0::Schema
1,568✔
317
          oa_schema ? JSI::Schema.ensure_schema(oa_schema) : nil
1,568✔
318
        end
319

320
        # @return [JSI::SchemaSet]
321
        def response_schemas
14✔
322
          JSI::SchemaSet.build do |schemas|
1,120✔
323
            if responses
1,120✔
324
              responses.each_value do |oa_response|
630✔
325
                if oa_response['content']
630✔
326
                  oa_response['content'].each_value do |oa_media_type|
546✔
327
                    if oa_media_type['schema']
546✔
328
                      schemas << oa_media_type['schema']
546✔
329
                    end
330
                  end
331
                end
332
              end
333
            end
334
          end
335
        end
336
      end
337
    end
338

339
    module Operation
14✔
340
      module V2Methods
14✔
341
        module Configurables
14✔
342
          attr_writer :scheme
14✔
343
          def scheme
14✔
344
            return @scheme if instance_variable_defined?(:@scheme)
×
345
            openapi_document.scheme
×
346
          end
347
          def server
14✔
348
            nil
349
          end
350
          def server_variables
14✔
351
            nil
352
          end
353

354
          attr_writer :request_media_type
14✔
355
          def request_media_type
14✔
356
            return @request_media_type if instance_variable_defined?(:@request_media_type)
×
357
            if key?('consumes')
×
358
              Request.best_media_type(consumes)
×
359
            else
360
              openapi_document.request_media_type
×
361
            end
362
          end
363
        end
364
        include Configurables
14✔
365
        include(OpenAPI::Operation)
14✔
366

367
        # the body parameter
368
        # @return [#to_hash]
369
        # @raise [Scorpio::OpenAPI::SemanticError] if there's more than one body param
370
        def body_parameter
14✔
371
          body_parameters = (parameters || []).select { |parameter| parameter['in'] == 'body' }
×
372
          if body_parameters.size == 0
×
373
            nil
374
          elsif body_parameters.size == 1
375
            body_parameters.first
×
376
          else
377
            # TODO blame
378
            raise(OpenAPI::SemanticError, "multiple body parameters on operation #{operation.pretty_inspect.chomp}")
×
379
          end
380
        end
381

382
        # request schema for the given media_type
383
        # @param media_type unused
384
        # @return [JSI::Schema]
385
        def request_schema(media_type: nil)
14✔
386
          if body_parameter && body_parameter['schema']
×
387
            JSI::Schema.ensure_schema(body_parameter['schema'])
×
388
          else
389
            nil
390
          end
391
        end
392

393
        # @return [JSI::SchemaSet]
394
        def request_schemas
14✔
395
          request_schema ? JSI::SchemaSet[request_schema] : JSI::SchemaSet[]
×
396
        end
397

398
        # @param status [Integer, String] response status
399
        # @param media_type unused
400
        # @return [JSI::Schema]
401
        def response_schema(status: , media_type: nil)
14✔
402
          oa_response = self.oa_response(status: status)
×
403
          oa_response_schema = oa_response ? oa_response['schema'] : nil # Scorpio::OpenAPI::V2::Schema
×
404
          oa_response_schema ? JSI::Schema.ensure_schema(oa_response_schema) : nil
×
405
        end
406

407
        # @return [JSI::SchemaSet]
408
        def response_schemas
14✔
409
          JSI::SchemaSet.build do |schemas|
×
410
            if responses
×
411
              responses.each_value do |oa_response|
×
412
                if oa_response['schema']
×
413
                  schemas << oa_response['schema']
×
414
                end
415
              end
416
            end
417
          end
418
        end
419
      end
420
    end
421
  end
422
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