openapi-sourcetools 0.7.1 → 0.8.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,96 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Copyright © 2024-2025 Ismo Kärkkäinen
5
+ # Licensed under Universal Permissive License. See LICENSE.txt.
6
+
7
+ require_relative 'common'
8
+ require_relative 'loaders'
9
+ require_relative 'gen'
10
+
11
+
12
+ module OpenAPISourceTools
13
+ def self.executable_bits_on(mode)
14
+ mode = mode.to_s(8).chars
15
+ mode.size.times do |k|
16
+ m = mode[k].to_i(8)
17
+ # Applies to Unix-likes. Other system, check and handle.
18
+ m += 1 unless 3 < mode.size - k || m.zero? || m.odd?
19
+ mode[k] = m
20
+ end
21
+ m = 0
22
+ mode.each do |v|
23
+ m = 8 * m + v
24
+ end
25
+ m
26
+ end
27
+
28
+ # Runs all tasks that generate the results.
29
+ # Used internally by openapi-generate.
30
+ class Generator
31
+ def initialize(document_content, input_name, output_directory, config_prefix)
32
+ Gen.setup(document_content, input_name, output_directory, config_prefix)
33
+ Gen.loaders = Loaders.loaders
34
+ end
35
+
36
+ def context_binding
37
+ binding
38
+ end
39
+
40
+ def load(generator_names)
41
+ generator_names.each do |name|
42
+ idx = Gen.loaders.index { |loader| loader.call(name) }
43
+ return Common.aargh("No loader could handle #{name}", 2) if idx.nil?
44
+ end
45
+ 0
46
+ rescue StandardError => e
47
+ Common.aargh(e.to_s, 2)
48
+ end
49
+
50
+ def generate(t)
51
+ t.generate(context_binding)
52
+ rescue Exception => e
53
+ Common.aargh(e.to_s, 4)
54
+ end
55
+
56
+ def output_name(t, index)
57
+ name = t.output_name
58
+ name = "#{index}.txt" if name.nil?
59
+ File.join(Gen.outdir, name)
60
+ end
61
+
62
+ def save(name, contents, executable)
63
+ d = File.dirname(name)
64
+ FileUtils.mkdir_p(d) unless File.directory?(d)
65
+ f = File.new(name, File::WRONLY | File::CREAT | File::TRUNC)
66
+ s = executable ? f.stat : nil
67
+ f.write(contents)
68
+ f.close
69
+ return unless executable
70
+ mode = OpenAPISourceTools.executable_bits_on(s.mode)
71
+ File.chmod(mode, name) unless mode == s.mode
72
+ end
73
+
74
+ def run
75
+ # This allows tasks to be added while processing.
76
+ # Not intended to be done but might prove handy.
77
+ # Also exposes current task index in case new task is added in the middle.
78
+ Gen.task_index = 0
79
+ while Gen.task_index < Gen.tasks.size
80
+ Gen.t = Gen.tasks[Gen.task_index]
81
+ Gen.task = Gen.t
82
+ out = generate(Gen.t)
83
+ Gen.task_index += 1
84
+ return out if out.is_a?(Integer)
85
+ next if Gen.t.discard || out.empty?
86
+ name = output_name(Gen.t, Gen.task_index - 1)
87
+ begin
88
+ save(name, out, Gen.t.executable)
89
+ rescue StandardError => e
90
+ return Common.aargh("Error writing output file: #{name}\n#{e}", 3)
91
+ end
92
+ end
93
+ 0
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright © 2024-2025 Ismo Kärkkäinen
4
+ # Licensed under Universal Permissive License. See LICENSE.txt.
5
+
6
+ require_relative 'task'
7
+
8
+ module OpenAPISourceTools
9
+ # Helper class supposed to contain helpful methods.
10
+ # Exposed as Gen.h if HelperTask has been run. It is automatically
11
+ # added as the first task but later tasks can remove it.
12
+ class Helper
13
+ attr_reader :doc, :parents
14
+ attr_accessor :parent_parameters
15
+
16
+ # Stores the nearest Hash for each Hash.
17
+ def store_parents(obj, parent = nil)
18
+ if obj.is_a?(Hash)
19
+ @parents[obj] = parent
20
+ obj.each_value do |v|
21
+ store_parents(v, obj)
22
+ end
23
+ elsif obj.is_a?(Array)
24
+ obj.each do |v|
25
+ store_parents(v, parent)
26
+ end
27
+ end
28
+ end
29
+
30
+ def initialize(doc)
31
+ @doc = doc
32
+ @parents = {}.compare_by_identity
33
+ store_parents(@doc)
34
+ end
35
+
36
+ def parent(object)
37
+ @parents[object]
38
+ end
39
+
40
+ COMPONENTS = '#/components/'
41
+
42
+ def category_and_name(ref_or_obj)
43
+ ref = ref_or_obj.is_a?(Hash) ? ref_or_obj['$ref'] : ref_or_obj
44
+ return nil unless ref.is_a?(String)
45
+ return nil unless ref.start_with?(Helper::COMPONENTS)
46
+ idx = ref.index('/', Helper::COMPONENTS.size)
47
+ return nil if idx.nil?
48
+ category = ref[Helper::COMPONENTS.size...idx]
49
+ [ category, ref[(idx + 1)...ref.size] ]
50
+ end
51
+
52
+ def dereference(ref_or_obj)
53
+ cn = category_and_name(ref_or_obj)
54
+ return nil if cn.nil?
55
+ cs = @doc.dig('components', cn.first) || {}
56
+ cs[cn.last]
57
+ end
58
+
59
+ def basename(ref_or_obj)
60
+ cn = category_and_name(ref_or_obj)
61
+ return nil if cn.nil?
62
+ cn.last
63
+ end
64
+
65
+ def parameters(operation_object, empty_unless_local = false)
66
+ return [] if empty_unless_local && !operation_object.key?('parameters')
67
+ cps = @doc.dig('components', 'parameters') || {}
68
+ uniqs = {}
69
+ path_item_object = parent(operation_object)
70
+ [path_item_object, operation_object].each do |p|
71
+ p.fetch('parameters', []).each do |param|
72
+ r = basename(param)
73
+ r = cps[r] if r.is_a?(String)
74
+ uniqs["#{r['name']}:#{r['in']}"] = param
75
+ end
76
+ end
77
+ uniqs.keys.sort!.map { |k| uniqs[k] }
78
+ end
79
+ end
80
+
81
+ # Task class to add an Helper instance to Gen.h, for convenience.
82
+ class HelperTask
83
+ include OpenAPISourceTools::TaskInterface
84
+
85
+ def generate(_context_binding)
86
+ Gen.h = Helper.new(Gen.doc) if Gen.h.nil?
87
+ end
88
+
89
+ def discard
90
+ true
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright © 2024-2025 Ismo Kärkkäinen
4
+ # Licensed under Universal Permissive License. See LICENSE.txt.
5
+
6
+ require_relative 'task'
7
+
8
+
9
+ # Original loader functions. These are accessible via Gen.loaders. New loaders
10
+ # should be added there.
11
+ module OpenAPISourceTools
12
+ # Loaders used to load gems and files and set config etc.
13
+ # Exposed as Gen.loaders if you need to modify the array.
14
+ module Loaders
15
+ # Prefix etc. and loader pairs for all loaders.
16
+
17
+ REQ_PREFIX = 'req:'
18
+
19
+ def self.req_loader(name)
20
+ return false unless name.downcase.start_with?(REQ_PREFIX)
21
+ begin
22
+ t = OpenAPISourceTools::RestoreProcessorStorage.new({})
23
+ Gen.tasks.push(t)
24
+ base = name.slice(REQ_PREFIX.size...name.size)
25
+ require(base)
26
+ Gen.config = nil
27
+ t.x = Gen.x # In case setup code replaced the object.
28
+ rescue LoadError => e
29
+ raise StandardError, "Failed to require #{name}\n#{e}"
30
+ rescue Exception => e
31
+ raise StandardError, "Problem with #{name}\n#{e}\n#{e.backtrace.join("\n")}"
32
+ end
33
+ true
34
+ end
35
+
36
+ REREQ_PREFIX = 'rereq:'
37
+
38
+ def self.rereq_loader(name)
39
+ return false unless name.downcase.start_with?(REREQ_PREFIX)
40
+ begin
41
+ t = OpenAPISourceTools::RestoreProcessorStorage.new({})
42
+ Gen.tasks.push(t)
43
+ code = name.slice(REREQ_PREFIX.size...name.size)
44
+ eval(code)
45
+ Gen.config = nil
46
+ t.x = Gen.x # In case setup code replaced the object.
47
+ rescue LoadError => e
48
+ raise StandardError, "Failed to require again #{name}\n#{e}"
49
+ rescue Exception => e
50
+ raise StandardError, "Problem with #{name}\n#{e}\n#{e.backtrace.join("\n")}"
51
+ end
52
+ true
53
+ end
54
+
55
+ RUBY_EXT = '.rb'
56
+
57
+ def self.ruby_loader(name)
58
+ return false unless name.downcase.end_with?(RUBY_EXT)
59
+ origwd = Dir.pwd
60
+ d = File.dirname(name)
61
+ Dir.chdir(d) unless d == '.'
62
+ begin
63
+ t = OpenAPISourceTools::RestoreProcessorStorage.new({})
64
+ Gen.tasks.push(t)
65
+ base = File.basename(name)
66
+ Gen.config = base[0..-4] if Gen.config.nil?
67
+ require(File.join(Dir.pwd, base))
68
+ Gen.config = nil
69
+ t.x = Gen.x # In case setup code replaced the object.
70
+ rescue LoadError => e
71
+ raise StandardError, "Failed to require #{name}\n#{e}"
72
+ rescue Exception => e
73
+ raise StandardError, "Problem with #{name}\n#{e}\n#{e.backtrace.join("\n")}"
74
+ end
75
+ Dir.chdir(origwd) unless d == '.'
76
+ true
77
+ end
78
+
79
+ YAML_PREFIX = 'yaml:'
80
+ YAML_EXTS = [ '.yaml', '.yml' ].freeze
81
+
82
+ def self.yaml_loader(name)
83
+ d = name.downcase
84
+ if d.start_with?(YAML_PREFIX)
85
+ name = name.slice(YAML_PREFIX.size...name.size)
86
+ elsif (YAML_EXTS.index { |s| d.end_with?(s) }).nil?
87
+ return false
88
+ end
89
+ n, _sep, f = name.partition(':')
90
+ raise StandardError, 'No name given.' if n.empty?
91
+ raise StandardError, 'No filename given.' if f.empty?
92
+ doc = YAML.safe_load_file(f)
93
+ raise StandardError, "#{name} #{n} exists already." unless Gen.d.add(n, doc)
94
+ true
95
+ rescue Errno::ENOENT
96
+ raise StandardError, "Not found: #{f}\n#{e}"
97
+ rescue Exception => e # Whatever was raised, we want it.
98
+ raise StandardError, "Failed to read as YAML: #{f}\n#{e}"
99
+ end
100
+
101
+ BIN_PREFIX = 'bin:'
102
+
103
+ def self.bin_loader(name)
104
+ return false unless name.downcase.start_with?(BIN_PREFIX)
105
+ n, _sep, f = name.slice(BIN_PREFIX.size...name.size).partition(':')
106
+ raise StandardError, 'No name given.' if n.empty?
107
+ raise StandardError, 'No filename given.' if f.empty?
108
+ doc = File.binread(f)
109
+ raise StandardError, "#{name} #{n} exists already." unless Gen.d.add(n, doc)
110
+ true
111
+ rescue Errno::ENOENT
112
+ raise StandardError, "Not found: #{f}\n#{e}"
113
+ rescue Exception => e # Whatever was raised, we want it.
114
+ raise StandardError, "Failed to read #{f}\n#{e}"
115
+ end
116
+
117
+ CONFIG_PREFIX = 'config:'
118
+
119
+ def self.config_loader(name)
120
+ return false unless name.downcase.start_with?(CONFIG_PREFIX)
121
+ raise StandardError, "Config name remains: #{Gen.config}" unless Gen.config.nil?
122
+ n = name.slice(CONFIG_PREFIX.size...name.size)
123
+ raise StandardError, 'No name given.' if n.empty?
124
+ # Interpretation left completely to config loading.
125
+ Gen.config = n
126
+ true
127
+ end
128
+
129
+ SEPARATOR_PREFIX = 'separator:'
130
+
131
+ def self.separator_loader(name)
132
+ return false unless name.downcase.start_with?(SEPARATOR_PREFIX)
133
+ n = name.slice(SEPARATOR_PREFIX.size...name.size)
134
+ n = nil if n.empty?
135
+ Gen.separator = n
136
+ true
137
+ end
138
+
139
+ def self.loaders
140
+ [
141
+ method(:req_loader),
142
+ method(:rereq_loader),
143
+ method(:ruby_loader),
144
+ method(:yaml_loader),
145
+ method(:bin_loader),
146
+ method(:config_loader),
147
+ method(:separator_loader)
148
+ ]
149
+ end
150
+
151
+ def self.document
152
+ <<EOB
153
+ - #{Loaders::REQ_PREFIX}req_name : requires the gem.
154
+ - #{Loaders::REREQ_PREFIX}code : runs code to add gem tasks again.
155
+ - ruby_file#{Loaders::RUBY_EXT} : changes to Ruby file directory and requires the file.
156
+ - #{Loaders::YAML_PREFIX}name:filename : Loads YAML file into Gen.d.name.
157
+ - name:filename.{#{(Loaders::YAML_EXTS.map { |s| s[1...s.size] }).join('|')}} : Loads YAML file into Gen.d.name.
158
+ - #{Loaders::BIN_PREFIX}name:filename : Loads binary file into Gen.d.name.
159
+ - #{Loaders::CONFIG_PREFIX}name : Sets Gen.config for next gem/Ruby file configuration loading.
160
+ - #{Loaders::SEPARATOR_PREFIX}string : Sets Gen.separator to string.
161
+ EOB
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright © 2024-2025 Ismo Kärkkäinen
4
+ # Licensed under Universal Permissive License. See LICENSE.txt.
5
+
6
+
7
+ module OpenAPISourceTools
8
+ # Output configuration settings for easy storage.
9
+ # You can have it in configuration and pass hash to initialize.
10
+ class OutputConfiguration
11
+ attr_reader :indent_character, :indent_step
12
+ attr_reader :tab, :tab_replaces_count
13
+
14
+ def initialize(cfg = {})
15
+ @indent_character = cfg['indent_character'] || ' '
16
+ @indent_step = cfg['indent_step'] || 4
17
+ @tab = cfg['tab'] || "\t"
18
+ @tab_replaces_count = cfg['tab_replaces_count'] || 0
19
+ end
20
+ end
21
+
22
+ # Output indentation helper class.
23
+ # Exposed as Gen.output for use from templates.
24
+ class Output
25
+ attr_reader :config
26
+ attr_accessor :last_indent
27
+
28
+ def initialize(cfg = OutputConfiguration.new)
29
+ @config = cfg
30
+ @last_indent = 0
31
+ end
32
+
33
+ def config=(cfg)
34
+ cfg = OutputConfiguration.new(cfg) if cfg.is_a?(Hash)
35
+ raise ArgumentError, "Expected OutputConfiguration or Hash, got #{cfg.class}" unless cfg.is_a?(OutputConfiguration)
36
+ @config = cfg
37
+ @last_indent = 0
38
+ end
39
+
40
+ # Takes an array of code blocks/lines or integers/booleans and produces
41
+ # indented output using the separator character.
42
+ # Set class attributes to obtain desired outcome.
43
+ def join(blocks, separator = "\n")
44
+ indented = []
45
+ blocks.flatten!
46
+ indent = 0
47
+ blocks.each do |block|
48
+ if block.nil?
49
+ indent = 0
50
+ elsif block.is_a?(Integer)
51
+ indent += block
52
+ elsif block.is_a?(TrueClass)
53
+ indent += @config.indent_step
54
+ elsif block.is_a?(FalseClass)
55
+ indent -= @config.indent_step
56
+ else
57
+ block = block.to_s unless block.is_a?(String)
58
+ if block.empty?
59
+ indented.push('')
60
+ next
61
+ end
62
+ if indent.zero?
63
+ indented.push(block)
64
+ next
65
+ end
66
+ if @config.tab_replaces_count.positive?
67
+ tabs = @config.tab * (indent / @config.tab_replaces_count)
68
+ chars = @config.indent_character * (indent % @config.tab_replaces_count)
69
+ else
70
+ tabs = ''
71
+ chars = @config.indent_character * indent
72
+ end
73
+ lines = block.lines(chomp: true)
74
+ lines.each do |line|
75
+ indented.push("#{tabs}#{chars}#{line}")
76
+ end
77
+ end
78
+ end
79
+ @last_indent = indent
80
+ indented.join(separator)
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,268 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright © 2024-2025 Ismo Kärkkäinen
4
+ # Licensed under Universal Permissive License. See LICENSE.txt.
5
+
6
+ require_relative 'task'
7
+ require 'yaml'
8
+
9
+
10
+ module OpenAPISourceTools
11
+ # Class that contains security scheme objects and what headers
12
+ # and parameters are added to the request when the scheme is used.
13
+ class SecuritySchemeInfo
14
+ include Comparable
15
+
16
+ attr_reader :headers, :parameters, :query_parameters, :cookies
17
+
18
+ def initialize(security_scheme = {}, scheme_templates = [])
19
+ @headers = {}
20
+ @parameters = {}
21
+ @query_parameters = {}
22
+ @cookies = {}
23
+ scheme_templates.each do |template|
24
+ s = template['scheme']
25
+ match = true
26
+ s.each do |k, v|
27
+ unless v == security_scheme[k]
28
+ match = false
29
+ break
30
+ end
31
+ end
32
+ next unless match
33
+ o = template['output']
34
+ fill_in(@headers, o['headers'] || {}, security_scheme)
35
+ fill_in(@parameters, o['parameters'] || {}, security_scheme)
36
+ fill_in(@query_parameters, o['query_parameters'] || {}, security_scheme)
37
+ fill_in(@cookies, o['cookies'] || {}, security_scheme)
38
+ break
39
+ end
40
+ end
41
+
42
+ def fill_in(output, source, scheme)
43
+ source.each do |k, v|
44
+ if k.start_with?('<') && k.end_with?('>')
45
+ scheme_key = k[1..-2]
46
+ scheme_value = scheme[scheme_key]
47
+ raise "Missing security scheme value for #{scheme_key}" if scheme_value.nil?
48
+ output[scheme_value] = v
49
+ else
50
+ output[k] = v
51
+ end
52
+ end
53
+ end
54
+
55
+ def merge!(other)
56
+ @headers.merge!(other.headers)
57
+ @parameters.merge!(other.parameters)
58
+ @query_parameters.merge!(other.query_parameters)
59
+ @cookies.merge!(other.cookies)
60
+ end
61
+
62
+ def merge(other)
63
+ out = SecuritySchemeInfo.new({}, [])
64
+ out.merge!(self)
65
+ out.merge!(other)
66
+ out
67
+ end
68
+
69
+ def <=>(other)
70
+ # Only really interested in equality.
71
+ @headers <=> other.headers || @parameters <=> other.parameters || @query_parameters <=> other.query_parameters || @cookies <=> other.cookies
72
+ end
73
+ end
74
+
75
+ class ScopedSecuritySchemeInfo
76
+ include Comparable
77
+
78
+ attr_reader :ssi, :scopes
79
+
80
+ def initialize(ssi, scopes)
81
+ @ssi = ssi
82
+ @scopes = scopes
83
+ end
84
+
85
+ def merge(other)
86
+ @ssi = @ssi.merge(other.ssi)
87
+ @scopes = @scopes.concat(other.scopes).uniq
88
+ end
89
+
90
+ def <=>(other)
91
+ # Only really interested in equality.
92
+ @ssi <=> other.ssi || @scopes <=> other.scopes
93
+ end
94
+ end
95
+
96
+ # Helper class for dealing with securitySchemes.
97
+ # Adds :security key to each operation object and to root.
98
+ # They contain the used securitySchemes in use and the security schemes
99
+ # in effect for that operation.
100
+ class SecuritySchemesTask
101
+ include TaskInterface
102
+
103
+ def default_configuration
104
+ # type apiKey, name, in.
105
+ # type http, scheme, bearerFormat.
106
+ # type mutualTLS, beyond the scope of code templates.
107
+ # type oauth2, flows object.
108
+ # type openIdConnect, openIdConnectUrl. More for login?
109
+ YAML.safe_load(%q(---
110
+ security_schemes:
111
+ - scheme:
112
+ type: apiKey
113
+ in: header
114
+ output:
115
+ headers:
116
+ '<name>': string
117
+ - scheme:
118
+ type: apiKey
119
+ in: query
120
+ output:
121
+ parameters:
122
+ '<name>': string
123
+ - scheme:
124
+ type: apiKey
125
+ in: query
126
+ output:
127
+ query_parameters:
128
+ '<name>': string
129
+ - scheme:
130
+ type: apiKey
131
+ in: cookie
132
+ output:
133
+ cookies:
134
+ '<name>': true
135
+ - scheme:
136
+ type: http
137
+ scheme: basic
138
+ output:
139
+ headers:
140
+ Authorization: string
141
+ - scheme:
142
+ type: http
143
+ scheme: bearer
144
+ output:
145
+ headers:
146
+ Authorization: string
147
+ - scheme:
148
+ type: mutualTLS
149
+ output: {}
150
+ - scheme:
151
+ type: oauth2
152
+ output:
153
+ headers:
154
+ Authorization: string
155
+ - scheme:
156
+ type: openIdConnect
157
+ output:
158
+ headers:
159
+ Authorization: string
160
+ ))
161
+ end
162
+
163
+ def convert_security_schemes(doc)
164
+ ss = doc.dig('components', 'securitySchemes')
165
+ return nil if ss.nil?
166
+ # Should create unique objects. Different name may lead to same object.
167
+ out = {}
168
+ ss.each do |name, security_scheme|
169
+ out[name] = SecuritySchemeInfo.new(security_scheme)
170
+ end
171
+ out
172
+ end
173
+
174
+ def operation_object_security(doc, schemes)
175
+ # Find all operation objects and security in effect.
176
+ all_ops = []
177
+ seen_secs = Set.new
178
+ root_sec = doc['security'] || []
179
+ not_method = %w[parameters servers summary description]
180
+ doc['paths'].each_value do |path_item|
181
+ path_sec = path_item['security'] || root_sec
182
+ path_item.each do |method, op|
183
+ next if not_method.include?(method)
184
+ op_sec = op['security'] || path_sec
185
+ all_ops.push([ op, op_sec ])
186
+ op_sec.each do |security_requirement|
187
+ names = security_requirement.keys
188
+ seen_secs.merge(names)
189
+ if names.empty? && !schemes.key?('')
190
+ schemes[''] = SecuritySchemeInfo.new
191
+ end
192
+ end
193
+ end
194
+ end
195
+ # Check that all seen secs names have a scheme. Report all in one place.
196
+ missing = false
197
+ seen_secs.to_a.sort!.each do |name|
198
+ unless schemes.key?(name)
199
+ missing = true
200
+ warn("#/components/securitySchemes is missing: #{name}")
201
+ end
202
+ end
203
+ return 1 if missing
204
+ # Now we know all individual parts are available.
205
+ # Map security arrays of objects to arrays of arrays.
206
+ all_scopeds = [] # For having just one instance for unique data combination.
207
+ all_ops.each do |pair|
208
+ sec = pair.second.map do |sec_req|
209
+ keys = sec_req.keys.sort!
210
+ values = keys.map { |name| sec_req[name].sort! }
211
+ if keys.empty?
212
+ keys = [ '' ]
213
+ values = [ [] ]
214
+ end
215
+ s3is = []
216
+ keys.size.times do |idx|
217
+ name = keys[idx]
218
+ scopes = values[idx]
219
+ s3i = ScopedSecuritySchemeInfo.new(schemes[name], scopes)
220
+ idx = all_scopeds.index(s3i)
221
+ if idx.nil?
222
+ all_scopeds.push(s3i)
223
+ else
224
+ s3i = all_scopeds(idx) # Use the same instance everywhere.
225
+ end
226
+ s3is.push(s3i)
227
+ end
228
+ s3is
229
+ end
230
+ pair.first[:security] = sec # Arrays of ScopedSecuritySchemeInfo.
231
+ # When individual objects are not needed, provide merged together items.
232
+ all_merged = []
233
+ pair.first[:security_merged] = sec.map do |s3is| # ScopedSecuritySchemeInfos.
234
+ m = s3is.first
235
+ s3is[1..].each do |s3i|
236
+ m = m.merge(s3i)
237
+ end
238
+ idx = all_merged.index(m)
239
+ if idx.nil?
240
+ all_merged.push(m)
241
+ else
242
+ m = all_merged[idx] # Use the same instance everywhere.
243
+ end
244
+ m
245
+ end
246
+ end
247
+ all_merged.map(&:ssi).uniq.sort!
248
+ end
249
+
250
+ def generate(_context_binding)
251
+ # Get security_schemes from config, append defaults to it.
252
+ scheme_templates = gen.configuration['security_schemes'] || []
253
+ scheme_templates.concat(default_configuration['security_schemes'])
254
+ simple_schemes = convert_security_schemes(Gen.doc)
255
+ # For all operation objects, add :security array with all used schemes.
256
+ merged_schemes = operation_object_security(Gen.doc, simple_schemes || {})
257
+ return merged_schemes if merged_schemes.is_a?(Integer)
258
+ Gen.doc[:securitySchemes] = simple_schemes unless simple_schemes.nil?
259
+ Gen.doc[:securitySchemes_merged] = merged_schemes unless merged_schemes.empty?
260
+ end
261
+
262
+ def discard
263
+ true
264
+ end
265
+
266
+ COMPONENTS = '#/components/'
267
+ end
268
+ end