openapi-sourcetools 0.12.1 → 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-addschemas +29 -13
- data/bin/openapi-logicsplit +96 -0
- data/bin/openapi-order +20 -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 +15 -3
- data/lib/openapi/sourcetools/order.rb +42 -18
- data/lib/openapi/sourcetools/schemaio.rb +264 -0
- data/lib/openapi/sourcetools/version.rb +1 -1
- metadata +10 -1
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
|
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
|
|
@@ -57,7 +73,7 @@ def replace_inlines(obj, components, top_level_name = nil)
|
|
|
57
73
|
unless looks_like_type
|
|
58
74
|
# Some kind of intermediate-level object.
|
|
59
75
|
obj.keys.sort! { |a, b| a.to_s <=> b.to_s }.each do |key|
|
|
60
|
-
next if key == 'securitySchemes'
|
|
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,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
|
|
@@ -216,7 +217,7 @@ def default_order_data(order_key, regexp_prefix)
|
|
|
216
217
|
- re:^x-openapi-sourcetools.*$
|
|
217
218
|
'*':
|
|
218
219
|
#{order_key}: &OperationObject
|
|
219
|
-
- '
|
|
220
|
+
- '\$ref'
|
|
220
221
|
- operationId
|
|
221
222
|
- summary
|
|
222
223
|
- description
|
|
@@ -349,7 +350,7 @@ end
|
|
|
349
350
|
|
|
350
351
|
# This needs to extract all path/order pairs, store and sort.
|
|
351
352
|
|
|
352
|
-
def extract_orders(path, order, collection, order_key, regexp_prefix)
|
|
353
|
+
def extract_orders(path, order, collection, order_key, regexp_prefix, visited)
|
|
353
354
|
return true unless order.is_a?(Hash)
|
|
354
355
|
order.each do |key, value|
|
|
355
356
|
if key == order_key
|
|
@@ -357,9 +358,13 @@ def extract_orders(path, order, collection, order_key, regexp_prefix)
|
|
|
357
358
|
OpenAPISourceTools::Common.aargh("Not an array: #{path.join('.')}.#{order_key} =\n#{YAML.dump(value)}")
|
|
358
359
|
return false
|
|
359
360
|
end
|
|
361
|
+
return true if visited.member?(value.object_id)
|
|
362
|
+
visited.add(value.object_id)
|
|
360
363
|
value.each_with_index do |v, k|
|
|
361
|
-
|
|
362
|
-
|
|
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)}")
|
|
363
368
|
return false
|
|
364
369
|
end
|
|
365
370
|
key_path = OpenAPISourceTools::Ordering::KeyPath.new(path, regexp_prefix)
|
|
@@ -368,7 +373,7 @@ def extract_orders(path, order, collection, order_key, regexp_prefix)
|
|
|
368
373
|
else
|
|
369
374
|
key = key.to_s unless key.is_a?(String)
|
|
370
375
|
path.push(key)
|
|
371
|
-
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)
|
|
372
377
|
path.pop
|
|
373
378
|
end
|
|
374
379
|
end
|
|
@@ -473,14 +478,17 @@ def main
|
|
|
473
478
|
regexp_prefix = p
|
|
474
479
|
end
|
|
475
480
|
opts.on('-h', '--help', 'Print this help and exit.') do
|
|
476
|
-
$stdout.puts
|
|
481
|
+
$stdout.puts <<EOF
|
|
482
|
+
#{opts}
|
|
477
483
|
Loads YAML document and applies ordering to object keys, and arrays with
|
|
478
484
|
objects, when an order matches the key path to object.
|
|
479
485
|
|
|
480
486
|
Can be used on generic YAML or JSON files. Default order is for OpenAPI
|
|
481
487
|
documents.
|
|
482
488
|
|
|
483
|
-
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: "<"'.
|
|
484
492
|
The values can be any combination of the following:
|
|
485
493
|
- Literal key name.
|
|
486
494
|
- Regular expression starting with "#{regexp_prefix}".
|
|
@@ -493,13 +501,13 @@ position are sorted within that position.
|
|
|
493
501
|
The path to the "#{order_key}" can have any combination of the following:
|
|
494
502
|
- Literal key name.
|
|
495
503
|
- Regular expression starting with "#{regexp_prefix}".
|
|
496
|
-
- "*" that matches one key of any
|
|
497
|
-
- "**" 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.
|
|
498
506
|
The path is used to determine if a value in the document should be ordered
|
|
499
507
|
using the instructions. At top level the path is empty and applies to the root
|
|
500
508
|
of the document. Long paths are considered to be more specific than short
|
|
501
509
|
paths, because they supposedly contain more literal keys or limiting regular
|
|
502
|
-
expressions so
|
|
510
|
+
expressions so they are intended for a specific case. Of paths of same length
|
|
503
511
|
one with "**" is considered less specific than one without.
|
|
504
512
|
|
|
505
513
|
The path to a value in the document is matched against paths to "#{order_key}"
|
|
@@ -539,7 +547,7 @@ so provide it if you want to keep it. Useful for readability.
|
|
|
539
547
|
|
|
540
548
|
When reading an order file, --key and --regexp are over-ridden by root-level
|
|
541
549
|
object "#{metadata}" keys "#{metadata_key}" and "#{metadata_regexp}" values.
|
|
542
|
-
|
|
550
|
+
EOF
|
|
543
551
|
exit 0
|
|
544
552
|
end
|
|
545
553
|
end
|
|
@@ -562,7 +570,7 @@ object "#{metadata}" keys "#{metadata_key}" and "#{metadata_regexp}" values.
|
|
|
562
570
|
order_key = md.fetch(metadata_key, order_key)
|
|
563
571
|
regexp_prefix = md.fetch(metadata_regexp, regexp_prefix)
|
|
564
572
|
path_orders = []
|
|
565
|
-
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)
|
|
566
574
|
path_orders.sort!.reverse! # Longest key pattern sequence first.
|
|
567
575
|
order_document(path_orders, [], doc)
|
|
568
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)
|
|
@@ -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-
|
|
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
|
-
|
|
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
|
|
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"
|
|
@@ -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
|
-
|
|
22
|
-
|
|
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 |
|
|
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(
|
|
216
|
-
return true if
|
|
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
|
-
|
|
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].
|
|
264
|
+
group[:keys].reverse! unless group[:kp].ascending
|
|
265
|
+
group[:keys].each { |k| ordered[s2orig[k]] = unordered_hash[s2orig[k]] }
|
|
242
266
|
end
|
|
243
|
-
|
|
244
|
-
|
|
267
|
+
unordered_hash.clear
|
|
268
|
+
unordered_hash.merge!(ordered)
|
|
245
269
|
true
|
|
246
270
|
end
|
|
247
271
|
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright © 2026 Ismo Kärkkäinen
|
|
4
|
+
# Licensed under Universal Permissive License. See LICENSE.txt.
|
|
5
|
+
|
|
6
|
+
require_relative 'common'
|
|
7
|
+
require 'yaml'
|
|
8
|
+
require 'json'
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Gem namespace.
|
|
12
|
+
module OpenAPISourceTools
|
|
13
|
+
# Methods and classes to get schemas in and out of documents.
|
|
14
|
+
module SchemaIO
|
|
15
|
+
OPENAPI_PATHS = <<EOF
|
|
16
|
+
---
|
|
17
|
+
separator: "/" # For splitting paths.
|
|
18
|
+
ignore: "#" # What to ignore in split paths.
|
|
19
|
+
named: "#/components/schemas" # Before named schema.
|
|
20
|
+
reference: "$ref" # Reference key name.
|
|
21
|
+
referenced: "#/components/schemas" # Reference path before schema name.
|
|
22
|
+
EOF
|
|
23
|
+
|
|
24
|
+
JSON_SCHEMA_PATHS = <<EOF
|
|
25
|
+
---
|
|
26
|
+
separator: "/"
|
|
27
|
+
ignore: "#"
|
|
28
|
+
named: ~
|
|
29
|
+
reference: "$ref"
|
|
30
|
+
referenced: "#/$defs"
|
|
31
|
+
EOF
|
|
32
|
+
|
|
33
|
+
JSON_SCHEMA_DEFS_PATHS = <<EOF
|
|
34
|
+
---
|
|
35
|
+
separator: "/"
|
|
36
|
+
ignore: "#"
|
|
37
|
+
named: "#/$defs"
|
|
38
|
+
reference: "$ref"
|
|
39
|
+
referenced: "#/$defs"
|
|
40
|
+
EOF
|
|
41
|
+
|
|
42
|
+
BUILTINS = {
|
|
43
|
+
openapi: OPENAPI_PATHS,
|
|
44
|
+
jsonschema: JSON_SCHEMA_PATHS,
|
|
45
|
+
jsonschemadefs: JSON_SCHEMA_DEFS_PATHS
|
|
46
|
+
}.freeze
|
|
47
|
+
|
|
48
|
+
def self.get(name)
|
|
49
|
+
BUILTINS[name.is_a?(String) ? name.downcase.to_sym : name]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Checks format and does some convenience preparations.
|
|
53
|
+
class SchemaFormat
|
|
54
|
+
attr_reader :separator, :named, :reference, :referenced
|
|
55
|
+
|
|
56
|
+
def initialize(format)
|
|
57
|
+
f = format.is_a?(Hash) ? format : YAML.safe_load(format)
|
|
58
|
+
raise ArgumentError, 'Format is not an object.' unless f.is_a?(Hash)
|
|
59
|
+
errors = []
|
|
60
|
+
@separator = f['separator']
|
|
61
|
+
errors.push('separator must be a non-empty string.') unless @separator.is_a?(String) && !@separator.empty?
|
|
62
|
+
@ignore = f['ignore']
|
|
63
|
+
errors.push('ignore must be a string or an array of strings.') unless @ignore.is_a?(String) || @ignore.is_a?(Array)
|
|
64
|
+
if @ignore.is_a?(Array)
|
|
65
|
+
errors.push('ignore array values must be non-empty strings') unless @ignore.index { |v| !v.is_a?(String) || v.empty? }.nil?
|
|
66
|
+
else
|
|
67
|
+
@ignore = [ @ignore ]
|
|
68
|
+
end
|
|
69
|
+
@ignore.push('')
|
|
70
|
+
@named = f['named']
|
|
71
|
+
@reference = f['reference']
|
|
72
|
+
errors.push('Reference key must be non-empty string.') if @reference.nil?
|
|
73
|
+
@referenced = f['referenced']
|
|
74
|
+
raise ArgumentError, errors.join("\n") unless errors.empty?
|
|
75
|
+
@named = '' if @named.nil?
|
|
76
|
+
@named += @separator unless @named.empty? || @named.end_with?(@separator)
|
|
77
|
+
@referenced += @separator unless @referenced.nil? || @referenced.empty? || @referenced.end_with?(@separator)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def ignore?(str)
|
|
81
|
+
@ignore.include?(str)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def path(str)
|
|
85
|
+
str.split(@separator).reject { |p| ignore?(p) }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def reference?(key, value)
|
|
89
|
+
# Accurate enough for now.
|
|
90
|
+
@reference == key && value.is_a?(String) && value.start_with?(@referenced)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def reference_name(ref)
|
|
94
|
+
ref[@referenced.size..]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def make_reference(name)
|
|
98
|
+
"#{@referenced}#{name}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def root_schema?
|
|
102
|
+
@named.empty?
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def self.load_data(data)
|
|
107
|
+
# File name?
|
|
108
|
+
if data.is_a?(String)
|
|
109
|
+
begin
|
|
110
|
+
return YAML.safe_load_file(data)
|
|
111
|
+
rescue StandardError
|
|
112
|
+
# Was not a file, it is ok.
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
# Internal key?
|
|
116
|
+
d = get(data)
|
|
117
|
+
return YAML.safe_load(d) unless d.nil?
|
|
118
|
+
# Try to parse as JSON.
|
|
119
|
+
if data.is_a?(String)
|
|
120
|
+
begin
|
|
121
|
+
return JSON.parse(data)
|
|
122
|
+
rescue JSON::ParserError
|
|
123
|
+
# Was not.
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
nil
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def self.key_data(data)
|
|
130
|
+
d = load_data(data)
|
|
131
|
+
d.nil? ? nil : SchemaFormat.new(d)
|
|
132
|
+
rescue ArgumentError => e
|
|
133
|
+
Common.aargh(e.to_s)
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Changes references from source format to target format.
|
|
138
|
+
def self.alter_refs(schema, source_access, target_access)
|
|
139
|
+
if schema.is_a?(Array)
|
|
140
|
+
schema.each do |v|
|
|
141
|
+
alter_refs(v, source_access, target_access)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
return unless schema.is_a?(Hash)
|
|
145
|
+
schema.keys.each do |key|
|
|
146
|
+
value = schema[key]
|
|
147
|
+
if source_access.format.reference?(key, value)
|
|
148
|
+
# Change the prefix in the reference, unless equal.
|
|
149
|
+
unless target_access.format.referenced == source_access.format.referenced
|
|
150
|
+
value = target_access.format.make_reference(source_access.format.reference_name(value))
|
|
151
|
+
end
|
|
152
|
+
# Change reference key name if needed.
|
|
153
|
+
if source_access.format.reference == target_access.format.reference
|
|
154
|
+
schema[key] = value
|
|
155
|
+
else
|
|
156
|
+
schema.delete(key)
|
|
157
|
+
schema[target_access.format.reference] = value
|
|
158
|
+
end
|
|
159
|
+
elsif value.is_a?(Hash) || value.is_a?(Array)
|
|
160
|
+
alter_refs(value, source_access, target_access)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Handles document with schemas in stated places. Reads or modifies.
|
|
166
|
+
class SchemaAccess
|
|
167
|
+
attr_reader :document, :format
|
|
168
|
+
|
|
169
|
+
def initialize(format, document)
|
|
170
|
+
@document = document
|
|
171
|
+
@format = format
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Used when reading from given document.
|
|
175
|
+
|
|
176
|
+
def list_schema_names
|
|
177
|
+
schemas = @document.dig(*@format.path(@format.referenced))
|
|
178
|
+
schemas.keys.sort!
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def find_named_schema(name)
|
|
182
|
+
if @format.root_schema?
|
|
183
|
+
# Make a copy and remove the root key to referenced definitions.
|
|
184
|
+
out = {}.merge!(@document)
|
|
185
|
+
first = @format.path(@format.referenced).first
|
|
186
|
+
out.delete(first) unless first.nil?
|
|
187
|
+
out
|
|
188
|
+
else
|
|
189
|
+
find_by_reference(@format.make_reference(name))
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def find_referenced_schemas(name, schema)
|
|
194
|
+
name2schema = {}
|
|
195
|
+
return name2schema if schema.nil?
|
|
196
|
+
existing = Set.new # Stops loops.
|
|
197
|
+
existing.add(name) unless @format.root_schema?
|
|
198
|
+
todo = [ schema ]
|
|
199
|
+
until todo.empty?
|
|
200
|
+
s = todo.pop
|
|
201
|
+
refs = Set.new
|
|
202
|
+
gather_refs(s, refs, existing)
|
|
203
|
+
# Add all new references to the todo list, if present in the file.
|
|
204
|
+
refs.each do |r|
|
|
205
|
+
existing.add(r)
|
|
206
|
+
idx = r.rindex(@format.separator)
|
|
207
|
+
next if idx.nil?
|
|
208
|
+
refd = find_by_reference(r)
|
|
209
|
+
next if refd.nil?
|
|
210
|
+
todo.push(refd)
|
|
211
|
+
name2schema[r[(idx + 1)..]] = todo.last
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
name2schema
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Used when writing to given document.
|
|
218
|
+
|
|
219
|
+
def put_named_schema(name, schema)
|
|
220
|
+
if @format.root_schema?
|
|
221
|
+
# If clear was to be done we'd need to retain "$defs" or such.
|
|
222
|
+
# In intended use, JSON schema document is empty when writing to root.
|
|
223
|
+
@document.merge!(schema)
|
|
224
|
+
else
|
|
225
|
+
path = make_path(@format.named, name)
|
|
226
|
+
OpenAPISourceTools::Common.bury(@document, path, schema)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Call only after put_named_schema.
|
|
231
|
+
def put_referenced_schema(name, schema)
|
|
232
|
+
path = make_path(@format.referenced, name)
|
|
233
|
+
OpenAPISourceTools::Common.bury(@document, path, schema)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
private
|
|
237
|
+
|
|
238
|
+
def make_path(start, name)
|
|
239
|
+
@format.path(start).push(name)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def find_by_reference(ref)
|
|
243
|
+
# Implicitly expects that references always have same start followed by name.
|
|
244
|
+
@document.dig(*@format.path(ref))
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def gather_refs(schema, found, existing)
|
|
248
|
+
if schema.is_a?(Array)
|
|
249
|
+
schema.each do |v|
|
|
250
|
+
gather_refs(v, found, existing)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
return unless schema.is_a?(Hash)
|
|
254
|
+
schema.each do |key, value|
|
|
255
|
+
if @format.reference?(key, value)
|
|
256
|
+
found.add(value) unless existing.member?(value)
|
|
257
|
+
elsif value.is_a?(Hash) || value.is_a?(Array)
|
|
258
|
+
gather_refs(value, found, existing)
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
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.13'
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ismo Kärkkäinen
|
|
@@ -48,11 +48,15 @@ executables:
|
|
|
48
48
|
- openapi-frequencies
|
|
49
49
|
- openapi-generate
|
|
50
50
|
- openapi-generate-checkconfig
|
|
51
|
+
- openapi-logicsplit
|
|
51
52
|
- openapi-merge
|
|
52
53
|
- openapi-modifypaths
|
|
53
54
|
- openapi-order
|
|
54
55
|
- openapi-patterntests
|
|
55
56
|
- openapi-processpaths
|
|
57
|
+
- openapi-schema-in
|
|
58
|
+
- openapi-schema-ls
|
|
59
|
+
- openapi-schema-out
|
|
56
60
|
extensions: []
|
|
57
61
|
extra_rdoc_files: []
|
|
58
62
|
files:
|
|
@@ -69,11 +73,15 @@ files:
|
|
|
69
73
|
- bin/openapi-frequencies
|
|
70
74
|
- bin/openapi-generate
|
|
71
75
|
- bin/openapi-generate-checkconfig
|
|
76
|
+
- bin/openapi-logicsplit
|
|
72
77
|
- bin/openapi-merge
|
|
73
78
|
- bin/openapi-modifypaths
|
|
74
79
|
- bin/openapi-order
|
|
75
80
|
- bin/openapi-patterntests
|
|
76
81
|
- bin/openapi-processpaths
|
|
82
|
+
- bin/openapi-schema-in
|
|
83
|
+
- bin/openapi-schema-ls
|
|
84
|
+
- bin/openapi-schema-out
|
|
77
85
|
- lib/openapi/sourcetools.rb
|
|
78
86
|
- lib/openapi/sourcetools/apiobjects.rb
|
|
79
87
|
- lib/openapi/sourcetools/common.rb
|
|
@@ -85,6 +93,7 @@ files:
|
|
|
85
93
|
- lib/openapi/sourcetools/loaders.rb
|
|
86
94
|
- lib/openapi/sourcetools/order.rb
|
|
87
95
|
- lib/openapi/sourcetools/output.rb
|
|
96
|
+
- lib/openapi/sourcetools/schemaio.rb
|
|
88
97
|
- lib/openapi/sourcetools/task.rb
|
|
89
98
|
- lib/openapi/sourcetools/version.rb
|
|
90
99
|
homepage: https://xn--ismo-krkkinen-gfbd.fi/openapi-sourcetools/index.html
|