openapi-sourcetools 0.12.0 → 0.13

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,100 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Copyright © 2026 Ismo Kärkkäinen
5
+ # Licensed under Universal Permissive License. See LICENSE.txt.
6
+
7
+ require_relative '../lib/openapi/sourcetools/common'
8
+ require_relative '../lib/openapi/sourcetools/schemaio'
9
+ require 'optparse'
10
+ require 'yaml'
11
+ require 'json'
12
+ include OpenAPISourceTools
13
+
14
+
15
+ # Functions related to saving the document path dump.
16
+
17
+ def to_json(doc)
18
+ JSON.dump(doc)
19
+ end
20
+
21
+ def to_pretty_json(doc)
22
+ JSON.pretty_generate(doc)
23
+ end
24
+
25
+ def to_yaml(doc)
26
+ YAML.dump(doc)
27
+ end
28
+
29
+ def main
30
+ input_file = nil
31
+ output_file = nil
32
+ doc_paths = :openapi
33
+ output_default = nil
34
+ format = 'pretty_json'
35
+ formats = {
36
+ 'yaml' => method(:to_yaml),
37
+ 'json' => method(:to_json),
38
+ 'pretty_json' => method(:to_pretty_json)
39
+ }
40
+
41
+ parser = OptionParser.new do |opts|
42
+ opts.summary_indent = ' '
43
+ opts.summary_width = 22
44
+ opts.banner = 'Usage: openapi-schema-ls [options]'
45
+ opts.separator ''
46
+ opts.separator 'Options:'
47
+ opts.on('-i', '--input FILE', 'Read input from FILE, not stdin.') do |f|
48
+ input_file = f
49
+ end
50
+ opts.on('-o', '--output FILE', 'Write result to FILE, not stdout.') do |f|
51
+ output_file = f
52
+ end
53
+ opts.on('--inputpaths DATA', 'Input path data.') do |data|
54
+ doc_paths = data
55
+ end
56
+ opts.on('--dump DATA', 'Dump internal path data to output and exit.') do |data|
57
+ output_default = data
58
+ end
59
+ opts.on('-f', '--format FORMAT', "DATA output format: #{formats.keys.sort!.join(', ')}.") do |fmt|
60
+ format = fmt
61
+ end
62
+ opts.on('-h', '--help', 'Print this help and exit.') do
63
+ $stdout.puts <<EOF
64
+ #{opts}
65
+ Lists named schemas in the input document.
66
+
67
+ The document paths DATA is either a file name, internal data name, or a
68
+ JSON-encoded object with necessary keys. Same data also applies to both
69
+ openapi-schema-in and openapi-schema-out. Internal data names are:
70
+ #{SchemaIO::BUILTINS.keys.sort!.join(', ')}
71
+
72
+ DATA defaults to OpenAPI format.
73
+ EOF
74
+ return 0
75
+ end
76
+ end
77
+ parser.order!
78
+
79
+ return Common.aargh("Unknown format: #{format}", 1) unless formats.key?(format)
80
+
81
+ unless output_default.nil?
82
+ doc = SchemaIO.get(output_default)
83
+ return Commpon.aargh("Not internal data format: #{output_default}", 1) if doc.nil?
84
+ out = (format == 'yaml') ? doc : formats[format].call(YAML.safe_load(doc))
85
+ return Common.dump_result(output_file, out, 3)
86
+ end
87
+
88
+ dp = SchemaIO.key_data(doc_paths)
89
+ return Common.aargh("Not file or internal format name or format as JSON object: #{doc_paths}", 2) if dp.nil?
90
+
91
+ doc = Common.load_source(input_file)
92
+ return 2 if doc.nil?
93
+
94
+ src = SchemaIO::SchemaAccess.new(dp, doc)
95
+ out = src.list_schema_names.join("\n")
96
+
97
+ Common.dump_result(output_file, out, 3)
98
+ end
99
+
100
+ exit(main) if File.basename(__FILE__) == File.basename($PROGRAM_NAME)
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Copyright © 2026 Ismo Kärkkäinen
5
+ # Licensed under Universal Permissive License. See LICENSE.txt.
6
+
7
+ require_relative '../lib/openapi/sourcetools/common'
8
+ require_relative '../lib/openapi/sourcetools/schemaio'
9
+ require 'optparse'
10
+ require 'yaml'
11
+ require 'json'
12
+ include OpenAPISourceTools
13
+
14
+
15
+ # Functions related to saving the output.
16
+
17
+ def to_json(doc, _line_width)
18
+ JSON.dump(doc)
19
+ end
20
+
21
+ def to_pretty_json(doc, _line_width)
22
+ JSON.pretty_generate(doc)
23
+ end
24
+
25
+ def to_yaml(doc, line_width)
26
+ YAML.dump(doc, line_width: line_width)
27
+ end
28
+
29
+ def main
30
+ input_file = nil
31
+ output_file = nil
32
+ name = nil
33
+ refs = true
34
+ format = 'pretty_json'
35
+ formats = {
36
+ 'yaml' => method(:to_yaml),
37
+ 'json' => method(:to_json),
38
+ 'pretty_json' => method(:to_pretty_json)
39
+ }
40
+ line_width = 80
41
+ doc_paths = :openapi
42
+ schema_paths = :jsonschema
43
+ additions = {}
44
+
45
+ parser = OptionParser.new do |opts|
46
+ opts.summary_indent = ' '
47
+ opts.summary_width = 27
48
+ opts.banner = 'Usage: openapi-schema-out [options]'
49
+ opts.separator ''
50
+ opts.separator 'Options:'
51
+ opts.on('-i', '--input FILE', 'Read input from FILE, not stdin.') do |f|
52
+ input_file = f
53
+ end
54
+ opts.on('-o', '--output FILE', 'Write result to FILE, not stdout.') do |f|
55
+ output_file = f
56
+ end
57
+ opts.on('-n', '--name NAME', 'Use schema NAME.') do |n|
58
+ name = n
59
+ end
60
+ opts.on('--[no-]refs', "Include referred schemas (default #{Common.yesno(refs)}).") do |r|
61
+ refs = r
62
+ end
63
+ opts.on('-f', '--format FORMAT', "Schema output format: #{formats.keys.sort!.join(', ')}.") do |fmt|
64
+ format = fmt
65
+ end
66
+ opts.on('-w', '--width WIDTH', "Set YAML line width, default: #{line_width}") do |w|
67
+ line_width = w.to_i
68
+ end
69
+ opts.on('--inputpaths DATA', 'Input path data.') do |data|
70
+ doc_paths = data
71
+ end
72
+ opts.on('--schemapaths DATA', 'Schema path data.') do |data|
73
+ schema_paths = data
74
+ end
75
+ opts.on('-a', '--add KEY:[json:]VALUE', 'Add KEY with (JSON) VALUE to output schema.') do |kv|
76
+ k, v = kv.split(':', 2)
77
+ return Common.aargh("Invalid key-value pair: #{kv}", 1) if k.nil? || v.nil?
78
+ return Common.aargh("Empty key: #{kv}", 1) if k.empty?
79
+ v = JSON.parse(v[5..]) if v.downcase.start_with?('json:')
80
+ additions[k] = v
81
+ rescue JSON::ParserError
82
+ return Common.aargh("Invalid JSON in argument value: #{kv}", 1)
83
+ end
84
+ opts.on('-h', '--help', 'Print this help and exit.') do
85
+ $stdout.puts <<EOF
86
+ #{opts}
87
+ Takes schema with name from input and places it into output.
88
+
89
+ The document and schema paths DATA is either a file name, internal data name,
90
+ or a JSON-encoded object with necessary keys. Internal data names are:
91
+ #{SchemaIO::BUILTINS.keys.sort!.join(', ')}
92
+
93
+ Depending on input, schema NAME is used to find and store the schema in the
94
+ location indicated by DATA for output.
95
+
96
+ DATA values default to input being OpenAPI format and output is JSON schema
97
+ with schema at root and referenced schemas, if output, under "$defs" key.
98
+
99
+ Adding KEY:VALUE pairs can be used to add keys to the output schema with
100
+ string values. Using KEY:json:VALUE will parse VALUE as JSON and use that.
101
+ The format indicator is case-insensitive. Value can be empty but key cannot
102
+ be empty. Pass value starting with "json:" as 'key:json:"json:the_rest"'.
103
+ Usable for adding "$schema" and "$id" keys to the output schema.
104
+ EOF
105
+ return 0
106
+ end
107
+ end
108
+ parser.order!
109
+
110
+ return Common.aargh('Schema name must be given.', 1) if name.nil?
111
+
112
+ return Common.aargh("Unknown format: #{format}", 1) unless formats.key?(format)
113
+
114
+ dp = SchemaIO.key_data(doc_paths)
115
+ return Common.aargh("Not file or internal format name or format as JSON object: #{doc_paths}", 2) if dp.nil?
116
+ sp = SchemaIO.key_data(schema_paths)
117
+ return Common.aargh("Not file or internal format name or format as JSON object: #{schema_paths}", 2) if sp.nil?
118
+
119
+ return Common.aargh(%(Name "#{name}" has document path separator "#{dp.separator}"), 1) if name.include?(dp.separator)
120
+ return Common.aargh(%(Name "#{name}" has schema path separator "#{sp.separator}"), 1) if name.include?(sp.separator)
121
+
122
+ doc = Common.load_source(input_file)
123
+ return 2 if doc.nil?
124
+ return Common.aargh('Document is not an object.', 2) unless doc.is_a?(Hash)
125
+
126
+ src = SchemaIO::SchemaAccess.new(dp, doc)
127
+ tgt = SchemaIO::SchemaAccess.new(sp, {})
128
+
129
+ desired = src.find_named_schema(name)
130
+ return Common.aargh("Schema not found: #{name}", 1) if desired.nil?
131
+ desired.merge!(additions) unless additions.empty?
132
+ tgt.put_named_schema(name, desired)
133
+
134
+ if refs
135
+ refd = src.find_referenced_schemas(name, desired)
136
+ refd.each { |n, s| tgt.put_referenced_schema(n, s) }
137
+ end
138
+ SchemaIO.alter_refs(tgt.document, src, tgt) # After referenced schemas have been found.
139
+
140
+ out = formats[format].call(tgt.document, line_width)
141
+ Common.dump_result(output_file, out, 3)
142
+ end
143
+
144
+ exit(main) if File.basename(__FILE__) == File.basename($PROGRAM_NAME)
@@ -1,12 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright © 2024-2025 Ismo Kärkkäinen
3
+ # Copyright © 2024-2026 Ismo Kärkkäinen
4
4
  # Licensed under Universal Permissive License. See LICENSE.txt.
5
5
 
6
6
  module OpenAPISourceTools
7
+ # Various schema-related constants.
8
+ module Schemas
9
+ ARRAY_VALUED = %w[allOf anyOf oneOf prefixItems].freeze
10
+ OTHER_VALUED = %w[items additionalProperties unevaluatedItems unevaluatedProperties if then else not].freeze
11
+ KEY2SCHEMA = %w[properties patternProperties dependentSchemas].freeze
12
+ HANDLED = [].concat(ARRAY_VALUED, OTHER_VALUED, KEY2SCHEMA).freeze
13
+ IGNORED = %w[example examples].freeze
14
+ end
15
+
7
16
  # Various classes for handling objects in the API specification.
8
17
  # Used in various programs.
9
18
  module ApiObjects
19
+ LOGIC_KEYS = Set.new(%w[allOf anyOf oneOf not if then else dependentSchemas]).freeze
20
+ DOCUMENT_KEYS = %w[example examples title summary description deprecated readOnly writeOnly].freeze
10
21
 
11
22
  def self.same(a, b, ignored_keys = Set.new(%w[summary description]))
12
23
  return a == b unless a.is_a?(Hash) && b.is_a?(Hash)
@@ -89,7 +100,7 @@ module OpenAPISourceTools
89
100
  next if value.key?('$ref')
90
101
  @components.to_reference_object(value)
91
102
  end
92
- else
103
+ elsif !Schemas::IGNORED.include?(key)
93
104
  replace(value)
94
105
  end
95
106
  end
@@ -114,6 +125,7 @@ module OpenAPISourceTools
114
125
  obj.each do |key, value|
115
126
  if key == @key && value.is_a?(Hash)
116
127
  value.keys.sort!.each do |sub_key|
128
+ next if Schemas::IGNORED.include?(sub_key)
117
129
  sub_value = value[sub_key]
118
130
  next unless sub_value.is_a?(Hash)
119
131
  next if sub_value.key?('$ref')
@@ -173,7 +185,7 @@ module OpenAPISourceTools
173
185
  @items.each do |k, v|
174
186
  return ref_string(k) if ApiObjects.same(obj, v, @ignored_keys)
175
187
  end
176
- # One of the numbers will not match existing keys. More number than keys.
188
+ # One of the numbers will not match existing keys. More numbers than keys.
177
189
  (@items.size + 1).times do |n|
178
190
  # 'x' is to simplify find and replace (Schema1x vs Schema1 and Schema10)
179
191
  cand = "#{@prefix}#{n}x"
@@ -240,17 +252,11 @@ module OpenAPISourceTools
240
252
  pk = @parts[k]
241
253
  ppk = pp[k]
242
254
  if pk.key?('fixed')
243
- if ppk.key?('fixed')
244
- c = pk['fixed'] <=> ppk['fixed']
245
- else
246
- return -1
247
- end
255
+ return -1 unless ppk.key?('fixed')
256
+ c = pk['fixed'] <=> ppk['fixed']
248
257
  else
249
- if ppk.key?('fixed')
250
- return 1
251
- else
252
- c = pk.fetch('parameter', '') <=> ppk.fetch('parameter', '')
253
- end
258
+ return 1 if ppk.key?('fixed')
259
+ c = pk.fetch('parameter', '') <=> ppk.fetch('parameter', '')
254
260
  end
255
261
  return c unless c.zero?
256
262
  end
@@ -10,7 +10,7 @@ module OpenAPISourceTools
10
10
  # Common methods used in programs and elsewhere gathered into one place.
11
11
  module Common
12
12
  def self.aargh(message, return_value = nil)
13
- message = message.map(&:to_s).join("\n") if message.is_a? Array
13
+ message = message.join("\n") if message.is_a? Array
14
14
  $stderr.puts message
15
15
  return_value
16
16
  end
@@ -8,11 +8,11 @@ require_relative 'gen'
8
8
  require 'find'
9
9
  require 'yaml'
10
10
 
11
+ # Gem namespace.
11
12
  module OpenAPISourceTools
12
13
  # Configuration file find and load convenience functions.
13
14
  # See the first 3 methods. The rest are intended to be internal helpers.
14
15
  module ConfigLoader
15
-
16
16
  # A function to find all files with a given prefix.
17
17
  # Prefix is taken from Gen.config if nil.
18
18
  # Returns an array of ConfigFileInfo objects.
@@ -43,9 +43,10 @@ module OpenAPISourceTools
43
43
 
44
44
  # Maps an array of ConfigFileInfo objects to an array of their contents.
45
45
  def self.contents_array(config_file_infos)
46
- config_file_infos.map(&:content).reject(&:nil?)
46
+ config_file_infos.map(&:content).compact
47
47
  end
48
48
 
49
+ # The result with data about the config file.
49
50
  class ConfigFileInfo
50
51
  include Comparable
51
52
 
@@ -10,6 +10,7 @@ require_relative 'loaders'
10
10
  require_relative 'gen'
11
11
 
12
12
 
13
+ # Gem namespace.
13
14
  module OpenAPISourceTools
14
15
  def self.executable_bits_on(mode)
15
16
  mode = mode.to_s(8).chars
@@ -82,7 +82,7 @@ module OpenAPISourceTools
82
82
  d = name.downcase
83
83
  if d.start_with?(YAML_PREFIX)
84
84
  name = name.slice(YAML_PREFIX.size...name.size)
85
- elsif (YAML_EXTS.index { |s| d.end_with?(s) }).nil?
85
+ elsif YAML_EXTS.index { |s| d.end_with?(s) }.nil?
86
86
  return false
87
87
  end
88
88
  n, _sep, f = name.partition(':')
@@ -140,9 +140,9 @@ module OpenAPISourceTools
140
140
  method(:req_loader),
141
141
  method(:eval_loader),
142
142
  method(:ruby_loader),
143
+ method(:config_loader),
143
144
  method(:yaml_loader),
144
145
  method(:bin_loader),
145
- method(:config_loader),
146
146
  method(:separator_loader)
147
147
  ]
148
148
  end
@@ -153,7 +153,7 @@ module OpenAPISourceTools
153
153
  - #{Loaders::EVAL_PREFIX}code : runs code to add gem tasks again.
154
154
  - ruby_file#{Loaders::RUBY_EXT} : changes to Ruby file directory and requires the file.
155
155
  - #{Loaders::YAML_PREFIX}name:filename : Loads YAML file into Gen.d.name.
156
- - name:filename.{#{(Loaders::YAML_EXTS.map { |s| s[1...s.size] }).join('|')}} : Loads YAML file into Gen.d.name.
156
+ - name:filename.{#{Loaders::YAML_EXTS.map { |s| s[1...s.size] }.join('|')}} : Loads YAML file into Gen.d.name.
157
157
  - #{Loaders::BIN_PREFIX}name:filename : Loads binary file into Gen.d.name.
158
158
  - #{Loaders::CONFIG_PREFIX}name : Sets Gen.config for next gem/Ruby file configuration loading.
159
159
  - #{Loaders::SEPARATOR_PREFIX}string : Sets Gen.separator to string.
@@ -10,7 +10,7 @@ module OpenAPISourceTools
10
10
  # Used as replacement for '*'.
11
11
  ANY = Regexp.new('^.*$')
12
12
 
13
- def self.array_item_compare(a, b)
13
+ def self.array_item_compare(a, b, patterns)
14
14
  a[:values].size.times do |k|
15
15
  av = a[:values][k]
16
16
  bv = b[:values][k]
@@ -18,8 +18,14 @@ module OpenAPISourceTools
18
18
  return 1 unless bv.nil?
19
19
  else
20
20
  return -1 if bv.nil?
21
- r = av <=> bv
22
- return r unless r.nil? || r.zero?
21
+ if (av.is_a?(TrueClass) || av.is_a?(FalseClass)) && (bv.is_a?(TrueClass) || bv.is_a?(FalseClass))
22
+ r = (av ? 1 : 0) <=> (bv ? 1 : 0)
23
+ else
24
+ r = av <=> bv
25
+ end
26
+ next if r.nil? || r.zero?
27
+ r = -r unless patterns[k].ascending
28
+ return r
23
29
  end
24
30
  end
25
31
  0
@@ -129,18 +135,23 @@ module OpenAPISourceTools
129
135
  # Hence items.size, and the more multiple, the earlier.
130
136
  d = @items.size <=> other.items.size
131
137
  return d unless d.zero?
132
- -@items.count(:multi) <=> -other.items.count(:multi)
138
+ d = -@items.count(:multi) <=> -other.items.count(:multi)
139
+ return d unless d.zero?
140
+ # Questionable if I should compare to the object_id or equality.
141
+ # Now if you use same regular expression, they are the same.
142
+ -@items.count(ANY) <=> -other.items.count(ANY)
133
143
  end
134
144
  end
135
145
 
136
146
  # Book-keeping class for pattern and its location.
137
147
  class KeyPattern
138
- attr_reader :pattern, :index, :priority
148
+ attr_reader :pattern, :index, :priority, :ascending
139
149
 
140
- def initialize(pattern, index, priority)
150
+ def initialize(pattern, index, priority, sort_operator)
141
151
  @pattern = pattern
142
152
  @index = index
143
153
  @priority = priority
154
+ @ascending = sort_operator == '<'
144
155
  end
145
156
 
146
157
  def match?(string)
@@ -160,22 +171,34 @@ module OpenAPISourceTools
160
171
  false
161
172
  end
162
173
 
174
+ def self.ordervalue(item)
175
+ return [ item, '<' ] if item.is_a?(String)
176
+ return nil unless item.is_a?(Hash)
177
+ return nil unless item.size == 1
178
+ key = item.keys.first
179
+ return nil unless key.is_a?(String)
180
+ return nil unless ['<', '>'].include?(item[key])
181
+ [ key, item[key] ]
182
+ end
183
+
163
184
  attr_reader :patterns
164
185
 
165
186
  def initialize(keys, regexp_prefix)
166
187
  @patterns = []
167
188
  has_asterisk = false
168
- keys.each_with_index do |string, idx|
189
+ keys.each_with_index do |ov, idx|
190
+ string = ov.first
191
+ sort_operator = ov.last
169
192
  if string.start_with?(regexp_prefix)
170
- @patterns.push(KeyPattern.new(Regexp.new(string[regexp_prefix.size..]), idx, idx))
193
+ @patterns.push(KeyPattern.new(Regexp.new(string[regexp_prefix.size..]), idx, idx, sort_operator))
171
194
  elsif string == '*'
172
- @patterns.push(KeyPattern.new(Regexp.new('^.*$'), idx, keys.size)) unless has_asterisk # First used.
195
+ @patterns.push(KeyPattern.new(Regexp.new('^.*$'), idx, keys.size, sort_operator)) unless has_asterisk # First used.
173
196
  has_asterisk = true
174
197
  else
175
- @patterns.push(KeyPattern.new(Regexp.new("^#{Regexp.escape(string)}$"), idx, idx))
198
+ @patterns.push(KeyPattern.new(Regexp.new("^#{Regexp.escape(string)}$"), idx, idx, sort_operator))
176
199
  end
177
200
  end
178
- @patterns.push(KeyPattern.new(Regexp.new('^.*$'), @patterns.size, keys.size + 1)) unless has_asterisk
201
+ @patterns.push(KeyPattern.new(Regexp.new('^.*$'), @patterns.size, keys.size + 1, '<')) unless has_asterisk
179
202
  end
180
203
 
181
204
  def apply_array(array)
@@ -207,13 +230,13 @@ module OpenAPISourceTools
207
230
  end
208
231
  values.push(amended)
209
232
  end
210
- values.sort! { |a, b| OpenAPISourceTools::Ordering.array_item_compare(a, b) }
233
+ values.sort! { |a, b| OpenAPISourceTools::Ordering.array_item_compare(a, b, @patterns) }
211
234
  values.each_with_index { |amended, i| array[i] = amended[:item] }
212
235
  true
213
236
  end
214
237
 
215
- def apply_hash(hash)
216
- return true if hash.size < 2
238
+ def apply_hash(unordered_hash)
239
+ return true if unordered_hash.size < 2
217
240
  groups = @patterns.map do |kp|
218
241
  {
219
242
  kp: kp,
@@ -221,7 +244,7 @@ module OpenAPISourceTools
221
244
  }
222
245
  end
223
246
  s2orig = {}
224
- hash.each_key do |key|
247
+ unordered_hash.each_key do |key|
225
248
  skey = key.is_a?(String) ? key : key.to_s
226
249
  s2orig[skey] = key
227
250
  best_priority = groups.size + 1
@@ -238,10 +261,11 @@ module OpenAPISourceTools
238
261
  ordered = {}
239
262
  groups.each do |group|
240
263
  group[:keys].sort!
241
- group[:keys].each { |k| ordered[s2orig[k]] = hash[s2orig[k]] }
264
+ group[:keys].reverse! unless group[:kp].ascending
265
+ group[:keys].each { |k| ordered[s2orig[k]] = unordered_hash[s2orig[k]] }
242
266
  end
243
- hash.clear
244
- hash.merge!(ordered)
267
+ unordered_hash.clear
268
+ unordered_hash.merge!(ordered)
245
269
  true
246
270
  end
247
271