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.
- checksums.yaml +4 -4
- data/bin/openapi-addmediatypes +59 -0
- data/bin/openapi-addrequestbodies +57 -0
- data/bin/openapi-addschemas +30 -14
- data/bin/openapi-generate-checkconfig +73 -0
- data/bin/openapi-logicsplit +96 -0
- data/bin/openapi-order +21 -12
- data/bin/openapi-schema-in +143 -0
- data/bin/openapi-schema-ls +100 -0
- data/bin/openapi-schema-out +144 -0
- data/lib/openapi/sourcetools/apiobjects.rb +19 -13
- data/lib/openapi/sourcetools/common.rb +1 -1
- data/lib/openapi/sourcetools/config.rb +3 -2
- data/lib/openapi/sourcetools/generate.rb +1 -0
- data/lib/openapi/sourcetools/loaders.rb +3 -3
- data/lib/openapi/sourcetools/order.rb +42 -18
- data/lib/openapi/sourcetools/schemaio.rb +264 -0
- data/lib/openapi/sourcetools/task.rb +1 -1
- data/lib/openapi/sourcetools/version.rb +2 -1
- metadata +17 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: edfaf16a37a66c6f64cddb2580cd5a85c6daf21937a403358ea03d28b35a47d6
|
|
4
|
+
data.tar.gz: 6e81dcc098183b9547fbc9dd956fb0f11e2ccf556dbf7d8baaf73821bfe9334e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 13c4fd8d5a67c9af5072a8d7e29bee3bec6a82e0a7e9a39284e492871cfb7f9dfca736293d2218df5d91efa2fc200ec0c90b0e8887e0991793dec11a714a692f
|
|
7
|
+
data.tar.gz: 22ff4d3552d478a72d1e7b5c5d413e4de0c912479da1469ebbb3afa3311e23bd3cb769c69ec40547448e63cd4fbde038383cfef1122fb4a3a8de5297d14d1134
|
|
@@ -0,0 +1,59 @@
|
|
|
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/apiobjects'
|
|
8
|
+
require_relative '../lib/openapi/sourcetools/common'
|
|
9
|
+
require 'optparse'
|
|
10
|
+
require 'yaml'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main
|
|
14
|
+
input_name = nil
|
|
15
|
+
output_name = nil
|
|
16
|
+
path = %w[components mediaTypes]
|
|
17
|
+
components = OpenAPISourceTools::ApiObjects::Components.new(path, 'MediaType')
|
|
18
|
+
replacer = OpenAPISourceTools::ApiObjects::ValueSubValueReplacer.new('content', components)
|
|
19
|
+
|
|
20
|
+
parser = OptionParser.new do |opts|
|
|
21
|
+
opts.summary_indent = ' '
|
|
22
|
+
opts.summary_width = 26
|
|
23
|
+
opts.banner = 'Usage: openapi-addmediatypes [options]'
|
|
24
|
+
opts.separator ''
|
|
25
|
+
opts.separator 'Options:'
|
|
26
|
+
opts.on('-i', '--input FILE', 'Read API spec from FILE, not stdin.') do |f|
|
|
27
|
+
input_name = f
|
|
28
|
+
end
|
|
29
|
+
opts.on('-o', '--output FILE', 'Output to FILE, not stdout.') do |f|
|
|
30
|
+
output_name = f
|
|
31
|
+
end
|
|
32
|
+
components.add_options(opts)
|
|
33
|
+
opts.on('-h', '--help', 'Print this help and exit.') do
|
|
34
|
+
$stdout.puts %(#{opts}
|
|
35
|
+
|
|
36
|
+
Loads API document in OpenAPI format and moves media types under components
|
|
37
|
+
and replaces the original with reference.
|
|
38
|
+
|
|
39
|
+
#{components.help}
|
|
40
|
+
)
|
|
41
|
+
exit 0
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
parser.order!
|
|
45
|
+
|
|
46
|
+
doc = OpenAPISourceTools::Common.load_source(input_name)
|
|
47
|
+
return 2 if doc.nil?
|
|
48
|
+
|
|
49
|
+
components.items = doc.dig(*path) || {}
|
|
50
|
+
replacer.replace(doc.dig('components', 'headers') || {})
|
|
51
|
+
replacer.replace(doc.dig('components', 'responses') || {})
|
|
52
|
+
replacer.replace(doc.dig('components', 'parameters') || {})
|
|
53
|
+
replacer.replace(doc.fetch('paths', {}))
|
|
54
|
+
OpenAPISourceTools::Common.bury(doc, path, components.items) unless components.items.empty?
|
|
55
|
+
|
|
56
|
+
OpenAPISourceTools::Common.dump_result(output_name, doc, 3)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
exit(main) if File.basename(__FILE__) == File.basename($PROGRAM_NAME)
|
|
@@ -0,0 +1,57 @@
|
|
|
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/apiobjects'
|
|
8
|
+
require_relative '../lib/openapi/sourcetools/common'
|
|
9
|
+
require 'optparse'
|
|
10
|
+
require 'yaml'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main
|
|
14
|
+
input_name = nil
|
|
15
|
+
output_name = nil
|
|
16
|
+
path = %w[components requestBodies]
|
|
17
|
+
components = OpenAPISourceTools::ApiObjects::Components.new(path, 'RequestBody')
|
|
18
|
+
replacer = OpenAPISourceTools::ApiObjects::ValueReplacer.new('requestBody', components)
|
|
19
|
+
|
|
20
|
+
parser = OptionParser.new do |opts|
|
|
21
|
+
opts.summary_indent = ' '
|
|
22
|
+
opts.summary_width = 26
|
|
23
|
+
opts.banner = 'Usage: openapi-addrequestbodies [options]'
|
|
24
|
+
opts.separator ''
|
|
25
|
+
opts.separator 'Options:'
|
|
26
|
+
opts.on('-i', '--input FILE', 'Read API spec from FILE, not stdin.') do |f|
|
|
27
|
+
input_name = f
|
|
28
|
+
end
|
|
29
|
+
opts.on('-o', '--output FILE', 'Output to FILE, not stdout.') do |f|
|
|
30
|
+
output_name = f
|
|
31
|
+
end
|
|
32
|
+
components.add_options(opts)
|
|
33
|
+
opts.on('-h', '--help', 'Print this help and exit.') do
|
|
34
|
+
$stdout.puts %(#{opts}
|
|
35
|
+
|
|
36
|
+
Loads API document in OpenAPI format and moves request body under components
|
|
37
|
+
and replaces the original with reference.
|
|
38
|
+
|
|
39
|
+
#{components.help}
|
|
40
|
+
)
|
|
41
|
+
exit 0
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
parser.order!
|
|
45
|
+
|
|
46
|
+
doc = OpenAPISourceTools::Common.load_source(input_name)
|
|
47
|
+
return 2 if doc.nil?
|
|
48
|
+
|
|
49
|
+
components.items = doc.dig(*path) || {}
|
|
50
|
+
replacer.replace(doc.dig('components', 'pathItems') || {})
|
|
51
|
+
replacer.replace(doc.fetch('paths', {}))
|
|
52
|
+
OpenAPISourceTools::Common.bury(doc, path, components.items) unless components.items.empty?
|
|
53
|
+
|
|
54
|
+
OpenAPISourceTools::Common.dump_result(output_name, doc, 3)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
exit(main) if File.basename(__FILE__) == File.basename($PROGRAM_NAME)
|
data/bin/openapi-addschemas
CHANGED
|
@@ -16,6 +16,14 @@ def remove_subitem(obj, path)
|
|
|
16
16
|
parent.delete(path.last) || {}
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
def replace_child_inlines(obj, components)
|
|
20
|
+
return true unless obj.is_a?(Hash)
|
|
21
|
+
obj.keys.sort!.each do |name|
|
|
22
|
+
return false unless replace_inlines(obj[name], components)
|
|
23
|
+
end
|
|
24
|
+
true
|
|
25
|
+
end
|
|
26
|
+
|
|
19
27
|
def replace_inlines(obj, components, top_level_name = nil)
|
|
20
28
|
if obj.is_a?(Array)
|
|
21
29
|
obj.each do |o|
|
|
@@ -29,22 +37,30 @@ def replace_inlines(obj, components, top_level_name = nil)
|
|
|
29
37
|
return true
|
|
30
38
|
end
|
|
31
39
|
# Is inlined, process parts recursively.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
40
|
+
looks_like_type = false
|
|
41
|
+
# Array-valued.
|
|
42
|
+
OpenAPISourceTools::Schemas::ARRAY_VALUED.each do |prop_name|
|
|
43
|
+
value = obj[prop_name]
|
|
44
|
+
next unless value.is_a?(Array)
|
|
45
|
+
return false unless replace_inlines(value, components)
|
|
46
|
+
looks_like_type = true
|
|
47
|
+
end
|
|
48
|
+
# Other-valued things.
|
|
49
|
+
OpenAPISourceTools::Schemas::OTHER_VALUED.each do |prop_name|
|
|
50
|
+
value = obj[prop_name]
|
|
51
|
+
next if value.nil? || value.is_a?(Array)
|
|
52
|
+
return false unless replace_inlines(value, components)
|
|
53
|
+
looks_like_type = true
|
|
36
54
|
end
|
|
37
|
-
|
|
55
|
+
# Keys of these have object values that should be processed.
|
|
56
|
+
OpenAPISourceTools::Schemas::KEY2SCHEMA.each do |prop_name|
|
|
38
57
|
props = obj[prop_name]
|
|
39
|
-
next if props.nil?
|
|
40
|
-
props
|
|
41
|
-
|
|
42
|
-
end
|
|
58
|
+
next if props.nil? || !props.is_a?(Hash)
|
|
59
|
+
return false unless replace_child_inlines(props, components)
|
|
60
|
+
looks_like_type = true
|
|
43
61
|
end
|
|
44
|
-
looks_like_type = false
|
|
45
62
|
# required is omitted since it is in schema and in parameters.
|
|
46
|
-
%w[type
|
|
47
|
-
unevaluatedItems unevaluatedProperties enum const minimum maximum multipleOf
|
|
63
|
+
%w[type enum const minimum maximum multipleOf
|
|
48
64
|
exclusiveMinimum exclusiveMaximum maxLength minLength pattern maxItems minItems
|
|
49
65
|
uniqueItems maxContains minContains maxProperties minProperties propertyNames
|
|
50
66
|
dependentRequired format contentEncoding contentMediaType contentSchema
|
|
@@ -56,8 +72,8 @@ def replace_inlines(obj, components, top_level_name = nil)
|
|
|
56
72
|
end
|
|
57
73
|
unless looks_like_type
|
|
58
74
|
# Some kind of intermediate-level object.
|
|
59
|
-
|
|
60
|
-
next if key == 'securitySchemes'
|
|
75
|
+
obj.keys.sort! { |a, b| a.to_s <=> b.to_s }.each do |key|
|
|
76
|
+
next if key == 'securitySchemes' || OpenAPISourceTools::Schemas::IGNORED.include?(key) || OpenAPISourceTools::Schemas::HANDLED.include?(key)
|
|
61
77
|
return false unless replace_inlines(obj[key], components)
|
|
62
78
|
end
|
|
63
79
|
return true
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Copyright © 2024-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/config'
|
|
9
|
+
require_relative '../lib/openapi/sourcetools/gen'
|
|
10
|
+
require 'optparse'
|
|
11
|
+
include OpenAPISourceTools
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main
|
|
15
|
+
prefix = nil
|
|
16
|
+
keys = nil
|
|
17
|
+
read = false
|
|
18
|
+
|
|
19
|
+
parser = OptionParser.new do |opts|
|
|
20
|
+
opts.summary_indent = ' '
|
|
21
|
+
opts.summary_width = 26
|
|
22
|
+
opts.banner = 'Usage: openapi-generate-checkconfig [options]'
|
|
23
|
+
opts.separator ''
|
|
24
|
+
opts.separator 'Options:'
|
|
25
|
+
opts.on('-c', '--config NAME', 'Use config prefix NAME. (config:NAME)') do |c|
|
|
26
|
+
prefix = c
|
|
27
|
+
end
|
|
28
|
+
opts.on('-s', '--separator STRING', 'Use separator STRING. (separator:STRING)') do |s|
|
|
29
|
+
Gen.separator = s unless s.empty?
|
|
30
|
+
end
|
|
31
|
+
opts.on('-r', '--[no-]read', "Show if file is read by read_contents, default #{Common.yesno(read)}.") do |b|
|
|
32
|
+
read = b
|
|
33
|
+
end
|
|
34
|
+
opts.on('-k', '--keys KEYS', 'Show all values of KEYS.') do |ks|
|
|
35
|
+
keys = ks.split('/').reject(&:empty?) unless ks.empty?
|
|
36
|
+
keys = nil if !keys.nil? && keys.empty?
|
|
37
|
+
end
|
|
38
|
+
opts.on('-h', '--help', 'Print this help and exit.') do
|
|
39
|
+
$stdout.puts %(#{opts}
|
|
40
|
+
Displays configurations files that are found using specific prefix and an
|
|
41
|
+
optional separator character or string.
|
|
42
|
+
|
|
43
|
+
If KEYS, separated by "/", is given, the value of that key path is displayed.
|
|
44
|
+
That and read check require the configuration files to be YAML/JSON.
|
|
45
|
+
)
|
|
46
|
+
exit 0
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
parser.order!
|
|
50
|
+
|
|
51
|
+
return Common.aargh('Config must be given.', 1) if prefix.nil?
|
|
52
|
+
cfgs = ConfigLoader.find_files(name_prefix: prefix)
|
|
53
|
+
ConfigLoader.read_contents(cfgs) if read || !keys.nil?
|
|
54
|
+
root_dir, _p = ConfigLoader.prepare_prefix(prefix, Gen.wd)
|
|
55
|
+
drop = root_dir.size + 1
|
|
56
|
+
cfgs.each do |cfg|
|
|
57
|
+
ks = cfg.keys.join('/')
|
|
58
|
+
info = "#{cfg.root}: #{ks.empty? ? '(root)' : ks}, #{cfg.path[drop..]}"
|
|
59
|
+
ki = nil
|
|
60
|
+
unless keys.nil? || cfg.content.nil?
|
|
61
|
+
v = cfg.content.dig(*keys)
|
|
62
|
+
ki = v.to_s unless v.nil?
|
|
63
|
+
end
|
|
64
|
+
ri = read ? !cfg.content.nil? : nil
|
|
65
|
+
next if (read || !keys.nil?) && ki.nil? && ri.nil?
|
|
66
|
+
puts info
|
|
67
|
+
puts " #{ki}" unless ki.nil?
|
|
68
|
+
puts " File is #{ri ? '' : 'not '}read." unless ri.nil?
|
|
69
|
+
end
|
|
70
|
+
0
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
exit(main) if File.basename(__FILE__) == File.basename($PROGRAM_NAME)
|
|
@@ -0,0 +1,96 @@
|
|
|
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/apiobjects'
|
|
8
|
+
require_relative '../lib/openapi/sourcetools/common'
|
|
9
|
+
require 'optparse'
|
|
10
|
+
include OpenAPISourceTools
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def mixture?(schema)
|
|
14
|
+
has_logic = false
|
|
15
|
+
has_other = false
|
|
16
|
+
schema.each_key do |key|
|
|
17
|
+
if OpenAPISourceTools::ApiObjects::LOGIC_KEYS.member?(key)
|
|
18
|
+
has_logic = true
|
|
19
|
+
return true if has_other
|
|
20
|
+
elsif !OpenAPISourceTools::ApiObjects::DOCUMENT_KEYS.include?(key)
|
|
21
|
+
has_other = true
|
|
22
|
+
return true if has_logic
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
has_logic && has_other
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def split(name, schema, added_schemas, schema_path)
|
|
29
|
+
logic = {}
|
|
30
|
+
other = {}
|
|
31
|
+
schema.each do |key, value|
|
|
32
|
+
if OpenAPISourceTools::ApiObjects::LOGIC_KEYS.member?(key)
|
|
33
|
+
logic[key] = value
|
|
34
|
+
else
|
|
35
|
+
other[key] = value
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
logic_name = "#{name}Logic"
|
|
39
|
+
other_name = "#{name}Other"
|
|
40
|
+
added_schemas[logic_name] = logic
|
|
41
|
+
added_schemas[other_name] = other
|
|
42
|
+
schema.clear
|
|
43
|
+
schema['allOf'] = [
|
|
44
|
+
{ '$ref' => "#{schema_path}/#{logic_name}" },
|
|
45
|
+
{ '$ref' => "#{schema_path}/#{other_name}" }
|
|
46
|
+
]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def main
|
|
50
|
+
input_name = nil
|
|
51
|
+
output_name = nil
|
|
52
|
+
schema_path = '#/components/schemas'
|
|
53
|
+
|
|
54
|
+
parser = OptionParser.new do |opts|
|
|
55
|
+
opts.summary_indent = ' '
|
|
56
|
+
opts.summary_width = 26
|
|
57
|
+
opts.banner = 'Usage: openapi-logicsplit [options]'
|
|
58
|
+
opts.separator ''
|
|
59
|
+
opts.separator 'Options:'
|
|
60
|
+
opts.on('-i', '--input FILE', 'Read API spec from FILE, not stdin.') do |f|
|
|
61
|
+
input_name = f
|
|
62
|
+
end
|
|
63
|
+
opts.on('-o', '--output FILE', 'Output to FILE, not stdout.') do |f|
|
|
64
|
+
output_name = f
|
|
65
|
+
end
|
|
66
|
+
opts.on('-h', '--help', 'Print this help and exit.') do
|
|
67
|
+
$stdout.puts %(#{opts}
|
|
68
|
+
|
|
69
|
+
Loads API document in OpenAPI format and splits each schema with a mixture of
|
|
70
|
+
logic keywords and other keywords so that the schema has only allOf with
|
|
71
|
+
references to logic and other schema. The aim is to simplify code generation.
|
|
72
|
+
|
|
73
|
+
The openapi-addschemas must have been run. Processes "#{schema_path}".
|
|
74
|
+
)
|
|
75
|
+
return 0
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
parser.order!
|
|
79
|
+
|
|
80
|
+
doc = Common.load_source(input_name)
|
|
81
|
+
return 2 if doc.nil?
|
|
82
|
+
|
|
83
|
+
path = schema_path.split('/').reject { |x| x == '#' || x.empty? }
|
|
84
|
+
schemas = doc.dig(*path)
|
|
85
|
+
added_schemas = {}
|
|
86
|
+
unless schemas.nil?
|
|
87
|
+
schemas.keys.sort.each do |name|
|
|
88
|
+
s = schemas[name]
|
|
89
|
+
split(name, s, added_schemas, schema_path) if mixture?(s)
|
|
90
|
+
end
|
|
91
|
+
schemas.merge!(added_schemas)
|
|
92
|
+
end
|
|
93
|
+
Common.dump_result(output_name, doc, 3)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
exit(main) if File.basename(__FILE__) == File.basename($PROGRAM_NAME)
|
data/bin/openapi-order
CHANGED
|
@@ -53,6 +53,7 @@ def default_order_data(order_key, regexp_prefix)
|
|
|
53
53
|
server: *ServerObject
|
|
54
54
|
parameters:
|
|
55
55
|
#{order_key}: &ParameterObject_order
|
|
56
|
+
- required: '>' # True before false.
|
|
56
57
|
- name
|
|
57
58
|
- in
|
|
58
59
|
'*': &ParameterObject
|
|
@@ -213,9 +214,10 @@ def default_order_data(order_key, regexp_prefix)
|
|
|
213
214
|
- parameters
|
|
214
215
|
- '*'
|
|
215
216
|
- additionalOperations
|
|
217
|
+
- re:^x-openapi-sourcetools.*$
|
|
216
218
|
'*':
|
|
217
219
|
#{order_key}: &OperationObject
|
|
218
|
-
- '
|
|
220
|
+
- '\$ref'
|
|
219
221
|
- operationId
|
|
220
222
|
- summary
|
|
221
223
|
- description
|
|
@@ -348,7 +350,7 @@ end
|
|
|
348
350
|
|
|
349
351
|
# This needs to extract all path/order pairs, store and sort.
|
|
350
352
|
|
|
351
|
-
def extract_orders(path, order, collection, order_key, regexp_prefix)
|
|
353
|
+
def extract_orders(path, order, collection, order_key, regexp_prefix, visited)
|
|
352
354
|
return true unless order.is_a?(Hash)
|
|
353
355
|
order.each do |key, value|
|
|
354
356
|
if key == order_key
|
|
@@ -356,9 +358,13 @@ def extract_orders(path, order, collection, order_key, regexp_prefix)
|
|
|
356
358
|
OpenAPISourceTools::Common.aargh("Not an array: #{path.join('.')}.#{order_key} =\n#{YAML.dump(value)}")
|
|
357
359
|
return false
|
|
358
360
|
end
|
|
361
|
+
return true if visited.member?(value.object_id)
|
|
362
|
+
visited.add(value.object_id)
|
|
359
363
|
value.each_with_index do |v, k|
|
|
360
|
-
|
|
361
|
-
|
|
364
|
+
ov = OpenAPISourceTools::Ordering::Order.ordervalue(v)
|
|
365
|
+
value[k] = ov
|
|
366
|
+
next unless ov.nil?
|
|
367
|
+
OpenAPISourceTools::Common.aargh("Not valid order value: #{path}.#{order_key}[#{k}] =\n#{YAML.dump(v)}")
|
|
362
368
|
return false
|
|
363
369
|
end
|
|
364
370
|
key_path = OpenAPISourceTools::Ordering::KeyPath.new(path, regexp_prefix)
|
|
@@ -367,7 +373,7 @@ def extract_orders(path, order, collection, order_key, regexp_prefix)
|
|
|
367
373
|
else
|
|
368
374
|
key = key.to_s unless key.is_a?(String)
|
|
369
375
|
path.push(key)
|
|
370
|
-
return false unless extract_orders(path, value, collection, order_key, regexp_prefix)
|
|
376
|
+
return false unless extract_orders(path, value, collection, order_key, regexp_prefix, visited)
|
|
371
377
|
path.pop
|
|
372
378
|
end
|
|
373
379
|
end
|
|
@@ -472,14 +478,17 @@ def main
|
|
|
472
478
|
regexp_prefix = p
|
|
473
479
|
end
|
|
474
480
|
opts.on('-h', '--help', 'Print this help and exit.') do
|
|
475
|
-
$stdout.puts
|
|
481
|
+
$stdout.puts <<EOF
|
|
482
|
+
#{opts}
|
|
476
483
|
Loads YAML document and applies ordering to object keys, and arrays with
|
|
477
484
|
objects, when an order matches the key path to object.
|
|
478
485
|
|
|
479
486
|
Can be used on generic YAML or JSON files. Default order is for OpenAPI
|
|
480
487
|
documents.
|
|
481
488
|
|
|
482
|
-
The order instructions key "#{order_key}" value must be an array of strings
|
|
489
|
+
The order instructions key "#{order_key}" value must be an array of strings
|
|
490
|
+
or single-item map from string to "<" or ">" to indicate ascending (default)
|
|
491
|
+
or descending order. Hence any 'key' is same as 'key: "<"'.
|
|
483
492
|
The values can be any combination of the following:
|
|
484
493
|
- Literal key name.
|
|
485
494
|
- Regular expression starting with "#{regexp_prefix}".
|
|
@@ -492,13 +501,13 @@ position are sorted within that position.
|
|
|
492
501
|
The path to the "#{order_key}" can have any combination of the following:
|
|
493
502
|
- Literal key name.
|
|
494
503
|
- Regular expression starting with "#{regexp_prefix}".
|
|
495
|
-
- "*" that matches one key of any
|
|
496
|
-
- "**" that matches one or more keys of any
|
|
504
|
+
- "*" that matches one key of any content.
|
|
505
|
+
- "**" that matches one or more keys of any content.
|
|
497
506
|
The path is used to determine if a value in the document should be ordered
|
|
498
507
|
using the instructions. At top level the path is empty and applies to the root
|
|
499
508
|
of the document. Long paths are considered to be more specific than short
|
|
500
509
|
paths, because they supposedly contain more literal keys or limiting regular
|
|
501
|
-
expressions so
|
|
510
|
+
expressions so they are intended for a specific case. Of paths of same length
|
|
502
511
|
one with "**" is considered less specific than one without.
|
|
503
512
|
|
|
504
513
|
The path to a value in the document is matched against paths to "#{order_key}"
|
|
@@ -538,7 +547,7 @@ so provide it if you want to keep it. Useful for readability.
|
|
|
538
547
|
|
|
539
548
|
When reading an order file, --key and --regexp are over-ridden by root-level
|
|
540
549
|
object "#{metadata}" keys "#{metadata_key}" and "#{metadata_regexp}" values.
|
|
541
|
-
|
|
550
|
+
EOF
|
|
542
551
|
exit 0
|
|
543
552
|
end
|
|
544
553
|
end
|
|
@@ -561,7 +570,7 @@ object "#{metadata}" keys "#{metadata_key}" and "#{metadata_regexp}" values.
|
|
|
561
570
|
order_key = md.fetch(metadata_key, order_key)
|
|
562
571
|
regexp_prefix = md.fetch(metadata_regexp, regexp_prefix)
|
|
563
572
|
path_orders = []
|
|
564
|
-
return 4 unless extract_orders([], order, path_orders, order_key, regexp_prefix)
|
|
573
|
+
return 4 unless extract_orders([], order, path_orders, order_key, regexp_prefix, Set.new)
|
|
565
574
|
path_orders.sort!.reverse! # Longest key pattern sequence first.
|
|
566
575
|
order_document(path_orders, [], doc)
|
|
567
576
|
doc = formats[format].call(doc, line_width)
|
|
@@ -0,0 +1,143 @@
|
|
|
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
|
+
include OpenAPISourceTools
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Functions related to saving the output.
|
|
14
|
+
|
|
15
|
+
def to_json(doc, _line_width)
|
|
16
|
+
JSON.dump(doc)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_pretty_json(doc, _line_width)
|
|
20
|
+
JSON.pretty_generate(doc)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_yaml(doc, line_width)
|
|
24
|
+
YAML.dump(doc, line_width: line_width)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def main
|
|
28
|
+
input_file = nil
|
|
29
|
+
schema_file = nil
|
|
30
|
+
output_file = nil
|
|
31
|
+
name = nil
|
|
32
|
+
refs = true
|
|
33
|
+
format = 'yaml'
|
|
34
|
+
formats = {
|
|
35
|
+
'yaml' => method(:to_yaml),
|
|
36
|
+
'json' => method(:to_json),
|
|
37
|
+
'pretty_json' => method(:to_pretty_json)
|
|
38
|
+
}
|
|
39
|
+
line_width = 80
|
|
40
|
+
doc_paths = :openapi
|
|
41
|
+
schema_paths = :jsonschema
|
|
42
|
+
drops = []
|
|
43
|
+
|
|
44
|
+
parser = OptionParser.new do |opts|
|
|
45
|
+
opts.summary_indent = ' '
|
|
46
|
+
opts.summary_width = 26
|
|
47
|
+
opts.banner = 'Usage: openapi-schema-in [options]'
|
|
48
|
+
opts.separator ''
|
|
49
|
+
opts.separator 'Options:'
|
|
50
|
+
opts.on('-i', '--input FILE', 'Read input from FILE, not stdin.') do |f|
|
|
51
|
+
input_file = f
|
|
52
|
+
end
|
|
53
|
+
opts.on('-s', '--schema FILE', 'Read schema from FILE, not stdin.') do |f|
|
|
54
|
+
schema_file = f
|
|
55
|
+
end
|
|
56
|
+
opts.on('-o', '--output FILE', 'Write result to FILE, not stdout.') do |f|
|
|
57
|
+
output_file = f
|
|
58
|
+
end
|
|
59
|
+
opts.on('-n', '--name NAME', 'Use schema NAME.') do |n|
|
|
60
|
+
name = n
|
|
61
|
+
end
|
|
62
|
+
opts.on('--[no-]refs', "Include referred schemas (default #{Common.yesno(refs)}).") do |r|
|
|
63
|
+
refs = r
|
|
64
|
+
end
|
|
65
|
+
opts.on('-f', '--format FORMAT', "Document output format: #{formats.keys.sort!.join(', ')}.") do |fmt|
|
|
66
|
+
format = fmt
|
|
67
|
+
end
|
|
68
|
+
opts.on('-w', '--width WIDTH', "Set YAML line width, default: #{line_width}") do |w|
|
|
69
|
+
line_width = w.to_i
|
|
70
|
+
end
|
|
71
|
+
opts.on('--inputpaths DATA', 'Input path data.') do |data|
|
|
72
|
+
doc_paths = data
|
|
73
|
+
end
|
|
74
|
+
opts.on('--schemapaths DATA', 'Schema path data.') do |data|
|
|
75
|
+
schema_paths = data
|
|
76
|
+
end
|
|
77
|
+
opts.on('-d', '--drop KEY', 'Drop KEY from chosen schema.') do |k|
|
|
78
|
+
drops.push(k)
|
|
79
|
+
end
|
|
80
|
+
opts.on('-h', '--help', 'Print this help and exit.') do
|
|
81
|
+
$stdout.puts <<EOF
|
|
82
|
+
#{opts}
|
|
83
|
+
Takes schema with name and places it into document.
|
|
84
|
+
|
|
85
|
+
The document and schema paths DATA is either a file name, internal data name,
|
|
86
|
+
or a JSON-encoded object containing paths data. Internal data names are:
|
|
87
|
+
#{SchemaIO::BUILTINS.keys.sort!.join(', ')}
|
|
88
|
+
|
|
89
|
+
Depending on input, schema NAME is used to find and store the schema in the
|
|
90
|
+
location indicated by DATA for input and output.
|
|
91
|
+
|
|
92
|
+
DATA values default to input and output being OpenAPI format, schema file is
|
|
93
|
+
JSON schema with the named schema at root.
|
|
94
|
+
|
|
95
|
+
Dropping keys can be used to remove for example "$schema" and "$id" keys from
|
|
96
|
+
the chosen schema, if present.
|
|
97
|
+
EOF
|
|
98
|
+
return 0
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
parser.order!
|
|
102
|
+
|
|
103
|
+
drops.sort!.uniq! unless drops.size < 2
|
|
104
|
+
|
|
105
|
+
return Common.aargh('Cannot read both document and schema from stdin.', 1) if input_file.nil? && schema_file.nil?
|
|
106
|
+
return Common.aargh('Schema name must be given.', 1) if name.nil?
|
|
107
|
+
|
|
108
|
+
return Common.aargh("Unknown format: #{format}", 1) unless formats.key?(format)
|
|
109
|
+
|
|
110
|
+
dp = SchemaIO.key_data(doc_paths)
|
|
111
|
+
return Common.aargh("Not file or internal format name or format as JSON object: #{doc_paths}", 2) if dp.nil?
|
|
112
|
+
sp = SchemaIO.key_data(schema_paths)
|
|
113
|
+
return Common.aargh("Not file or internal format name or format as JSON object: #{schema_paths}", 2) if sp.nil?
|
|
114
|
+
|
|
115
|
+
return Common.aargh(%(Name "#{name}" has document path separator "#{dp.separator}"), 1) if name.include?(dp.separator)
|
|
116
|
+
return Common.aargh(%(Name "#{name}" has schema path separator "#{sp.separator}"), 1) if name.include?(sp.separator)
|
|
117
|
+
|
|
118
|
+
doc = Common.load_source(input_file)
|
|
119
|
+
return 2 if doc.nil?
|
|
120
|
+
return Common.aargh('Document is not an object.', 2) unless doc.is_a?(Hash)
|
|
121
|
+
schema = Common.load_source(schema_file)
|
|
122
|
+
return 2 if schema.nil?
|
|
123
|
+
return Common.aargh('Schema is not an object.', 2) unless schema.is_a?(Hash)
|
|
124
|
+
|
|
125
|
+
tgt = SchemaIO::SchemaAccess.new(dp, doc)
|
|
126
|
+
src = SchemaIO::SchemaAccess.new(sp, schema)
|
|
127
|
+
|
|
128
|
+
desired = src.find_named_schema(name)
|
|
129
|
+
return Common.aargh("Schema not found: #{name}", 1) if desired.nil?
|
|
130
|
+
drops.each { |k| desired.delete(k) }
|
|
131
|
+
tgt.put_named_schema(name, desired)
|
|
132
|
+
|
|
133
|
+
if refs
|
|
134
|
+
refd = src.find_referenced_schemas(name, desired)
|
|
135
|
+
refd.each { |n, s| tgt.put_referenced_schema(n, s) }
|
|
136
|
+
end
|
|
137
|
+
SchemaIO.alter_refs(tgt.document, src, tgt) # After referenced schemas have been found.
|
|
138
|
+
|
|
139
|
+
out = formats[format].call(tgt.document, line_width)
|
|
140
|
+
Common.dump_result(output_file, out, 3)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
exit(main) if File.basename(__FILE__) == File.basename($PROGRAM_NAME)
|