openapi-sourcetools 0.8.0 → 0.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 783a57d4ca224bb9710cb523d3ee2fd2dee0e821cb7382ffc32fa9b8cb36b4f1
4
- data.tar.gz: b7c02ac6cd2e30fc4f9f5d9fb87bfcd84b8f60b76007e82d82a1a1bfb7f37e42
3
+ metadata.gz: 44ff7d64b4781fdfbab07060f6faa294a8092e342530e6cde786e50ff8f69db1
4
+ data.tar.gz: 893e593407c4734e67c83bffe77a67bb55b2ecef3970620a416580fd33109437
5
5
  SHA512:
6
- metadata.gz: eec2859f712324b85f9db32f5c92d4e3947a80021f9bf920f287f9b01a2a56ab7d20e3f2b5d30e039a2e606e57b4ca0f0b5872961e91f1898c44c402eade14ef
7
- data.tar.gz: 72bbafa47688fd1eb3dd78d1ff0782d8e1a3510e541e4ad4f4e49ceec056cedaf9c6ebbbbe4dd9c7c6b446f3c0dd4c22d54a23afb0bacfa8f5dea33fe751e55a
6
+ metadata.gz: 741c12063a6d5df5d090f26b80c60a7810083537076573cb10f9ba5a3ca3afc8109ab6faa305707139c881a7efa1f478d7d0de562db67b44f93c0329ae8263e4
7
+ data.tar.gz: 9e1c2bc87442258d75517beb13f99c1e0b210b7074ee622db8057e1e4ebf17c253538ba52b893b0f148baa21188a306976a9783d778e92dd769caee6eb84aef6
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Copyright © 2025 Ismo Kärkkäinen
5
+ # Licensed under Universal Permissive License. See LICENSE.txt.
6
+
7
+ require_relative '../lib/openapi/sourcetools/apiobjects'
8
+ require_relative '../lib/openapi/sourcetools/common'
9
+ require 'optparse'
10
+ include OpenAPISourceTools
11
+
12
+
13
+ def add_combinations(combinations, security)
14
+ security.each do |s|
15
+ subset = s.keys.sort!
16
+ next if subset.empty?
17
+ combinations.add(subset)
18
+ end
19
+ end
20
+
21
+ def gather_security(doc)
22
+ combinations = Set.new
23
+ security = doc.fetch('security', [])
24
+ add_combinations(combinations, security)
25
+ operation_objects = []
26
+ doc.fetch('paths', {}).each_value do |item|
27
+ oos = ApiObjects.operation_objects(item).values
28
+ oos.each do |oo|
29
+ add_combinations(combinations, oo['security'] || [])
30
+ operation_objects.push(oo)
31
+ end
32
+ end
33
+ [security, operation_objects, combinations]
34
+ end
35
+
36
+ def check_security(doc, combinations)
37
+ singles = Set.new
38
+ combinations.each do |c|
39
+ c.each do |s|
40
+ singles.add(s)
41
+ end
42
+ end
43
+ available = Set.new((doc.dig('components', 'securitySchemes') || {}).keys)
44
+ missing = false
45
+ singles.each do |s|
46
+ unless available.include?(s)
47
+ $stderr.puts "Security scheme unavailable: #{s}"
48
+ missing = true
49
+ end
50
+ end
51
+ !missing
52
+ end
53
+
54
+ def namelist2security(item)
55
+ item.keys.sort!.join(' ')
56
+ end
57
+
58
+ def security2hash(security)
59
+ out = {}
60
+ security.each { |s| out[namelist2security(s)] = s }
61
+ out
62
+ end
63
+
64
+ def add_security_to_operations(operation_objects, top_level_security)
65
+ operation_objects.each do |oo|
66
+ sec = security2hash(oo['security'] || top_level_security)
67
+ oo['security'] = sec.keys.sort!.map { |k| Marshal.load(Marshal.dump(sec[k])) }
68
+ end
69
+ end
70
+
71
+ def main
72
+ input_name = nil
73
+ output_name = nil
74
+
75
+ parser = OptionParser.new do |opts|
76
+ opts.summary_indent = ' '
77
+ opts.summary_width = 26
78
+ opts.banner = 'Usage: openapi-addsecurityschemes [options]'
79
+ opts.separator ''
80
+ opts.separator 'Options:'
81
+ opts.on('-i', '--input FILE', 'Read API spec from FILE, not stdin.') do |f|
82
+ input_name = f
83
+ end
84
+ opts.on('-o', '--output FILE', 'Output to FILE, not stdout.') do |f|
85
+ output_name = f
86
+ end
87
+ opts.on('-h', '--help', 'Print this help and exit.') do
88
+ $stdout.puts %(#{opts}
89
+
90
+ Loads API document in OpenAPI format and moves security schemes under components and
91
+ replaces the original with reference.
92
+ )
93
+ exit 0
94
+ end
95
+ end
96
+ parser.order!
97
+
98
+ doc = Common.load_source(input_name)
99
+ return 2 if doc.nil?
100
+
101
+ security, operation_objects, combinations = gather_security(doc)
102
+ return 4 unless check_security(doc, combinations)
103
+ add_security_to_operations(operation_objects, security)
104
+
105
+ Common.dump_result(output_name, doc, 3)
106
+ end
107
+
108
+ exit(main) unless defined?($unit_test)
data/bin/openapi-generate CHANGED
@@ -18,7 +18,7 @@ def main
18
18
 
19
19
  parser = OptionParser.new do |opts|
20
20
  opts.summary_indent = ' '
21
- opts.summary_width = 26
21
+ opts.summary_width = 20
22
22
  opts.banner = 'Usage: openapi-generate [options] generator-names...'
23
23
  opts.separator ''
24
24
  opts.separator 'Options:'
@@ -37,7 +37,7 @@ Loads API document in OpenAPI format and generator names. Built-in generator
37
37
  or additional document loaders accept the following:
38
38
  #{OpenAPISourceTools::Loaders.document.strip}
39
39
 
40
- During load each generator can add and modify tasks via Gen module:
40
+ During load each generator can add and modify tasks via Gen singleton:
41
41
  #{Gen.document.strip}
42
42
 
43
43
  After all generators have loaded succesfully, tasks are run.
@@ -48,10 +48,10 @@ After all generators have loaded succesfully, tasks are run.
48
48
  parser.order!
49
49
 
50
50
  return OpenAPISourceTools::Common.aargh('Generator names must be given.', 1) if ARGV.empty?
51
+ return OpenAPISourceTools::Common.aargh("Not a directory: #{output_dir}", 3) unless File.directory?(output_dir)
51
52
 
52
53
  a = OpenAPISourceTools::Common.load_source(input_name)
53
54
  return 2 if a.nil?
54
- return OpenAPISourceTools::Common.aargh("Not a directory: #{output_dir}", 3) unless File.directory?(output_dir)
55
55
  gen = OpenAPISourceTools::Generator.new(a, input_name, output_dir, config_prefix)
56
56
  ec = gen.load(ARGV)
57
57
  return ec unless ec.zero?
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Copyright © 2025 Ismo Kärkkäinen
5
+ # Licensed under Universal Permissive License. See LICENSE.txt.
6
+
7
+ require_relative '../lib/openapi/sourcetools/common'
8
+ require 'optparse'
9
+ include OpenAPISourceTools
10
+
11
+ def key(item)
12
+ "#{item['pattern']}::#{item.fetch('minLength', 0)}::#{item.fetch('maxLength', 'inf')}"
13
+ end
14
+
15
+ def find_patterns(doc, pmm)
16
+ if doc.is_a?(Hash)
17
+ if doc.key?('pattern')
18
+ item = { 'pattern' => doc['pattern'] }
19
+ item['minLength'] = doc['minLength'] if doc.key?('minLength')
20
+ item['maxLength'] = doc['maxLength'] if doc.key?('maxLength')
21
+ pmm[key(doc)] = item
22
+ return
23
+ end
24
+ doc = doc.values
25
+ end
26
+ doc.each { |v| find_patterns(v, pmm) } if doc.is_a?(Array)
27
+ end
28
+
29
+ def pattern_list2hash(list)
30
+ pmms = {}
31
+ list.each { |item| pmms[key(item)] = item }
32
+ pmms
33
+ end
34
+
35
+ def add_strings(item)
36
+ passes = []
37
+ fails = []
38
+ min_len = item.fetch('minLength', 0)
39
+ fails.push('f' * (min_len - 1)) unless min_len.zero?
40
+ max_len = item['maxLength']
41
+ fails.push('f' * (max_len + 1)) unless max_len.nil?
42
+ # Fails within length limits require considering the pattern.
43
+ # All passes require considering the pattern.
44
+ item['pass'] = passes
45
+ item['fail'] = fails
46
+ end
47
+
48
+ def merge_arrays(current, past)
49
+ if current.is_a?(Array)
50
+ if past.is_a?(Array)
51
+ return current.concat(past)
52
+ end
53
+ current
54
+ elsif past.is_a?(Array)
55
+ past
56
+ else
57
+ false
58
+ end
59
+ end
60
+
61
+ def merge_existing(pmms, existing)
62
+ pmms.each do |k, v|
63
+ ex = existing[k]
64
+ next if ex.nil?
65
+ %w[pass fail].each do |arr|
66
+ m = merge_arrays(v[arr], ex[arr])
67
+ v[arr] = m.is_a?(Array) ? m.sort!.uniq : m
68
+ end
69
+ ex.each do |ek, ev|
70
+ v[ek] = ev unless v.key?(ek)
71
+ end
72
+ end
73
+ end
74
+
75
+ def add_removed(pmms, existing)
76
+ existing.each { |k, v| pmms[k] = v unless pmms.key?(k) }
77
+ end
78
+
79
+ def compare(a, b)
80
+ d = a['pattern'] <=> b['pattern']
81
+ return d unless d.zero?
82
+ d = a.fetch('minLength', 0) <=> b.fetch('minLength', 0)
83
+ return d unless d.zero?
84
+ a.fetch('maxLength', Float::INFINITY) <=> b.fetch('maxLength', Float::INFINITY)
85
+ end
86
+
87
+ def pattern_hash2list(pmms)
88
+ pmms.values.sort { |a, b| compare(a, b) }
89
+ end
90
+
91
+ def main
92
+ array_name = 'patterns'
93
+ input_name = nil
94
+ output_name = nil
95
+ source_tests = nil
96
+ keep = false
97
+ chain = []
98
+
99
+ parser = OptionParser.new do |opts|
100
+ opts.summary_indent = ' '
101
+ opts.summary_width = 20
102
+ opts.banner = 'Usage: openapi-patterntests [options]'
103
+ opts.separator ''
104
+ opts.separator 'Options:'
105
+ opts.on('-i', '--input FILE', 'Read API spec from FILE, not stdin.') do |f|
106
+ input_name = f
107
+ end
108
+ opts.on('-o', '--output FILE', 'Output to FILE, not stdout.') do |f|
109
+ output_name = f
110
+ end
111
+ opts.on('-t', '--tests FILE', 'Read existing tests from FILE.') do |f|
112
+ source_tests = f
113
+ end
114
+ opts.on('-u', '--under STR', %(Top-level "#{array_name}" is under dot-separated keys.)) do |s|
115
+ chain = s.split('.').reject(&:empty?)
116
+ end
117
+ opts.on('-k', '--[no-]keep', "Keep missing test patterns, default = #{Common.yesno(keep)}.") do |b|
118
+ keep = b
119
+ end
120
+ opts.on('-h', '--help', 'Print this help and exit.') do
121
+ $stdout.puts %(#{opts}
122
+
123
+ Loads API document in OpenAPI format, extracts string patterns, and outputs
124
+ a YAML file that contains mapping from patterns to matching and not mathcing
125
+ strings for testing generated code.
126
+ )
127
+ exit 0
128
+ end
129
+ end
130
+ parser.order!
131
+
132
+ doc = Common.load_source(input_name)
133
+ return 2 if doc.nil?
134
+
135
+ if source_tests.nil?
136
+ ex = {}
137
+ pats = []
138
+ else
139
+ ex = Common.load_source(source_tests)
140
+ return 2 if ex.nil?
141
+ parent = chain.empty? ? ex : ex.dig(*chain)
142
+ return Common.aargh("Key chain #{chain.join('.')} not found in source tests.", 4) if parent.nil?
143
+ pats = parent[array_name] || []
144
+ end
145
+ chain.push(array_name)
146
+
147
+ existing = pattern_list2hash(pats)
148
+ pmms = {}
149
+ find_patterns(doc, pmms)
150
+ pmms.each_value { |item| add_strings(item) }
151
+ merge_existing(pmms, existing)
152
+ add_removed(pmms, existing) if keep
153
+ pats = pattern_hash2list(pmms)
154
+
155
+ Common.bury(ex, chain, pats)
156
+ Common.dump_result(output_name, ex, 3)
157
+ end
158
+
159
+ exit(main) if defined?($unit_test).nil?
@@ -187,5 +187,69 @@ module OpenAPISourceTools
187
187
  end
188
188
  out
189
189
  end
190
+
191
+ # Single server variable object.
192
+ class ServerVariableObject
193
+ include Comparable
194
+
195
+ attr_reader :name, :default, :enum
196
+
197
+ def initialize(name, variable_object)
198
+ @name = name
199
+ @default = variable_object['default']
200
+ @enum = (variable_object['enum'] || []).sort!
201
+ end
202
+
203
+ def <=>(other)
204
+ d = @name <=> other.name
205
+ return d unless d.zero?
206
+ d = @default <=> other.default
207
+ return d unless d.zero?
208
+ @enum <=> other.enum
209
+ end
210
+ end
211
+
212
+ # Single server object with variables.
213
+ class ServerObject
214
+ include Comparable
215
+
216
+ attr_reader :url, :variables
217
+
218
+ def initialize(server_object)
219
+ @url = server_object['url']
220
+ vs = server_object['variables'] || {}
221
+ @variables = vs.keys.sort!.map do |name|
222
+ obj = vs[name]
223
+ ServerVariableObject.new(name, obj)
224
+ end
225
+ end
226
+
227
+ def <=>(other)
228
+ d = @url <=> other.url
229
+ return d unless d.zero?
230
+ if @variables.nil? || other.variables.nil?
231
+ return -1 if @variables.nil?
232
+ return 1 if other.variables.nil?
233
+ end
234
+ @variables <=> other.variables
235
+ end
236
+ end
237
+
238
+ # Combines servers array with set identifier.
239
+ class ServerAlternatives
240
+ include Comparable
241
+
242
+ attr_reader :servers
243
+ attr_accessor :set_id
244
+
245
+ def initialize(server_objects)
246
+ @servers = server_objects.map { |so| ServerObject.new(so) }
247
+ @servers.sort!
248
+ end
249
+
250
+ def <=>(other)
251
+ @servers <=> other.servers
252
+ end
253
+ end
190
254
  end
191
255
  end
@@ -9,97 +9,77 @@ require_relative 'docs'
9
9
  require_relative 'output'
10
10
  require_relative 'config'
11
11
  require 'deep_merge'
12
-
12
+ require 'singleton'
13
13
 
14
14
  # The generation module that contains things visible to tasks.
15
- module Gen
15
+ class Gen
16
+ include Singleton
17
+
18
+ @docsrc = []
16
19
  def self.add_doc(symbol, docstr)
17
- return if docstr.nil?
18
- @docsrc = [] unless instance_variable_defined?('@docsrc')
19
20
  @docsrc.push("- #{symbol} : #{docstr}")
20
21
  end
21
22
  private_class_method :add_doc
22
23
 
23
- def self.read_attr(symbol, default)
24
- return if symbol.nil?
24
+ def self.attrib_reader(symbol, docstr, initial_value = nil)
25
+ add_doc(symbol, docstr)
25
26
  attr_reader(symbol)
26
- module_function(symbol)
27
- instance_variable_set("@#{symbol}", default)
27
+ instance_eval("def #{symbol}; Gen.instance.#{symbol}; end")
28
+ Gen.instance.instance_variable_set("@#{symbol}", initial_value) unless initial_value.nil?
28
29
  end
29
- private_class_method :read_attr
30
+ private_class_method :attrib_reader
30
31
 
31
- def self.mod_attr2_reader(symbol, symbol2, docstr = nil, default = nil)
32
- read_attr(symbol, default)
33
- read_attr(symbol2, default)
32
+ def self.attrib_accessor(symbol, docstr, initial_value = nil)
34
33
  add_doc(symbol, docstr)
35
- end
36
- private_class_method :mod_attr2_reader
37
-
38
- def self.mod_attr_reader(symbol, docstr = nil, default = nil)
39
- mod_attr2_reader(symbol, nil, docstr, default)
40
- end
41
- private_class_method :mod_attr_reader
42
-
43
- def self.rw_attr(symbol, default)
44
34
  attr_accessor(symbol)
45
- module_function(symbol)
46
- s = symbol.to_s
47
- module_function((s + '=').to_sym)
48
- instance_variable_set("@#{s}", default)
35
+ instance_eval("def #{symbol}; Gen.instance.#{symbol}; end")
36
+ instance_eval("def #{symbol}=(v); Gen.instance.#{symbol} = v; end")
37
+ Gen.instance.instance_variable_set("@#{symbol}", initial_value) unless initial_value.nil?
49
38
  end
50
- private_class_method :rw_attr
51
-
52
- def self.mod_attr2_accessor(symbol, symbol2, docstr = nil, default = nil)
53
- rw_attr(symbol, default)
54
- rw_attr(symbol2, default) unless symbol2.nil?
55
- add_doc(symbol, docstr)
56
- end
57
- private_class_method :mod_attr2_accessor
58
-
59
- def self.mod_attr_accessor(symbol, docstr = nil, default = nil)
60
- mod_attr2_accessor(symbol, nil, docstr, default)
61
- end
62
- private_class_method :mod_attr_accessor
63
-
64
- mod_attr_reader :doc, 'OpenAPI document.'
65
- mod_attr_reader :outdir, 'Output directory name.'
66
- mod_attr_reader :d, 'Other documents object.', OpenAPISourceTools::Docs.new
67
- mod_attr_reader :wd, 'Original working directory', Dir.pwd
68
- mod_attr_reader :configuration, 'Generator internal configuration'
69
- mod_attr_accessor :config, 'Configuration file name for next gem or Ruby file.'
70
- mod_attr_accessor :separator, 'Key separator in config file names.', nil
71
- mod_attr_accessor :in_name, 'OpenAPI document name, nil if stdin.'
72
- mod_attr_accessor :in_basename, 'OpenAPI document basename, nil if stdin.'
73
- mod_attr_reader :g, 'Hash for storing values visible to all tasks.', {}
74
- mod_attr_accessor :x, 'Hash for storing values visible to tasks from processor.', {}
75
- mod_attr_accessor :h, 'Instance of class with helper methods.'
76
- mod_attr_accessor :tasks, 'Tasks array.', []
77
- mod_attr2_accessor :task, :t, 'Current task instance.'
78
- mod_attr_accessor :task_index, 'Current task index.'
79
- mod_attr_accessor :loaders, 'Array of processor loader methods.', []
80
- mod_attr_accessor :output, 'Output-formatting helper.', OpenAPISourceTools::Output.new
81
-
82
- def self.load_config(config_prefix)
39
+ private_class_method :attrib_accessor
40
+
41
+ attrib_reader :doc, 'OpenAPI document.'
42
+ attrib_reader :outdir, 'Output directory name.'
43
+ attrib_reader :d, 'Other documents object.', OpenAPISourceTools::Docs.new
44
+ attrib_reader :wd, 'Original working directory', Dir.pwd
45
+ attrib_reader :configuration, 'Generator internal configuration'
46
+ attrib_accessor :config, 'Configuration file name for next gem or Ruby file.'
47
+ attrib_accessor :separator, 'Key separator in config file names.'
48
+ attrib_accessor :in_name, 'OpenAPI document name, nil if stdin.'
49
+ attrib_accessor :in_basename, 'OpenAPI document basename, nil if stdin.'
50
+ attrib_reader :g, 'Hash for storing values visible to all tasks.', {}
51
+ attrib_accessor :x, 'Hash for storing values visible to tasks from processor.', {}
52
+ attrib_accessor :h, 'Instance of class with helper methods.'
53
+ attrib_accessor :tasks, 'Tasks array.', []
54
+ attrib_accessor :t, 'Current task instance.'
55
+ attrib_accessor :task_index, 'Current task index.'
56
+ attrib_accessor :loaders, 'Array of processor loader methods.', []
57
+ attrib_accessor :output, 'Output-formatting helper.', OpenAPISourceTools::Output.new
58
+
59
+ def self.load_config(name_prefix, extensions = [ '.*' ])
83
60
  cfg = {}
84
- cfgs = OpenAPISourceTools::ConfigLoader.find_files(name_prefix: config_prefix)
61
+ cfgs = OpenAPISourceTools::ConfigLoader.find_files(name_prefix:, extensions:)
85
62
  cfgs = OpenAPISourceTools::ConfigLoader.read_contents(cfgs)
86
63
  cfgs.each { |c| cfg.deep_merge!(c) }
87
64
  cfg
88
65
  end
89
- private_class_method :load_config
90
66
 
91
- def self.setup(document_content, input_name, output_directory, config_prefix)
67
+ def setup(document_content, input_name, output_directory, config_prefix)
92
68
  @doc = document_content
93
69
  @outdir = output_directory
94
70
  unless input_name.nil?
95
71
  @in_name = File.basename(input_name)
96
72
  @in_basename = File.basename(input_name, '.*')
97
73
  end
98
- @configuration = load_config(config_prefix)
74
+ @configuration = Gen.load_config(config_prefix)
99
75
  add_task(task: OpenAPISourceTools::HelperTask.new)
100
76
  end
101
77
 
102
- def self.add_task(task:, name: nil, executable: false, x: nil)
78
+ def self.setup(document_content, input_name, output_directory, config_prefix)
79
+ Gen.instance.setup(document_content, input_name, output_directory, config_prefix)
80
+ end
81
+
82
+ def add_task(task:, name: nil, executable: false, x: nil)
103
83
  @tasks.push(task)
104
84
  # Since this method allows the user to pass their own task type instance,
105
85
  # assign optional values with defaults only when clearly given.
@@ -108,15 +88,27 @@ module Gen
108
88
  @tasks.last.x = x unless x.nil?
109
89
  end
110
90
 
111
- def self.add_write_content(name:, content:, executable: false)
91
+ def self.add_task(task:, name: nil, executable: false, x: nil)
92
+ Gen.instance.add_task(task:, name:, executable:, x:)
93
+ end
94
+
95
+ def add_write_content(name:, content:, executable: false)
112
96
  add_task(task: OpenAPISourceTools::WriteTask.new(name, content, executable))
113
97
  end
114
98
 
115
- def self.add(source:, template: nil, template_name: nil, name: nil, executable: false, x: nil)
99
+ def self.add_write_content(name:, content:, executable: false)
100
+ Gen.instance.add_write_content(name:, content:, executable:)
101
+ end
102
+
103
+ def add(source:, template: nil, template_name: nil, name: nil, executable: false, x: nil)
116
104
  add_task(task: OpenAPISourceTools::Task.new(source, template, template_name),
117
105
  name:, executable:, x:)
118
106
  end
119
107
 
108
+ def self.add(source:, template: nil, template_name: nil, name: nil, executable: false, x: nil)
109
+ Gen.instance.add(source:, template:, template_name:, name:, executable:, x:)
110
+ end
111
+
120
112
  def self.document
121
113
  @docsrc.join("\n") + %(
122
114
  - add_task(task:, name: nil, executable: false, x: nil) : Adds task object.
@@ -78,7 +78,6 @@ module OpenAPISourceTools
78
78
  Gen.task_index = 0
79
79
  while Gen.task_index < Gen.tasks.size
80
80
  Gen.t = Gen.tasks[Gen.task_index]
81
- Gen.task = Gen.t
82
81
  out = generate(Gen.t)
83
82
  Gen.task_index += 1
84
83
  return out if out.is_a?(Integer)
@@ -76,6 +76,47 @@ module OpenAPISourceTools
76
76
  end
77
77
  uniqs.keys.sort!.map { |k| uniqs[k] }
78
78
  end
79
+
80
+ def response_codes(responses_object)
81
+ responses_object.keys.sort! do |a, b|
82
+ ad = a.downcase
83
+ bd = b.downcase
84
+ if ad == 'default'
85
+ 1
86
+ elsif bd == 'default'
87
+ -1
88
+ else
89
+ ax = ad.end_with?('x')
90
+ bx = bd.end_with?('x')
91
+ if ax && bx || !ax && !bx
92
+ a <=> b # Both numbers or patterns.
93
+ else
94
+ ax ? 1 : -1
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ def response_code_condition(code, var: 'code', op_and: '&&', op_lte: '<=', op_eq: '==')
101
+ low = []
102
+ high = []
103
+ code.downcase.each_char do |c|
104
+ if c == 'x'
105
+ low.push('0')
106
+ high.push('9')
107
+ else
108
+ low.push(c)
109
+ high.push(c)
110
+ end
111
+ end
112
+ low = low.join
113
+ high = high.join
114
+ if low == high
115
+ "#{var} #{op_eq} #{low}"
116
+ else
117
+ "(#{low} #{op_lte} #{var}) #{op_and} (#{var} #{op_lte} #{high})"
118
+ end
119
+ end
79
120
  end
80
121
 
81
122
  # Task class to add an Helper instance to Gen.h, for convenience.
@@ -4,6 +4,7 @@
4
4
  # Licensed under Universal Permissive License. See LICENSE.txt.
5
5
 
6
6
  require_relative 'task'
7
+ require_relative 'gen'
7
8
 
8
9
 
9
10
  # Original loader functions. These are accessible via Gen.loaders. New loaders
@@ -33,19 +34,17 @@ module OpenAPISourceTools
33
34
  true
34
35
  end
35
36
 
36
- REREQ_PREFIX = 'rereq:'
37
+ EVAL_PREFIX = 'eval:'
37
38
 
38
- def self.rereq_loader(name)
39
- return false unless name.downcase.start_with?(REREQ_PREFIX)
39
+ def self.eval_loader(name)
40
+ return false unless name.downcase.start_with?(EVAL_PREFIX)
40
41
  begin
41
42
  t = OpenAPISourceTools::RestoreProcessorStorage.new({})
42
43
  Gen.tasks.push(t)
43
- code = name.slice(REREQ_PREFIX.size...name.size)
44
+ code = name.slice(EVAL_PREFIX.size...name.size)
44
45
  eval(code)
45
46
  Gen.config = nil
46
47
  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
48
  rescue Exception => e
50
49
  raise StandardError, "Problem with #{name}\n#{e}\n#{e.backtrace.join("\n")}"
51
50
  end
@@ -64,11 +63,11 @@ module OpenAPISourceTools
64
63
  Gen.tasks.push(t)
65
64
  base = File.basename(name)
66
65
  Gen.config = base[0..-4] if Gen.config.nil?
67
- require(File.join(Dir.pwd, base))
66
+ load(File.join(Dir.pwd, base))
68
67
  Gen.config = nil
69
68
  t.x = Gen.x # In case setup code replaced the object.
70
69
  rescue LoadError => e
71
- raise StandardError, "Failed to require #{name}\n#{e}"
70
+ raise StandardError, "Failed to load #{name}\n#{e}"
72
71
  rescue Exception => e
73
72
  raise StandardError, "Problem with #{name}\n#{e}\n#{e.backtrace.join("\n")}"
74
73
  end
@@ -139,7 +138,7 @@ module OpenAPISourceTools
139
138
  def self.loaders
140
139
  [
141
140
  method(:req_loader),
142
- method(:rereq_loader),
141
+ method(:eval_loader),
143
142
  method(:ruby_loader),
144
143
  method(:yaml_loader),
145
144
  method(:bin_loader),
@@ -151,7 +150,7 @@ module OpenAPISourceTools
151
150
  def self.document
152
151
  <<EOB
153
152
  - #{Loaders::REQ_PREFIX}req_name : requires the gem.
154
- - #{Loaders::REREQ_PREFIX}code : runs code to add gem tasks again.
153
+ - #{Loaders::EVAL_PREFIX}code : runs code to add gem tasks again.
155
154
  - ruby_file#{Loaders::RUBY_EXT} : changes to Ruby file directory and requires the file.
156
155
  - #{Loaders::YAML_PREFIX}name:filename : Loads YAML file into Gen.d.name.
157
156
  - name:filename.{#{(Loaders::YAML_EXTS.map { |s| s[1...s.size] }).join('|')}} : Loads YAML file into Gen.d.name.
@@ -46,13 +46,15 @@ module OpenAPISourceTools
46
46
  indent = 0
47
47
  blocks.each do |block|
48
48
  if block.nil?
49
- indent = 0
49
+ next
50
50
  elsif block.is_a?(Integer)
51
51
  indent += block
52
+ indent = 0 if indent.negative?
52
53
  elsif block.is_a?(TrueClass)
53
54
  indent += @config.indent_step
54
55
  elsif block.is_a?(FalseClass)
55
56
  indent -= @config.indent_step
57
+ indent = 0 if indent.negative?
56
58
  else
57
59
  block = block.to_s unless block.is_a?(String)
58
60
  if block.empty?
@@ -5,7 +5,7 @@
5
5
 
6
6
  module OpenAPISourceTools
7
7
  NAME = 'openapi-sourcetools'
8
- VERSION = '0.8.0'
8
+ VERSION = '0.9.0'
9
9
 
10
10
  def self.info(separator = ': ')
11
11
  "#{NAME}#{separator}#{VERSION}"
@@ -6,6 +6,7 @@
6
6
  require_relative 'sourcetools/task'
7
7
  require_relative 'sourcetools/config'
8
8
  require_relative 'sourcetools/version'
9
+ require_relative 'sourcetools/apiobjects'
9
10
  # Other modules or classes are exposed via Gen attributes as class instances as needed.
10
11
  # Docs is only needed for run-time storage of whatever loaders can handle.
11
12
  # Loaders array is exposed and can be added to at run-time.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openapi-sourcetools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ismo Kärkkäinen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-01-28 00:00:00.000000000 Z
11
+ date: 2025-04-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: deep_merge
@@ -41,11 +41,13 @@ executables:
41
41
  - openapi-addparameters
42
42
  - openapi-addresponses
43
43
  - openapi-addschemas
44
+ - openapi-addsecurityschemes
44
45
  - openapi-checkschemas
45
46
  - openapi-frequencies
46
47
  - openapi-generate
47
48
  - openapi-merge
48
49
  - openapi-modifypaths
50
+ - openapi-patterntests
49
51
  - openapi-processpaths
50
52
  extensions: []
51
53
  extra_rdoc_files: []
@@ -55,11 +57,13 @@ files:
55
57
  - bin/openapi-addparameters
56
58
  - bin/openapi-addresponses
57
59
  - bin/openapi-addschemas
60
+ - bin/openapi-addsecurityschemes
58
61
  - bin/openapi-checkschemas
59
62
  - bin/openapi-frequencies
60
63
  - bin/openapi-generate
61
64
  - bin/openapi-merge
62
65
  - bin/openapi-modifypaths
66
+ - bin/openapi-patterntests
63
67
  - bin/openapi-processpaths
64
68
  - lib/openapi/sourcetools.rb
65
69
  - lib/openapi/sourcetools/apiobjects.rb
@@ -71,7 +75,6 @@ files:
71
75
  - lib/openapi/sourcetools/helper.rb
72
76
  - lib/openapi/sourcetools/loaders.rb
73
77
  - lib/openapi/sourcetools/output.rb
74
- - lib/openapi/sourcetools/securityschemes.rb
75
78
  - lib/openapi/sourcetools/task.rb
76
79
  - lib/openapi/sourcetools/version.rb
77
80
  homepage: https://xn--ismo-krkkinen-gfbd.fi/openapi-sourcetools/index.html
@@ -1,268 +0,0 @@
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