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.
- checksums.yaml +4 -4
- data/bin/openapi-addheaders +8 -7
- data/bin/openapi-addparameters +50 -9
- data/bin/openapi-addresponses +8 -8
- data/bin/openapi-addschemas +10 -9
- data/bin/openapi-checkschemas +16 -15
- data/bin/openapi-frequencies +23 -25
- data/bin/openapi-generate +15 -12
- data/bin/openapi-merge +6 -6
- data/bin/openapi-modifypaths +16 -15
- data/bin/openapi-processpaths +15 -26
- data/lib/openapi/sourcetools/apiobjects.rb +191 -0
- data/lib/openapi/sourcetools/common.rb +82 -0
- data/lib/openapi/sourcetools/config.rb +158 -0
- data/lib/openapi/sourcetools/docs.rb +41 -0
- data/lib/{gen.rb → openapi/sourcetools/gen.rb} +38 -13
- data/lib/openapi/sourcetools/generate.rb +96 -0
- data/lib/openapi/sourcetools/helper.rb +93 -0
- data/lib/openapi/sourcetools/loaders.rb +164 -0
- data/lib/openapi/sourcetools/output.rb +83 -0
- data/lib/openapi/sourcetools/securityschemes.rb +268 -0
- data/lib/openapi/sourcetools/task.rb +137 -0
- data/lib/openapi/sourcetools/version.rb +13 -0
- data/lib/openapi/sourcetools.rb +15 -0
- metadata +42 -18
- data/lib/apiobjects.rb +0 -306
- data/lib/common.rb +0 -114
- data/lib/docs.rb +0 -33
- data/lib/generate.rb +0 -90
- data/lib/helper.rb +0 -94
- data/lib/loaders.rb +0 -96
- data/lib/output.rb +0 -58
- data/lib/task.rb +0 -101
@@ -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
|