openapi-sourcetools 0.11.0 → 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/bin/openapi-addschemas +27 -19
- data/bin/openapi-order +573 -0
- 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 +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 830a477c8647ad883e5a6619767f19cfae7c2d9b9baa100b0e10bdd911ef0fc1
|
|
4
|
+
data.tar.gz: 9e4a89d44c344c0099f826bf08c459b2eaa448e4d8b76b982e7ee8b7146199b2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eef01e1c3095d747213533d8ceeb4d0ba4107970b6541632b92b9ffaa0a0202d583ec6934f2af4af3dbf0dff4d0344e053bbf9201d8128cd474406fe11a8b308
|
|
7
|
+
data.tar.gz: 92fcbc4c9128e213ee4d67d7eebe69c5de9f5ef2576a0efdeae33b2f512a786d586e0eebe903fc932b43f59475ecbd780d925263adbd9f5039b113292db8ae97
|
data/bin/openapi-addschemas
CHANGED
|
@@ -29,31 +29,39 @@ def replace_inlines(obj, components, top_level_name = nil)
|
|
|
29
29
|
return true
|
|
30
30
|
end
|
|
31
31
|
# Is inlined, process parts recursively.
|
|
32
|
-
items
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
%w[allOf anyOf oneOf items prefixItems additionalProperties unevaluatedItems
|
|
33
|
+
unevaluatedProperties if then else not
|
|
34
|
+
].each do |prop_name|
|
|
35
|
+
return false unless replace_inlines(obj[prop_name], components)
|
|
36
|
+
end
|
|
37
|
+
%w[properties patternProperties dependentSchemas].each do |prop_name|
|
|
38
|
+
props = obj[prop_name]
|
|
39
|
+
next if props.nil?
|
|
40
|
+
props.keys.sort!.each do |name|
|
|
41
|
+
return false unless replace_inlines(props[name], components)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
looks_like_type = false
|
|
45
|
+
# required is omitted since it is in schema and in parameters.
|
|
46
|
+
%w[type items prefixItems properties patternProperties additionalProperties
|
|
47
|
+
unevaluatedItems unevaluatedProperties enum const minimum maximum multipleOf
|
|
48
|
+
exclusiveMinimum exclusiveMaximum maxLength minLength pattern maxItems minItems
|
|
49
|
+
uniqueItems maxContains minContains maxProperties minProperties propertyNames
|
|
50
|
+
dependentRequired format contentEncoding contentMediaType contentSchema
|
|
51
|
+
].each do |kw|
|
|
52
|
+
if obj.key?(kw)
|
|
53
|
+
looks_like_type = true
|
|
54
|
+
break
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
unless looks_like_type
|
|
58
|
+
# Some kind of intermediate-level object.
|
|
39
59
|
(obj.keys.sort! { |a, b| a.to_s <=> b.to_s }).each do |key|
|
|
40
60
|
next if key == 'securitySchemes'
|
|
41
61
|
return false unless replace_inlines(obj[key], components)
|
|
42
62
|
end
|
|
43
63
|
return true
|
|
44
64
|
end
|
|
45
|
-
case t
|
|
46
|
-
when 'array'
|
|
47
|
-
return false unless replace_inlines(obj['items'], components)
|
|
48
|
-
when 'object'
|
|
49
|
-
%w[properties patternProperties].each do |prop_name|
|
|
50
|
-
props = obj.fetch(prop_name, {})
|
|
51
|
-
props.keys.sort!.each do |name|
|
|
52
|
-
return false unless replace_inlines(props[name], components)
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
return false unless replace_inlines(obj['additionalProperties'], components)
|
|
56
|
-
end
|
|
57
65
|
r = components.ref_string(top_level_name) || components.reference(obj)
|
|
58
66
|
components.store_anchor(obj, r)
|
|
59
67
|
components.to_reference_object(obj, r) if top_level_name.nil?
|
data/bin/openapi-order
ADDED
|
@@ -0,0 +1,573 @@
|
|
|
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/order'
|
|
9
|
+
require 'optparse'
|
|
10
|
+
require 'yaml'
|
|
11
|
+
require 'json'
|
|
12
|
+
|
|
13
|
+
# Functions related to order instructions.
|
|
14
|
+
|
|
15
|
+
def metadata
|
|
16
|
+
'openapi-order-metadata'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def metadata_key
|
|
20
|
+
'key'
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def metadata_regexp
|
|
24
|
+
'regexp'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def default_order_data(order_key, regexp_prefix)
|
|
28
|
+
<<EOF
|
|
29
|
+
---
|
|
30
|
+
#{metadata}:
|
|
31
|
+
#{metadata_key}: "#{order_key}"
|
|
32
|
+
#{metadata_regexp}: "#{regexp_prefix}"
|
|
33
|
+
# Generic orders for any location of a key.
|
|
34
|
+
'**':
|
|
35
|
+
servers:
|
|
36
|
+
#{order_key}: &ServerObject_order
|
|
37
|
+
- url
|
|
38
|
+
- name
|
|
39
|
+
- description
|
|
40
|
+
- variables
|
|
41
|
+
'*':
|
|
42
|
+
#{order_key}: &ServerObject
|
|
43
|
+
- name
|
|
44
|
+
- description
|
|
45
|
+
variables:
|
|
46
|
+
#{order_key}:
|
|
47
|
+
- '*'
|
|
48
|
+
'*':
|
|
49
|
+
#{order_key}: &ServerVariableObject
|
|
50
|
+
- description
|
|
51
|
+
- enum
|
|
52
|
+
- default
|
|
53
|
+
server: *ServerObject
|
|
54
|
+
parameters:
|
|
55
|
+
#{order_key}: &ParameterObject_order
|
|
56
|
+
- name
|
|
57
|
+
- in
|
|
58
|
+
'*': &ParameterObject
|
|
59
|
+
#{order_key}:
|
|
60
|
+
- '$ref'
|
|
61
|
+
- name
|
|
62
|
+
- in
|
|
63
|
+
- required
|
|
64
|
+
- deprecated
|
|
65
|
+
- description
|
|
66
|
+
- content
|
|
67
|
+
- schema
|
|
68
|
+
- '*'
|
|
69
|
+
- #{regexp_prefix}^example[s]?$
|
|
70
|
+
schema: &Schema
|
|
71
|
+
#{order_key}:
|
|
72
|
+
- '$ref'
|
|
73
|
+
- title
|
|
74
|
+
- description
|
|
75
|
+
- deprecated
|
|
76
|
+
- type
|
|
77
|
+
- format
|
|
78
|
+
- enum
|
|
79
|
+
- const
|
|
80
|
+
- default
|
|
81
|
+
- readOnly
|
|
82
|
+
- writeOnly
|
|
83
|
+
# Numeric types.
|
|
84
|
+
- minimum
|
|
85
|
+
- exclusiveMinimum
|
|
86
|
+
- exclusiveMaximum
|
|
87
|
+
- maximum
|
|
88
|
+
- multipleOf
|
|
89
|
+
# Strings.
|
|
90
|
+
- minLength
|
|
91
|
+
- maxLength
|
|
92
|
+
- pattern
|
|
93
|
+
# Core.
|
|
94
|
+
- allOf
|
|
95
|
+
- anyOf
|
|
96
|
+
- oneOf
|
|
97
|
+
- not
|
|
98
|
+
- if
|
|
99
|
+
- then
|
|
100
|
+
- else
|
|
101
|
+
# Objects.
|
|
102
|
+
- required
|
|
103
|
+
- dependentRequired
|
|
104
|
+
- minProperties
|
|
105
|
+
- maxProperties
|
|
106
|
+
- properties
|
|
107
|
+
- #{regexp_prefix}^.+Properties$
|
|
108
|
+
- propertyNames
|
|
109
|
+
- dependentSchemas
|
|
110
|
+
- discriminator
|
|
111
|
+
# Arrays.
|
|
112
|
+
- minItems
|
|
113
|
+
- maxItems
|
|
114
|
+
- uniqueItems
|
|
115
|
+
- items
|
|
116
|
+
- #{regexp_prefix}^.+Items$
|
|
117
|
+
- contains
|
|
118
|
+
- minContains
|
|
119
|
+
- maxContains
|
|
120
|
+
# Content
|
|
121
|
+
- #{regexp_prefix}^content.*$
|
|
122
|
+
- examples
|
|
123
|
+
required:
|
|
124
|
+
#{order_key}:
|
|
125
|
+
- '*'
|
|
126
|
+
dependentRequired:
|
|
127
|
+
#{order_key}:
|
|
128
|
+
- '*'
|
|
129
|
+
'*':
|
|
130
|
+
#{order_key}:
|
|
131
|
+
- '*'
|
|
132
|
+
allOf: &Schema_map
|
|
133
|
+
'*': *Schema
|
|
134
|
+
anyOf: *Schema_map
|
|
135
|
+
oneOf: *Schema_map
|
|
136
|
+
not: *Schema
|
|
137
|
+
if: *Schema
|
|
138
|
+
then: *Schema
|
|
139
|
+
else: *Schema
|
|
140
|
+
properties: &Schema_ordered_map
|
|
141
|
+
#{order_key}:
|
|
142
|
+
- '*'
|
|
143
|
+
'*': *Schema
|
|
144
|
+
patternProperties: *Schema_ordered_map
|
|
145
|
+
additionalProperties: *Schema
|
|
146
|
+
propertyNames: *Schema
|
|
147
|
+
items: *Schema
|
|
148
|
+
contains: *Schema
|
|
149
|
+
prefixItems: *Schema_map
|
|
150
|
+
unevaluatedItems: *Schema
|
|
151
|
+
unevaluatedProperties: *Schema
|
|
152
|
+
dependentSchemas: *Schema_ordered_map
|
|
153
|
+
discriminator:
|
|
154
|
+
#{order_key}:
|
|
155
|
+
- propertyName
|
|
156
|
+
- mapping
|
|
157
|
+
- defaultMapping
|
|
158
|
+
content:
|
|
159
|
+
#{order_key}:
|
|
160
|
+
- '*'
|
|
161
|
+
'*': &MediaTypeObject
|
|
162
|
+
#{order_key}:
|
|
163
|
+
- schema
|
|
164
|
+
- #{regexp_prefix}.*Schema$
|
|
165
|
+
- #{regexp_prefix}^(.*E|e)ncoding$
|
|
166
|
+
- '*'
|
|
167
|
+
- #{regexp_prefix}^example[s]?$
|
|
168
|
+
headers: &HeaderObject
|
|
169
|
+
#{order_key}:
|
|
170
|
+
- description
|
|
171
|
+
- required
|
|
172
|
+
- '*'
|
|
173
|
+
- schema
|
|
174
|
+
'*': *ParameterObject
|
|
175
|
+
responses:
|
|
176
|
+
#{order_key}:
|
|
177
|
+
- '*'
|
|
178
|
+
- default
|
|
179
|
+
'*': &ResponseObject
|
|
180
|
+
#{order_key}:
|
|
181
|
+
- '$ref'
|
|
182
|
+
- summary
|
|
183
|
+
- description
|
|
184
|
+
- headers
|
|
185
|
+
- content
|
|
186
|
+
- links
|
|
187
|
+
links:
|
|
188
|
+
#{order_key}:
|
|
189
|
+
- '*'
|
|
190
|
+
'*': &LinkObject
|
|
191
|
+
#{order_key}:
|
|
192
|
+
- description
|
|
193
|
+
- #{regexp_prefix}^operation.*
|
|
194
|
+
- parameters
|
|
195
|
+
- requestBody
|
|
196
|
+
requestBody: &RequestBodyObject
|
|
197
|
+
#{order_key}:
|
|
198
|
+
- description
|
|
199
|
+
- required
|
|
200
|
+
- content
|
|
201
|
+
security:
|
|
202
|
+
#{order_key}: &SecurityRequirementObject_order
|
|
203
|
+
- '*'
|
|
204
|
+
webhooks:
|
|
205
|
+
#{order_key}:
|
|
206
|
+
- '*'
|
|
207
|
+
'*': &PathItemObject
|
|
208
|
+
#{order_key}:
|
|
209
|
+
- '$ref'
|
|
210
|
+
- summary
|
|
211
|
+
- description
|
|
212
|
+
- servers
|
|
213
|
+
- parameters
|
|
214
|
+
- '*'
|
|
215
|
+
- additionalOperations
|
|
216
|
+
'*':
|
|
217
|
+
#{order_key}: &OperationObject
|
|
218
|
+
- '$ref'
|
|
219
|
+
- operationId
|
|
220
|
+
- summary
|
|
221
|
+
- description
|
|
222
|
+
- parameters
|
|
223
|
+
- requestBody
|
|
224
|
+
- responses
|
|
225
|
+
additionalOperations:
|
|
226
|
+
#{order_key}:
|
|
227
|
+
- '*'
|
|
228
|
+
'*': *OperationObject
|
|
229
|
+
callbacks:
|
|
230
|
+
#{order_key}:
|
|
231
|
+
- '*'
|
|
232
|
+
'*': *PathItemObject
|
|
233
|
+
#{order_key}:
|
|
234
|
+
- openapi
|
|
235
|
+
- '$self'
|
|
236
|
+
- jsonSchemaDialect
|
|
237
|
+
- info
|
|
238
|
+
- servers
|
|
239
|
+
- '*'
|
|
240
|
+
- paths
|
|
241
|
+
- components
|
|
242
|
+
info:
|
|
243
|
+
#{order_key}:
|
|
244
|
+
- title
|
|
245
|
+
- version
|
|
246
|
+
- summary
|
|
247
|
+
- description
|
|
248
|
+
- contact
|
|
249
|
+
- license
|
|
250
|
+
- termsOfService
|
|
251
|
+
contact:
|
|
252
|
+
#{order_key}:
|
|
253
|
+
- name
|
|
254
|
+
license:
|
|
255
|
+
#{order_key}:
|
|
256
|
+
- name
|
|
257
|
+
servers:
|
|
258
|
+
#{order_key}: *ServerObject_order
|
|
259
|
+
'*': *ServerObject
|
|
260
|
+
security:
|
|
261
|
+
#{order_key}: *SecurityRequirementObject_order
|
|
262
|
+
tags: # Do not order tags array but do order Tag Objects.
|
|
263
|
+
'*':
|
|
264
|
+
#{order_key}:
|
|
265
|
+
- name
|
|
266
|
+
- summary
|
|
267
|
+
- description
|
|
268
|
+
paths:
|
|
269
|
+
#{order_key}:
|
|
270
|
+
- '*'
|
|
271
|
+
'*': *PathItemObject
|
|
272
|
+
components:
|
|
273
|
+
#{order_key}:
|
|
274
|
+
- schemas
|
|
275
|
+
- '*'
|
|
276
|
+
- securitySchemes
|
|
277
|
+
- links
|
|
278
|
+
- callbacks
|
|
279
|
+
- examples
|
|
280
|
+
# Regular expressions are for names from various openapi-add* programs.
|
|
281
|
+
# Variations for number of digits ensure ordering by number.
|
|
282
|
+
schemas:
|
|
283
|
+
#{order_key}:
|
|
284
|
+
- '*'
|
|
285
|
+
- #{regexp_prefix}^Schema[0-9]{1}x$
|
|
286
|
+
- #{regexp_prefix}^Schema[0-9]{2}x$
|
|
287
|
+
- #{regexp_prefix}^Schema[0-9]{3,}x$
|
|
288
|
+
'*': *Schema
|
|
289
|
+
responses:
|
|
290
|
+
#{order_key}:
|
|
291
|
+
- '*'
|
|
292
|
+
- #{regexp_prefix}^Response[0-9]{1}x$
|
|
293
|
+
- #{regexp_prefix}^Response[0-9]{2}x$
|
|
294
|
+
- #{regexp_prefix}^Response[0-9]{3,}x$
|
|
295
|
+
'*': *ResponseObject
|
|
296
|
+
parameters:
|
|
297
|
+
#{order_key}:
|
|
298
|
+
- '*'
|
|
299
|
+
- #{regexp_prefix}^Parameter[0-9]{1}x$
|
|
300
|
+
- #{regexp_prefix}^Parameter[0-9]{2}x$
|
|
301
|
+
- #{regexp_prefix}^Parameter[0-9]{3,}x$
|
|
302
|
+
'*': *ParameterObject
|
|
303
|
+
requestBodies:
|
|
304
|
+
#{order_key}:
|
|
305
|
+
- '*'
|
|
306
|
+
- #{regexp_prefix}^RequestBody[0-9]{1}x$
|
|
307
|
+
- #{regexp_prefix}^RequestBody[0-9]{2}x$
|
|
308
|
+
- #{regexp_prefix}^RequestBody[0-9]{3,}x$
|
|
309
|
+
'*': *RequestBodyObject
|
|
310
|
+
headers:
|
|
311
|
+
#{order_key}:
|
|
312
|
+
- '*'
|
|
313
|
+
- #{regexp_prefix}^Header[0-9]{1}x$
|
|
314
|
+
- #{regexp_prefix}^Header[0-9]{2}x$
|
|
315
|
+
- #{regexp_prefix}^Header[0-9]{3,}x$
|
|
316
|
+
'*': *HeaderObject
|
|
317
|
+
securitySchemes:
|
|
318
|
+
#{order_key}:
|
|
319
|
+
- '*'
|
|
320
|
+
links:
|
|
321
|
+
#{order_key}:
|
|
322
|
+
- '*'
|
|
323
|
+
'*': *LinkObject
|
|
324
|
+
callbacks:
|
|
325
|
+
#{order_key}:
|
|
326
|
+
- '*'
|
|
327
|
+
'*': *PathItemObject
|
|
328
|
+
pathItems:
|
|
329
|
+
#{order_key}:
|
|
330
|
+
- '*'
|
|
331
|
+
'*': *PathItemObject
|
|
332
|
+
mediaTypes:
|
|
333
|
+
#{order_key}:
|
|
334
|
+
- '*'
|
|
335
|
+
- #{regexp_prefix}^MediaType[0-9]{1}x$
|
|
336
|
+
- #{regexp_prefix}^MediaType[0-9]{2}x$
|
|
337
|
+
- #{regexp_prefix}^MediaType[0-9]{3,}x$
|
|
338
|
+
'*': *MediaTypeObject
|
|
339
|
+
examples:
|
|
340
|
+
#{order_key}:
|
|
341
|
+
- '*'
|
|
342
|
+
EOF
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def default_order(order_key, regexp_prefix)
|
|
346
|
+
YAML.load(default_order_data(order_key, regexp_prefix), aliases: true)
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# This needs to extract all path/order pairs, store and sort.
|
|
350
|
+
|
|
351
|
+
def extract_orders(path, order, collection, order_key, regexp_prefix)
|
|
352
|
+
return true unless order.is_a?(Hash)
|
|
353
|
+
order.each do |key, value|
|
|
354
|
+
if key == order_key
|
|
355
|
+
unless value.is_a?(Array)
|
|
356
|
+
OpenAPISourceTools::Common.aargh("Not an array: #{path.join('.')}.#{order_key} =\n#{YAML.dump(value)}")
|
|
357
|
+
return false
|
|
358
|
+
end
|
|
359
|
+
value.each_with_index do |v, k|
|
|
360
|
+
next if v.is_a?(String)
|
|
361
|
+
OpenAPISourceTools::Common.aargh("Not a string: #{path}.#{order_key}[#{k}] =\n#{YAML.dump(v)}")
|
|
362
|
+
return false
|
|
363
|
+
end
|
|
364
|
+
key_path = OpenAPISourceTools::Ordering::KeyPath.new(path, regexp_prefix)
|
|
365
|
+
key_order = OpenAPISourceTools::Ordering::Order.new(value, regexp_prefix)
|
|
366
|
+
collection.push(OpenAPISourceTools::Ordering::KeyPathOrder.new(key_path, key_order))
|
|
367
|
+
else
|
|
368
|
+
key = key.to_s unless key.is_a?(String)
|
|
369
|
+
path.push(key)
|
|
370
|
+
return false unless extract_orders(path, value, collection, order_key, regexp_prefix)
|
|
371
|
+
path.pop
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Functions related to applying the order to the document.
|
|
377
|
+
|
|
378
|
+
def reorder(orders, path, item)
|
|
379
|
+
return false unless OpenAPISourceTools::Ordering::Order.orderable?(item)
|
|
380
|
+
orders.each do |order|
|
|
381
|
+
next unless order.path.match?(path)
|
|
382
|
+
order.order.apply(item)
|
|
383
|
+
break
|
|
384
|
+
end
|
|
385
|
+
true
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def order_document(orders, path, item)
|
|
389
|
+
return unless reorder(orders, path, item) # Scalars do not pass.
|
|
390
|
+
if item.is_a?(Array)
|
|
391
|
+
item.each_with_index do |element, idx|
|
|
392
|
+
# This needs something and using index as string matches the '*'
|
|
393
|
+
# used when elements of the array are meant to be processed. By
|
|
394
|
+
# using the index alone, the '*' can be replaced by a regular
|
|
395
|
+
# expression that matches specific indices, if so desired.
|
|
396
|
+
path.push(idx.to_s)
|
|
397
|
+
order_document(orders, path, element)
|
|
398
|
+
path.pop
|
|
399
|
+
end
|
|
400
|
+
else
|
|
401
|
+
item.each do |key, value|
|
|
402
|
+
key = key.to_s unless key.is_a?(String)
|
|
403
|
+
path.push(key)
|
|
404
|
+
order_document(orders, path, value)
|
|
405
|
+
path.pop
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# Functions related to saving the output.
|
|
411
|
+
|
|
412
|
+
def to_json(doc, _line_width)
|
|
413
|
+
JSON.dump(doc)
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def to_pretty_json(doc, _line_width)
|
|
417
|
+
JSON.pretty_generate(doc)
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def to_yaml(doc, line_width)
|
|
421
|
+
YAML.dump(doc, line_width: line_width)
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def main
|
|
425
|
+
input_name = nil
|
|
426
|
+
output_name = nil
|
|
427
|
+
order_name = nil
|
|
428
|
+
order_key = 'x-order'
|
|
429
|
+
regexp_prefix = 're:'
|
|
430
|
+
format = 'yaml'
|
|
431
|
+
formats = {
|
|
432
|
+
'yaml' => method(:to_yaml),
|
|
433
|
+
'json' => method(:to_json),
|
|
434
|
+
'pretty_json' => method(:to_pretty_json)
|
|
435
|
+
}
|
|
436
|
+
line_width = 80
|
|
437
|
+
output_default = false
|
|
438
|
+
|
|
439
|
+
parser = OptionParser.new do |opts|
|
|
440
|
+
opts.summary_indent = ' '
|
|
441
|
+
opts.summary_width = 20
|
|
442
|
+
opts.banner = 'Usage: openapi-order [options]'
|
|
443
|
+
opts.separator ''
|
|
444
|
+
opts.separator 'Options:'
|
|
445
|
+
opts.on('-i', '--input FILE', 'Read API spec from FILE, not stdin.') do |f|
|
|
446
|
+
input_name = f
|
|
447
|
+
end
|
|
448
|
+
opts.on('-o', '--output FILE', 'Output to FILE, not stdout.') do |f|
|
|
449
|
+
output_name = f
|
|
450
|
+
end
|
|
451
|
+
opts.on('--order FILE', 'Read order instructions from FILE.') do |f|
|
|
452
|
+
order_name = f
|
|
453
|
+
end
|
|
454
|
+
opts.on('-f', '--format FORMAT', "Output format: #{formats.keys.sort!.join(', ')}, default: #{format}") do |f|
|
|
455
|
+
unless formats.key?(f.downcase)
|
|
456
|
+
exit(OpenAPISourceTools::Common.aargh("Unknown format: #{f}", 1))
|
|
457
|
+
end
|
|
458
|
+
format = f
|
|
459
|
+
end
|
|
460
|
+
opts.on('-w', '--width WIDTH', "Set YAML line width, default: #{line_width}") do |w|
|
|
461
|
+
line_width = w.to_i
|
|
462
|
+
end
|
|
463
|
+
opts.separator ''
|
|
464
|
+
opts.separator 'Order options:'
|
|
465
|
+
opts.on('--default', 'Output default order to --output, ignore input.') do
|
|
466
|
+
output_default = true
|
|
467
|
+
end
|
|
468
|
+
opts.on('-k', '--key KEY', "Use KEY as default order key, default: #{order_key}") do |k|
|
|
469
|
+
order_key = k
|
|
470
|
+
end
|
|
471
|
+
opts.on('-r', '--regexp PREFIX', "Use PREFIX to indicate regular expression, default: #{regexp_prefix}") do |p|
|
|
472
|
+
regexp_prefix = p
|
|
473
|
+
end
|
|
474
|
+
opts.on('-h', '--help', 'Print this help and exit.') do
|
|
475
|
+
$stdout.puts %(#{opts}
|
|
476
|
+
Loads YAML document and applies ordering to object keys, and arrays with
|
|
477
|
+
objects, when an order matches the key path to object.
|
|
478
|
+
|
|
479
|
+
Can be used on generic YAML or JSON files. Default order is for OpenAPI
|
|
480
|
+
documents.
|
|
481
|
+
|
|
482
|
+
The order instructions key "#{order_key}" value must be an array of strings.
|
|
483
|
+
The values can be any combination of the following:
|
|
484
|
+
- Literal key name.
|
|
485
|
+
- Regular expression starting with "#{regexp_prefix}".
|
|
486
|
+
- "*" that matches any key that does not match anything else.
|
|
487
|
+
First match against a value determines the position, and "*" receives all
|
|
488
|
+
unmatched keys. If order array has no "*", every unmatched key will be placed
|
|
489
|
+
last. Only the first "*" has any effect. Multiple keys placed in the same
|
|
490
|
+
position are sorted within that position.
|
|
491
|
+
|
|
492
|
+
The path to the "#{order_key}" can have any combination of the following:
|
|
493
|
+
- Literal key name.
|
|
494
|
+
- Regular expression starting with "#{regexp_prefix}".
|
|
495
|
+
- "*" that matches one key of any value.
|
|
496
|
+
- "**" that matches one or more keys of any value.
|
|
497
|
+
The path is used to determine if a value in the document should be ordered
|
|
498
|
+
using the instructions. At top level the path is empty and applies to the root
|
|
499
|
+
of the document. Long paths are considered to be more specific than short
|
|
500
|
+
paths, because they supposedly contain more literal keys or limiting regular
|
|
501
|
+
expressions so thay are intended for a specific case. Of paths of same length
|
|
502
|
+
one with "**" is considered less specific than one without.
|
|
503
|
+
|
|
504
|
+
The path to a value in the document is matched against paths to "#{order_key}"
|
|
505
|
+
to find the most specific order. For elements in an array, the last item in
|
|
506
|
+
the path is the element index as a string. Hence you can use a "*" parallel
|
|
507
|
+
to "#{order_key}" to match any index, but also literals or regular expressions
|
|
508
|
+
to match specific index can be used.
|
|
509
|
+
|
|
510
|
+
For object value in the document, the order is used to sort the keys of the
|
|
511
|
+
object. Absence of a key in the object has no effect. Any non-string key is
|
|
512
|
+
converted to string for ordering purposes. Original keys are retained.
|
|
513
|
+
|
|
514
|
+
For array value in the document, the order is used to select values of keys
|
|
515
|
+
from array elements to use in comparison for sorting the array.
|
|
516
|
+
- Using known object property names is recommended.
|
|
517
|
+
- Values of keys should be scalars and have the same type for same key.
|
|
518
|
+
- Missing key places the element after one that has the key.
|
|
519
|
+
- Object and array values are considered to be a missing value.
|
|
520
|
+
- Elements that are not objects end up last because they have no keys.
|
|
521
|
+
Mixture of objects, arrays, and scalars is not expected in the intended use.
|
|
522
|
+
|
|
523
|
+
Scalar value in the document is not affected.
|
|
524
|
+
|
|
525
|
+
Non-string keys in the document and order paths are converted to strings for
|
|
526
|
+
processing.
|
|
527
|
+
|
|
528
|
+
The --key, --regexp, --format, and --output apply to --default output. If
|
|
529
|
+
format is YAML, the internal YAML document is output as is.
|
|
530
|
+
|
|
531
|
+
To avoid errors, the value of --key should not appear in the document so that
|
|
532
|
+
it only appears as order instructions key in the order document.
|
|
533
|
+
|
|
534
|
+
In case the document has a key that starts with "#{regexp_prefix}" you can change it in the
|
|
535
|
+
order file or use "#{regexp_prefix}^#{regexp_prefix}the_rest$" as an anchored regular
|
|
536
|
+
expression that only matches the key. The ":" is part of the prefix itself,
|
|
537
|
+
so provide it if you want to keep it. Useful for readability.
|
|
538
|
+
|
|
539
|
+
When reading an order file, --key and --regexp are over-ridden by root-level
|
|
540
|
+
object "#{metadata}" keys "#{metadata_key}" and "#{metadata_regexp}" values.
|
|
541
|
+
)
|
|
542
|
+
exit 0
|
|
543
|
+
end
|
|
544
|
+
end
|
|
545
|
+
parser.order!
|
|
546
|
+
|
|
547
|
+
if output_default
|
|
548
|
+
if format == 'yaml'
|
|
549
|
+
doc = default_order_data(order_key, regexp_prefix)
|
|
550
|
+
else
|
|
551
|
+
doc = default_order(order_key, regexp_prefix)
|
|
552
|
+
doc = formats[format].call(doc, line_width)
|
|
553
|
+
end
|
|
554
|
+
else
|
|
555
|
+
doc = OpenAPISourceTools::Common.load_source(input_name)
|
|
556
|
+
return 2 if doc.nil?
|
|
557
|
+
|
|
558
|
+
order = order_name.nil? ? default_order(order_key, regexp_prefix) : OpenAPISourceTools::Common.load_source(order_name)
|
|
559
|
+
return 4 if order.nil?
|
|
560
|
+
md = order.fetch(metadata, {})
|
|
561
|
+
order_key = md.fetch(metadata_key, order_key)
|
|
562
|
+
regexp_prefix = md.fetch(metadata_regexp, regexp_prefix)
|
|
563
|
+
path_orders = []
|
|
564
|
+
return 4 unless extract_orders([], order, path_orders, order_key, regexp_prefix)
|
|
565
|
+
path_orders.sort!.reverse! # Longest key pattern sequence first.
|
|
566
|
+
order_document(path_orders, [], doc)
|
|
567
|
+
doc = formats[format].call(doc, line_width)
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
OpenAPISourceTools::Common.dump_result(output_name, doc, 3)
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
exit(main) if File.basename(__FILE__) == File.basename($PROGRAM_NAME)
|
|
@@ -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
|
|
@@ -47,6 +47,7 @@ executables:
|
|
|
47
47
|
- openapi-generate
|
|
48
48
|
- openapi-merge
|
|
49
49
|
- openapi-modifypaths
|
|
50
|
+
- openapi-order
|
|
50
51
|
- openapi-patterntests
|
|
51
52
|
- openapi-processpaths
|
|
52
53
|
extensions: []
|
|
@@ -64,6 +65,7 @@ files:
|
|
|
64
65
|
- bin/openapi-generate
|
|
65
66
|
- bin/openapi-merge
|
|
66
67
|
- bin/openapi-modifypaths
|
|
68
|
+
- bin/openapi-order
|
|
67
69
|
- bin/openapi-patterntests
|
|
68
70
|
- bin/openapi-processpaths
|
|
69
71
|
- lib/openapi/sourcetools.rb
|
|
@@ -75,6 +77,7 @@ files:
|
|
|
75
77
|
- lib/openapi/sourcetools/generate.rb
|
|
76
78
|
- lib/openapi/sourcetools/helper.rb
|
|
77
79
|
- lib/openapi/sourcetools/loaders.rb
|
|
80
|
+
- lib/openapi/sourcetools/order.rb
|
|
78
81
|
- lib/openapi/sourcetools/output.rb
|
|
79
82
|
- lib/openapi/sourcetools/task.rb
|
|
80
83
|
- lib/openapi/sourcetools/version.rb
|
|
@@ -90,7 +93,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
90
93
|
requirements:
|
|
91
94
|
- - ">="
|
|
92
95
|
- !ruby/object:Gem::Version
|
|
93
|
-
version: 3.
|
|
96
|
+
version: 3.3.11
|
|
94
97
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
95
98
|
requirements:
|
|
96
99
|
- - ">="
|