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.
- checksums.yaml +4 -4
- data/LICENSE.txt +1 -1
- data/bin/openapi-addheaders +7 -23
- data/bin/openapi-addparameters +7 -7
- data/bin/openapi-addresponses +6 -22
- data/bin/openapi-addschemas +32 -26
- data/bin/openapi-clearrefs +46 -0
- data/bin/openapi-order +573 -0
- data/lib/openapi/sourcetools/apiobjects.rb +105 -1
- data/lib/openapi/sourcetools/common.rb +17 -2
- data/lib/openapi/sourcetools/config.rb +4 -0
- data/lib/openapi/sourcetools/order.rb +271 -0
- data/lib/openapi/sourcetools/version.rb +1 -1
- metadata +7 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Copyright © 2021-
|
|
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
|
-
|
|
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
|
|
@@ -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
|
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.
|
|
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.
|
|
96
|
+
version: 3.3.11
|
|
92
97
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
93
98
|
requirements:
|
|
94
99
|
- - ">="
|