openapi-sourcetools 0.10.1 → 0.12.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.
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright © 2021-2025 Ismo Kärkkäinen
3
+ # Copyright © 2021-2026 Ismo Kärkkäinen
4
4
  # Licensed under Universal Permissive License. See LICENSE.txt.
5
5
 
6
6
  require 'pathname'
@@ -56,6 +56,19 @@ module OpenAPISourceTools
56
56
  parts
57
57
  end
58
58
 
59
+ # Occasionally saved YAML has aliases. This is added to avoid them.
60
+ def self.ensure_separate(obj)
61
+ if obj.is_a?(Array)
62
+ return obj.map { |v| ensure_separate(v) }
63
+ end
64
+ return obj unless obj.is_a?(Hash)
65
+ out = {}
66
+ obj.each do |key, value|
67
+ out[key] = ensure_separate(value)
68
+ end
69
+ out
70
+ end
71
+
59
72
  def self.load_source(input)
60
73
  YAML.safe_load(input.nil? ? $stdin : File.read(input))
61
74
  rescue Errno::ENOENT
@@ -65,7 +78,9 @@ module OpenAPISourceTools
65
78
  end
66
79
 
67
80
  def self.dump_result(output, doc, error_return)
68
- doc = YAML.dump(doc, line_width: 1_000_000) unless doc.is_a?(String)
81
+ # Safe load did not allow aliases, but saved output may have them.
82
+ # Observed with empty object or array.
83
+ doc = YAML.dump(ensure_separate(doc), line_width: 1_000_000) unless doc.is_a?(String)
69
84
  if output.nil?
70
85
  $stdout.puts doc
71
86
  else
@@ -87,6 +87,10 @@ module OpenAPISourceTools
87
87
  return d unless d.zero?
88
88
  @path <=> other.path
89
89
  end
90
+
91
+ def to_s
92
+ "Root: #{@root} [#{@keys.join(', ')}] #{@path}"
93
+ end
90
94
  end
91
95
 
92
96
  def self.prepare_prefix(name_prefix, root)
@@ -0,0 +1,271 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright © 2026 Ismo Kärkkäinen
4
+ # Licensed under Universal Permissive License. See LICENSE.txt.
5
+
6
+ # Top-level module.
7
+ module OpenAPISourceTools
8
+ # Ordering methods and classes for openapi-order.
9
+ module Ordering
10
+ # Used as replacement for '*'.
11
+ ANY = Regexp.new('^.*$')
12
+
13
+ def self.array_item_compare(a, b)
14
+ a[:values].size.times do |k|
15
+ av = a[:values][k]
16
+ bv = b[:values][k]
17
+ if av.nil?
18
+ return 1 unless bv.nil?
19
+ else
20
+ return -1 if bv.nil?
21
+ r = av <=> bv
22
+ return r unless r.nil? || r.zero?
23
+ end
24
+ end
25
+ 0
26
+ end
27
+
28
+ # Allowed keys starting from root, with pattern, any or multiple any key.
29
+ # Initialized with the path to the order array.
30
+ class KeyPath
31
+ include Comparable
32
+
33
+ attr_reader :items, :multiple, :first_multiple
34
+
35
+ # the parent_keys is processed to an array of regular expressions and :multi.
36
+ # A '*' is eventually replaced with regular expression that accepts anything.
37
+ # A '**' is replaced by :multi that matches at least one key during match.
38
+ # A string starting with regexp_prefix is changed to a regular expression.
39
+ # Other string is turned to anchored regular expression that matches only the string.
40
+ # The order of '*' and '**' can be modified to minimize occurrences of :multi.
41
+ def initialize(parent_keys, regexp_prefix)
42
+ @multiple = false
43
+ @items = []
44
+ parent_keys.each do |key|
45
+ if key == '*'
46
+ if @items.last == :multi
47
+ # :multi, :one is same as :one, :multi as at least 2 arbitrary items.
48
+ @items.pop
49
+ @items.push(:one, :multi)
50
+ else
51
+ @items.push(:one)
52
+ end
53
+ elsif key == '**'
54
+ @multiple = true
55
+ if @items.last == :multi
56
+ # :multi, :multi is same as :one, :multi as at least 2 arbitrary items.
57
+ @items.pop
58
+ @items.push(:one, :multi)
59
+ else
60
+ @items.push(:multi)
61
+ end
62
+ elsif key.start_with?(regexp_prefix)
63
+ @items.push(Regexp.new(key[regexp_prefix.size..]))
64
+ else
65
+ @items.push(Regexp.new("^#{Regexp.escape(key)}$"))
66
+ end
67
+ end
68
+ @items.each_with_index do |item, idx|
69
+ @items[idx] = ANY if item == :one
70
+ end
71
+ @first_multiple = @items.index(:multi) || @items.size
72
+ end
73
+
74
+ # Finds indexes of regular expressions that match key, skipping given number.
75
+ def matching_item_indexes(key, skip_non_multis)
76
+ indexes = []
77
+ ([skip_non_multis, @first_multiple].min...@items.size).each do |idx|
78
+ item = @items[idx]
79
+ indexes.push(idx) if item == :multi || item.match?(key)
80
+ end
81
+ indexes
82
+ end
83
+
84
+ # Given possible matches, uses position_index to check for matches relevant
85
+ # to the current situation and checks recursively if a solution ending to
86
+ # the desired point can be found.
87
+ def search(index_arrays, array_index, position_index)
88
+ if array_index == index_arrays.size
89
+ # Last key is known to match last item, so if that were tested, it would pass.
90
+ return position_index == @items.size - 1
91
+ end
92
+ if @items[position_index] == :multi
93
+ # Multi absorbs current key and releases to next position.
94
+ return true if search(index_arrays, array_index + 1, position_index + 1)
95
+ # Multi absorbs current key and stays for more.
96
+ search(index_arrays, array_index + 1, position_index)
97
+ else
98
+ # Only current item to match with.
99
+ return false if index_arrays[array_index].index(position_index).nil?
100
+ search(index_arrays, array_index + 1, position_index + 1)
101
+ end
102
+ end
103
+
104
+ # Performs base checks whether match is possible and if so, obtains indexes
105
+ # od matching patterns and starts search using those.
106
+ def match?(key_path)
107
+ return true if @items.empty? && key_path.empty?
108
+ slack = key_path.size - @items.size
109
+ return false if slack.negative? # Path too short.
110
+ return false if slack.positive? && !@multiple # Path too long.
111
+ # Last key must match last item.
112
+ return false unless @items.last == :multi || @items.last.match?(key_path.last)
113
+ # First key must match first item.
114
+ return false unless @items.first == :multi || @items.first.match?(key_path.first)
115
+ return true if key_path.size <= 2 # Tested both already.
116
+ # Form arrays of possible match indexes for search. Omit first and last.
117
+ index_arrays = []
118
+ (1...(key_path.size - 1)).each do |idx|
119
+ key = key_path[idx]
120
+ idxs = matching_item_indexes(key, idx)
121
+ return false if idxs.empty?
122
+ index_arrays.push(idxs)
123
+ end
124
+ search(index_arrays, 0, @items.first == :multi ? 0 : 1)
125
+ end
126
+
127
+ def <=>(other)
128
+ # The purpose is to place longest "exact" items to the end.
129
+ # Hence items.size, and the more multiple, the earlier.
130
+ d = @items.size <=> other.items.size
131
+ return d unless d.zero?
132
+ -@items.count(:multi) <=> -other.items.count(:multi)
133
+ end
134
+ end
135
+
136
+ # Book-keeping class for pattern and its location.
137
+ class KeyPattern
138
+ attr_reader :pattern, :index, :priority
139
+
140
+ def initialize(pattern, index, priority)
141
+ @pattern = pattern
142
+ @index = index
143
+ @priority = priority
144
+ end
145
+
146
+ def match?(string)
147
+ @pattern.match?(string)
148
+ end
149
+
150
+ def any?
151
+ @index != @priority
152
+ end
153
+ end
154
+
155
+ # Handles ordering keys or an array using the given key order.
156
+ class Order
157
+ def self.orderable?(item)
158
+ return true if item.is_a?(Hash)
159
+ return true if item.is_a?(Array)
160
+ false
161
+ end
162
+
163
+ attr_reader :patterns
164
+
165
+ def initialize(keys, regexp_prefix)
166
+ @patterns = []
167
+ has_asterisk = false
168
+ keys.each_with_index do |string, idx|
169
+ if string.start_with?(regexp_prefix)
170
+ @patterns.push(KeyPattern.new(Regexp.new(string[regexp_prefix.size..]), idx, idx))
171
+ elsif string == '*'
172
+ @patterns.push(KeyPattern.new(Regexp.new('^.*$'), idx, keys.size)) unless has_asterisk # First used.
173
+ has_asterisk = true
174
+ else
175
+ @patterns.push(KeyPattern.new(Regexp.new("^#{Regexp.escape(string)}$"), idx, idx))
176
+ end
177
+ end
178
+ @patterns.push(KeyPattern.new(Regexp.new('^.*$'), @patterns.size, keys.size + 1)) unless has_asterisk
179
+ end
180
+
181
+ def apply_array(array)
182
+ return true if array.size < 2
183
+ values = []
184
+ array.each do |item|
185
+ amended = {
186
+ item: item,
187
+ values: []
188
+ }
189
+ unless item.is_a?(Hash)
190
+ amended[:values] = Array.new(@patterns.size, nil)
191
+ values.push(amended)
192
+ next
193
+ end
194
+ @patterns.each do |kp|
195
+ if kp.any?
196
+ amended[:values].push(nil)
197
+ next
198
+ end
199
+ # Find first matching key.
200
+ v = nil
201
+ item.each_key do |item_key|
202
+ next unless kp.match?(item_key)
203
+ v = item[item_key]
204
+ break
205
+ end
206
+ amended[:values].push((v.is_a?(Array) || v.is_a?(Hash)) ? nil : v)
207
+ end
208
+ values.push(amended)
209
+ end
210
+ values.sort! { |a, b| OpenAPISourceTools::Ordering.array_item_compare(a, b) }
211
+ values.each_with_index { |amended, i| array[i] = amended[:item] }
212
+ true
213
+ end
214
+
215
+ def apply_hash(hash)
216
+ return true if hash.size < 2
217
+ groups = @patterns.map do |kp|
218
+ {
219
+ kp: kp,
220
+ keys: []
221
+ }
222
+ end
223
+ s2orig = {}
224
+ hash.each_key do |key|
225
+ skey = key.is_a?(String) ? key : key.to_s
226
+ s2orig[skey] = key
227
+ best_priority = groups.size + 1
228
+ best_idx = nil
229
+ groups.each do |group|
230
+ kp = group[:kp]
231
+ next if best_priority <= kp.priority
232
+ next unless kp.match?(skey)
233
+ best_priority = kp.priority
234
+ best_idx = kp.index
235
+ end
236
+ groups[best_idx][:keys].push(skey) unless best_idx.nil?
237
+ end
238
+ ordered = {}
239
+ groups.each do |group|
240
+ group[:keys].sort!
241
+ group[:keys].each { |k| ordered[s2orig[k]] = hash[s2orig[k]] }
242
+ end
243
+ hash.clear
244
+ hash.merge!(ordered)
245
+ true
246
+ end
247
+
248
+ def apply(item)
249
+ return apply_array(item) if item.is_a?(Array)
250
+ return apply_hash(item) if item.is_a?(Hash)
251
+ false
252
+ end
253
+ end
254
+
255
+ # Holds KeyPath and the order that was specified.
256
+ class KeyPathOrder
257
+ include Comparable
258
+
259
+ attr_reader :path, :order
260
+
261
+ def initialize(key_path, order)
262
+ @path = key_path
263
+ @order = order
264
+ end
265
+
266
+ def <=>(other)
267
+ @path <=> other.path
268
+ end
269
+ end
270
+ end
271
+ end
@@ -5,7 +5,7 @@
5
5
 
6
6
  module OpenAPISourceTools
7
7
  NAME = 'openapi-sourcetools'
8
- VERSION = '0.10.1'
8
+ VERSION = '0.12.0'
9
9
 
10
10
  def self.info(separator = ': ')
11
11
  "#{NAME}#{separator}#{VERSION}"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openapi-sourcetools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.1
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ismo Kärkkäinen
@@ -42,10 +42,12 @@ executables:
42
42
  - openapi-addschemas
43
43
  - openapi-addsecurityschemes
44
44
  - openapi-checkschemas
45
+ - openapi-clearrefs
45
46
  - openapi-frequencies
46
47
  - openapi-generate
47
48
  - openapi-merge
48
49
  - openapi-modifypaths
50
+ - openapi-order
49
51
  - openapi-patterntests
50
52
  - openapi-processpaths
51
53
  extensions: []
@@ -58,10 +60,12 @@ files:
58
60
  - bin/openapi-addschemas
59
61
  - bin/openapi-addsecurityschemes
60
62
  - bin/openapi-checkschemas
63
+ - bin/openapi-clearrefs
61
64
  - bin/openapi-frequencies
62
65
  - bin/openapi-generate
63
66
  - bin/openapi-merge
64
67
  - bin/openapi-modifypaths
68
+ - bin/openapi-order
65
69
  - bin/openapi-patterntests
66
70
  - bin/openapi-processpaths
67
71
  - lib/openapi/sourcetools.rb
@@ -73,6 +77,7 @@ files:
73
77
  - lib/openapi/sourcetools/generate.rb
74
78
  - lib/openapi/sourcetools/helper.rb
75
79
  - lib/openapi/sourcetools/loaders.rb
80
+ - lib/openapi/sourcetools/order.rb
76
81
  - lib/openapi/sourcetools/output.rb
77
82
  - lib/openapi/sourcetools/task.rb
78
83
  - lib/openapi/sourcetools/version.rb
@@ -88,7 +93,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
88
93
  requirements:
89
94
  - - ">="
90
95
  - !ruby/object:Gem::Version
91
- version: 3.2.5
96
+ version: 3.3.11
92
97
  required_rubygems_version: !ruby/object:Gem::Requirement
93
98
  requirements:
94
99
  - - ">="