openapi-sourcetools 0.7.0 → 0.8.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-addheaders +11 -11
- data/bin/openapi-addparameters +52 -12
- data/bin/openapi-addresponses +11 -12
- data/bin/openapi-addschemas +41 -17
- data/bin/openapi-checkschemas +21 -21
- data/bin/openapi-frequencies +24 -26
- data/bin/openapi-generate +15 -12
- data/bin/openapi-merge +21 -15
- data/bin/openapi-modifypaths +17 -16
- data/bin/openapi-processpaths +16 -27
- data/lib/openapi/sourcetools/apiobjects.rb +191 -0
- data/lib/openapi/sourcetools/common.rb +82 -0
- data/lib/openapi/sourcetools/config.rb +158 -0
- data/lib/openapi/sourcetools/docs.rb +41 -0
- data/lib/{gen.rb → openapi/sourcetools/gen.rb} +38 -13
- data/lib/openapi/sourcetools/generate.rb +96 -0
- data/lib/openapi/sourcetools/helper.rb +93 -0
- data/lib/openapi/sourcetools/loaders.rb +164 -0
- data/lib/openapi/sourcetools/output.rb +83 -0
- data/lib/openapi/sourcetools/securityschemes.rb +268 -0
- data/lib/openapi/sourcetools/task.rb +137 -0
- data/lib/openapi/sourcetools/version.rb +13 -0
- data/lib/openapi/sourcetools.rb +15 -0
- metadata +42 -18
- data/lib/apiobjects.rb +0 -333
- data/lib/common.rb +0 -110
- data/lib/docs.rb +0 -33
- data/lib/generate.rb +0 -90
- data/lib/helper.rb +0 -94
- data/lib/loaders.rb +0 -96
- data/lib/output.rb +0 -58
- data/lib/task.rb +0 -101
data/bin/openapi-merge
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
# Copyright © 2021-
|
4
|
+
# Copyright © 2021-2025 Ismo Kärkkäinen
|
5
5
|
# Licensed under Universal Permissive License. See LICENSE.txt.
|
6
6
|
|
7
|
-
require_relative '../lib/common'
|
7
|
+
require_relative '../lib/openapi/sourcetools/common'
|
8
8
|
require 'optparse'
|
9
|
-
require 'yaml'
|
10
9
|
require 'set'
|
10
|
+
include OpenAPISourceTools
|
11
11
|
|
12
12
|
def raise_se(message)
|
13
13
|
raise StandardError, message
|
@@ -52,7 +52,7 @@ end
|
|
52
52
|
|
53
53
|
def gather_refs(doc, found)
|
54
54
|
doc.each_pair do |key, value|
|
55
|
-
if key == '$ref'
|
55
|
+
if key == '$ref' # Trust all refs to be valid.
|
56
56
|
found.add(value)
|
57
57
|
elsif value.is_a? Hash
|
58
58
|
gather_refs(value, found)
|
@@ -64,6 +64,13 @@ def gather_refs(doc, found)
|
|
64
64
|
end
|
65
65
|
end
|
66
66
|
|
67
|
+
def has_refd_anchor(item, refs)
|
68
|
+
return !((item.index { |v| has_refd_anchor(v, refs) }).nil?) if item.is_a?(Array)
|
69
|
+
return false unless item.is_a?(Hash)
|
70
|
+
return true if refs.member?('#' + item.fetch('$anchor', ''))
|
71
|
+
!((item.values.index { |v| has_refd_anchor(v, refs) }).nil?)
|
72
|
+
end
|
73
|
+
|
67
74
|
def prune(merged)
|
68
75
|
prev_refs = Set.new
|
69
76
|
loop do # May have references from deleted so repeat until nothing deleted.
|
@@ -74,16 +81,16 @@ def prune(merged)
|
|
74
81
|
refs.add("#/components/securitySchemes/#{key}")
|
75
82
|
end
|
76
83
|
end
|
84
|
+
# Add schema ref for all schemas that have referenced anchor somewhere.
|
85
|
+
(merged.dig(*%w[components schemas]) || {}).each do |name, schema|
|
86
|
+
refs.add("#/components/Schemas/#{name}") if has_refd_anchor(schema, refs)
|
87
|
+
end
|
77
88
|
used = {}
|
78
89
|
all = merged.fetch('components', {})
|
79
90
|
refs.each do |ref|
|
80
91
|
p = ref.split('/')
|
81
92
|
p.shift(2)
|
82
|
-
item = all
|
83
|
-
p.each do |key|
|
84
|
-
item = item.fetch(key, nil)
|
85
|
-
break if item.nil?
|
86
|
-
end
|
93
|
+
item = all.dig(*p)
|
87
94
|
next if item.nil?
|
88
95
|
sub = used
|
89
96
|
p.each_index do |k|
|
@@ -105,7 +112,6 @@ def main
|
|
105
112
|
output_file = nil
|
106
113
|
keep = false
|
107
114
|
|
108
|
-
ENV['POSIXLY_CORRECT'] = '1'
|
109
115
|
parser = OptionParser.new do |opts|
|
110
116
|
opts.summary_indent = ' '
|
111
117
|
opts.summary_width = 26
|
@@ -130,7 +136,7 @@ allowed only to append to the merged document, not re-define anything in it.
|
|
130
136
|
exit 0
|
131
137
|
end
|
132
138
|
end
|
133
|
-
parser.
|
139
|
+
parser.order!
|
134
140
|
|
135
141
|
max_depths = Hash.new(0)
|
136
142
|
max_depths['openapi'] = 1
|
@@ -143,16 +149,16 @@ allowed only to append to the merged document, not re-define anything in it.
|
|
143
149
|
max_depths['tags'] = 1
|
144
150
|
merged = {}
|
145
151
|
ARGV.each do |filename|
|
146
|
-
s = load_source(filename)
|
152
|
+
s = Common.load_source(filename)
|
147
153
|
return 2 if s.nil?
|
148
154
|
add_undefined(merged, s, filename, [], max_depths)
|
149
155
|
rescue StandardError => e
|
150
|
-
return aargh(e.to_s, 4)
|
156
|
+
return Common.aargh(e.to_s, 4)
|
151
157
|
end
|
152
158
|
|
153
159
|
prune(merged) unless keep
|
154
160
|
|
155
|
-
dump_result(output_file,
|
161
|
+
Common.dump_result(output_file, merged, 3)
|
156
162
|
end
|
157
163
|
|
158
|
-
exit(main) if
|
164
|
+
exit(main) if defined?($unit_test).nil?
|
data/bin/openapi-modifypaths
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
# Copyright © 2024 Ismo Kärkkäinen
|
4
|
+
# Copyright © 2024-2025 Ismo Kärkkäinen
|
5
5
|
# Licensed under Universal Permissive License. See LICENSE.txt.
|
6
6
|
|
7
|
-
require_relative '../lib/common'
|
7
|
+
require_relative '../lib/openapi/sourcetools/common'
|
8
8
|
require 'optparse'
|
9
|
-
|
9
|
+
include OpenAPISourceTools
|
10
|
+
|
10
11
|
|
11
12
|
def path2pieces(s)
|
12
|
-
s.split('/').reject
|
13
|
+
s.split('/').reject &:empty?
|
13
14
|
end
|
14
15
|
|
15
16
|
def pieces2path(p)
|
@@ -39,18 +40,18 @@ end
|
|
39
40
|
|
40
41
|
def add_op(s)
|
41
42
|
s = path2pieces(s)
|
42
|
-
|
43
|
+
proc { |path| add(s, path) }
|
43
44
|
end
|
44
45
|
|
45
46
|
def remove_op(s)
|
46
47
|
s = path2pieces(s)
|
47
|
-
|
48
|
+
proc { |path| remove(s, path) }
|
48
49
|
end
|
49
50
|
|
50
51
|
def replace_op(orig, s)
|
51
52
|
o = path2pieces(orig)
|
52
53
|
s = path2pieces(s)
|
53
|
-
|
54
|
+
proc { |path| replace(o, s, path) }
|
54
55
|
end
|
55
56
|
|
56
57
|
def perform_operations(paths, operations)
|
@@ -77,23 +78,23 @@ def main
|
|
77
78
|
opts.separator ''
|
78
79
|
opts.separator 'Options:'
|
79
80
|
opts.on('-i', '--input FILE', 'Read API spec from FILE, not stdin.') do |f|
|
80
|
-
exit(aargh(
|
81
|
+
exit(Common.aargh('Expected string to replace PREFIX.', 1)) unless orig.nil?
|
81
82
|
input_name = f
|
82
83
|
end
|
83
84
|
opts.on('-o', '--output FILE', 'Output to FILE, not stdout.') do |f|
|
84
|
-
exit(aargh(
|
85
|
+
exit(Common.aargh('Expected string to replace PREFIX.', 1)) unless orig.nil?
|
85
86
|
output_name = f
|
86
87
|
end
|
87
88
|
opts.on('-a', '--add STR', 'Add prefix STR to all paths.') do |s|
|
88
|
-
exit(aargh(
|
89
|
+
exit(Common.aargh('Expected string to replace PREFIX.', 1)) unless orig.nil?
|
89
90
|
operations.push(add_op(s))
|
90
91
|
end
|
91
92
|
opts.on('-d', '--delete PREFIX', 'Delete PREFIX when present.') do |s|
|
92
|
-
exit(aargh(
|
93
|
+
exit(Common.aargh('Expected string to replace PREFIX.', 1)) unless orig.nil?
|
93
94
|
operations.push(remove_op(s))
|
94
95
|
end
|
95
96
|
opts.on('-r', '--replace PREFIX STR', 'Replace PREFIX with STR when present.') do |s|
|
96
|
-
exit(aargh('Empty string to replace.', 1)) if s.empty?
|
97
|
+
exit(Common.aargh('Empty string to replace.', 1)) if s.empty?
|
97
98
|
orig = s
|
98
99
|
end
|
99
100
|
opts.on('-h', '--help', 'Print this help and exit.') do
|
@@ -106,18 +107,18 @@ STR and PREFIX are expected to be parts of a path surrounded by /.
|
|
106
107
|
end
|
107
108
|
end
|
108
109
|
parser.order! do |s|
|
109
|
-
exit(aargh("String without option: #{s}", 1)) if orig.nil?
|
110
|
+
exit(Common.aargh("String without option: #{s}", 1)) if orig.nil?
|
110
111
|
operations.push(replace_op(orig, s))
|
111
112
|
orig = nil
|
112
113
|
end
|
113
114
|
|
114
|
-
doc = load_source(input_name)
|
115
|
+
doc = Common.load_source(input_name)
|
115
116
|
return 2 if doc.nil?
|
116
117
|
|
117
118
|
p = perform_operations(doc.fetch('paths', {}), operations)
|
118
119
|
doc['paths'] = p unless p.empty?
|
119
120
|
|
120
|
-
dump_result(output_name, doc, 3)
|
121
|
+
Common.dump_result(output_name, doc, 3)
|
121
122
|
end
|
122
123
|
|
123
|
-
main if defined?($unit_test).nil?
|
124
|
+
exit(main) if defined?($unit_test).nil?
|
data/bin/openapi-processpaths
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
# Copyright © 2021-
|
4
|
+
# Copyright © 2021-2025 Ismo Kärkkäinen
|
5
5
|
# Licensed under Universal Permissive License. See LICENSE.txt.
|
6
6
|
|
7
|
-
require_relative '../lib/
|
7
|
+
require_relative '../lib/openapi/sourcetools/apiobjects'
|
8
|
+
require_relative '../lib/openapi/sourcetools/common'
|
8
9
|
require 'optparse'
|
9
|
-
|
10
|
+
include OpenAPISourceTools
|
10
11
|
|
11
12
|
|
12
13
|
def main
|
@@ -32,26 +33,22 @@ def main
|
|
32
33
|
opts.on('-h', '--help', 'Print this help and exit.') do
|
33
34
|
$stdout.puts %(#{opts}
|
34
35
|
|
35
|
-
|
36
|
-
|
36
|
+
Adds split path parts into API document path items under x-openapi-sourcetools
|
37
|
+
key. Checks for paths that may be ambiguous.
|
37
38
|
)
|
38
39
|
exit 0
|
39
40
|
end
|
40
41
|
end
|
41
|
-
parser.
|
42
|
+
parser.order!
|
42
43
|
|
43
|
-
doc = load_source(input_name)
|
44
|
+
doc = Common.load_source(input_name)
|
44
45
|
return 2 if doc.nil?
|
45
46
|
|
46
47
|
processed = {}
|
47
|
-
doc.fetch('paths', {}).
|
48
|
-
parts = split_path(path, true)
|
49
|
-
|
50
|
-
|
51
|
-
'orig' => value,
|
52
|
-
'lookalike' => [],
|
53
|
-
path: ServerPath.new(parts)
|
54
|
-
}
|
48
|
+
doc.fetch('paths', {}).each do |path, item|
|
49
|
+
parts = Common.split_path(path, true)
|
50
|
+
item['x-openapi-sourcetools-parts'] = parts # Added to original path item.
|
51
|
+
processed[path] = ApiObjects::ServerPath.new(parts)
|
55
52
|
end
|
56
53
|
|
57
54
|
# Find lookalike sets.
|
@@ -63,22 +60,14 @@ later stage tools. Checks for paths that may be ambiguous.
|
|
63
60
|
k.times do |n|
|
64
61
|
pn = paths[n]
|
65
62
|
b = processed[pn]
|
66
|
-
next unless a
|
67
|
-
a['lookalike'].push pn
|
68
|
-
b['lookalike'].push pk
|
63
|
+
next unless a.compare(b).zero?
|
69
64
|
$stderr.puts("Similar: #{pn} #{pk}")
|
70
65
|
lookalikes = true
|
71
66
|
end
|
72
67
|
end
|
73
|
-
return aargh('Similar paths found.', 4) if lookalikes && error
|
68
|
+
return Common.aargh('Similar paths found.', 4) if lookalikes && error
|
74
69
|
|
75
|
-
|
76
|
-
processed.each_value do |v|
|
77
|
-
v.keys.each { |k| v.delete(k) if k.is_a? Symbol }
|
78
|
-
end
|
79
|
-
doc['paths'] = processed
|
80
|
-
|
81
|
-
dump_result(output_name, YAML.dump(doc), 3)
|
70
|
+
Common.dump_result(output_name, doc, 3)
|
82
71
|
end
|
83
72
|
|
84
|
-
exit(main)
|
73
|
+
exit(main) unless defined?($unit_test)
|
@@ -0,0 +1,191 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright © 2024-2025 Ismo Kärkkäinen
|
4
|
+
# Licensed under Universal Permissive License. See LICENSE.txt.
|
5
|
+
|
6
|
+
module OpenAPISourceTools
|
7
|
+
# Various classes for handling objects in the API specification.
|
8
|
+
# Used in various programs.
|
9
|
+
module ApiObjects
|
10
|
+
|
11
|
+
def self.same(a, b, ignored_keys = Set.new(%w[summary description]))
|
12
|
+
return a == b unless a.is_a?(Hash) && b.is_a?(Hash)
|
13
|
+
keys = Set.new(a.keys + b.keys) - ignored_keys
|
14
|
+
keys.to_a.each do |k|
|
15
|
+
return false unless a.key?(k) && b.key?(k)
|
16
|
+
return false unless same(a[k], b[k], ignored_keys)
|
17
|
+
end
|
18
|
+
true
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.ref_string(name, schema_path)
|
22
|
+
"#{schema_path}/#{name}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.reference(obj, schemas, schema_path, ignored_keys = Set.new(%w[summary description]), prefix = 'Schema')
|
26
|
+
# Check if identical schema has been added and if so, return the $ref string.
|
27
|
+
schemas.keys.sort.each do |k|
|
28
|
+
return ref_string(k, schema_path) if same(obj, schemas[k], ignored_keys)
|
29
|
+
end
|
30
|
+
# One of the numbers will not match existing keys. More number than keys.
|
31
|
+
(schemas.size + 1).times do |n|
|
32
|
+
# 'x' is to simplify find and replace (Schema1x vs Schema1 and Schema10)
|
33
|
+
k = "#{prefix}#{n}x"
|
34
|
+
next if schemas.key?(k)
|
35
|
+
schemas[k] = obj.merge
|
36
|
+
return ref_string(k, schema_path)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# A component in the API specification for reference and anchor handling.
|
41
|
+
class Components
|
42
|
+
attr_reader :path, :prefix, :anchor2ref, :schema_names
|
43
|
+
attr_accessor :items, :ignored_keys
|
44
|
+
|
45
|
+
def initialize(path, prefix, ignored_keys = %w[summary description examples example $anchor])
|
46
|
+
path = "#/#{path.join('/')}/" if path.is_a?(Array)
|
47
|
+
path = "#{path}/" unless path.end_with?('/')
|
48
|
+
@path = path
|
49
|
+
@prefix = prefix
|
50
|
+
@anchor2ref = {}
|
51
|
+
@schema_names = Set.new
|
52
|
+
@items = {}
|
53
|
+
@ignored_keys = Set.new(ignored_keys)
|
54
|
+
end
|
55
|
+
|
56
|
+
def add_options(opts)
|
57
|
+
opts.on('--use FIELD', 'Use FIELD in comparisons.') do |f|
|
58
|
+
@ignored_keys.delete(f)
|
59
|
+
end
|
60
|
+
opts.on('--ignore FIELD', 'Ignore FIELD in comparisons.') do |f|
|
61
|
+
@ignored_keys.add(f)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def help
|
66
|
+
%(All fields are used in object equality comparisons except:\n#{@ignored_keys.to_a.sort!.join("\n")})
|
67
|
+
end
|
68
|
+
|
69
|
+
def add_schema_name(name)
|
70
|
+
@schema_names.add(name)
|
71
|
+
end
|
72
|
+
|
73
|
+
def ref_string(name)
|
74
|
+
return nil if name.nil?
|
75
|
+
"#{@path}#{name}"
|
76
|
+
end
|
77
|
+
|
78
|
+
def reference(obj)
|
79
|
+
# Check if identical schema has been added. If so, return the $ref string.
|
80
|
+
@items.each do |k, v|
|
81
|
+
return ref_string(k) if ApiObjects.same(obj, v, @ignored_keys)
|
82
|
+
end
|
83
|
+
# One of the numbers will not match existing keys. More number than keys.
|
84
|
+
(@items.size + 1).times do |n|
|
85
|
+
# 'x' is to simplify find and replace (Schema1x vs Schema1 and Schema10)
|
86
|
+
cand = "#{@prefix}#{n}x"
|
87
|
+
next if @items.key?(cand)
|
88
|
+
@items[cand] = obj.merge
|
89
|
+
@schema_names.add(cand)
|
90
|
+
return ref_string(cand)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def store_anchor(obj, ref = nil)
|
95
|
+
anchor_name = obj['$anchor']
|
96
|
+
return if anchor_name.nil?
|
97
|
+
ref = obj['$ref'] if ref.nil?
|
98
|
+
raise StandardError, 'ref is nil and no $ref or it is nil' if ref.nil?
|
99
|
+
@anchor2ref[anchor_name] = ref
|
100
|
+
end
|
101
|
+
|
102
|
+
def alter_anchors
|
103
|
+
replacements = {}
|
104
|
+
@anchor2ref.each_key do |a|
|
105
|
+
next if @schema_names.member?(a)
|
106
|
+
replacements[a] = ref_string(a)
|
107
|
+
@schema_names.add(a)
|
108
|
+
end
|
109
|
+
replacements.each do |a, r|
|
110
|
+
@anchor2ref[a] = r
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def anchor_ref_replacement(ref)
|
115
|
+
@anchor2ref[ref[1...ref.size]] || ref
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Represents path with fixed parts and variables.
|
120
|
+
class ServerPath
|
121
|
+
# Probably moves to a separate file once processpaths and frequencies receive
|
122
|
+
# some attention.
|
123
|
+
include Comparable
|
124
|
+
|
125
|
+
attr_accessor :parts
|
126
|
+
|
127
|
+
def initialize(parts)
|
128
|
+
@parts = parts
|
129
|
+
end
|
130
|
+
|
131
|
+
# Parameters are after fixed strings.
|
132
|
+
def <=>(other)
|
133
|
+
pp = other.is_a?(Array) ? other : other.parts
|
134
|
+
@parts.size.times do |k|
|
135
|
+
return 1 if pp.size <= k # Longer comes after shorter.
|
136
|
+
pk = @parts[k]
|
137
|
+
ppk = pp[k]
|
138
|
+
if pk.key?('fixed')
|
139
|
+
if ppk.key?('fixed')
|
140
|
+
c = pk['fixed'] <=> ppk['fixed']
|
141
|
+
else
|
142
|
+
return -1
|
143
|
+
end
|
144
|
+
else
|
145
|
+
if ppk.key?('fixed')
|
146
|
+
return 1
|
147
|
+
else
|
148
|
+
c = pk.fetch('parameter', '') <=> ppk.fetch('parameter', '')
|
149
|
+
end
|
150
|
+
end
|
151
|
+
return c unless c.zero?
|
152
|
+
end
|
153
|
+
(@parts.size < pp.size) ? -1 : 0
|
154
|
+
end
|
155
|
+
|
156
|
+
# Not fit for sorting. Variable equals anything.
|
157
|
+
def compare(other, range = nil)
|
158
|
+
pp = other.is_a?(Array) ? other : other.parts
|
159
|
+
if range.nil?
|
160
|
+
range = 0...@parts.size
|
161
|
+
elsif range.is_a? Number
|
162
|
+
range = range...(range + 1)
|
163
|
+
end
|
164
|
+
range.each do |k|
|
165
|
+
return 1 if pp.size <= k # Longer comes after shorter.
|
166
|
+
ppk = pp[k]
|
167
|
+
next unless ppk.key?('fixed')
|
168
|
+
pk = parts[k]
|
169
|
+
next unless pk.key?('fixed')
|
170
|
+
c = pk['fixed'] <=> ppk['fixed']
|
171
|
+
return c unless c.zero?
|
172
|
+
end
|
173
|
+
(@parts.size < pp.size) ? -1 : 0
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def self.operation_objects(path_item)
|
178
|
+
keys = %w[operationId requestBody responses callbacks]
|
179
|
+
out = {}
|
180
|
+
path_item.each do |method, op|
|
181
|
+
next unless op.is_a?(Hash)
|
182
|
+
keys.each do |key|
|
183
|
+
next unless op.key?(key)
|
184
|
+
out[method] = op
|
185
|
+
break
|
186
|
+
end
|
187
|
+
end
|
188
|
+
out
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright © 2021-2025 Ismo Kärkkäinen
|
4
|
+
# Licensed under Universal Permissive License. See LICENSE.txt.
|
5
|
+
|
6
|
+
require 'pathname'
|
7
|
+
require 'yaml'
|
8
|
+
|
9
|
+
module OpenAPISourceTools
|
10
|
+
# Common methods used in programs and elsewhere gathered into one place.
|
11
|
+
module Common
|
12
|
+
def self.aargh(message, return_value = nil)
|
13
|
+
message = message.map(&:to_s).join("\n") if message.is_a? Array
|
14
|
+
$stderr.puts message
|
15
|
+
return_value
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.yesno(boolean)
|
19
|
+
boolean ? 'yes' : 'no'
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.bury(doc, path, value)
|
23
|
+
(path.size - 1).times do |k|
|
24
|
+
p = path[k]
|
25
|
+
doc[p] = {} unless doc.key?(p)
|
26
|
+
doc = doc[p]
|
27
|
+
end
|
28
|
+
doc[path.last] = value
|
29
|
+
end
|
30
|
+
|
31
|
+
module Out
|
32
|
+
attr_reader :count
|
33
|
+
module_function :count
|
34
|
+
attr_accessor :quiet
|
35
|
+
module_function :quiet
|
36
|
+
module_function :quiet=
|
37
|
+
|
38
|
+
def self.put(message)
|
39
|
+
Common.aargh(message) unless @quiet
|
40
|
+
@count = @count.nil? ? 1 : @count + 1
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.split_path(p, spec = false)
|
45
|
+
parts = []
|
46
|
+
p = p.strip
|
47
|
+
unless spec
|
48
|
+
q = p.index('?')
|
49
|
+
p.slice!(0...q) unless q.nil?
|
50
|
+
end
|
51
|
+
p.split('/').each do |s|
|
52
|
+
next if s.empty?
|
53
|
+
s = { (spec && s.include?('{') ? 'parameter' : 'fixed') => s }
|
54
|
+
parts.push(s)
|
55
|
+
end
|
56
|
+
parts
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.load_source(input)
|
60
|
+
YAML.safe_load(input.nil? ? $stdin : File.read(input))
|
61
|
+
rescue Errno::ENOENT
|
62
|
+
aargh "Could not load #{input || 'stdin'}"
|
63
|
+
rescue StandardError => e
|
64
|
+
aargh "#{e}\nFailed to read #{input || 'stdin'}"
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.dump_result(output, doc, error_return)
|
68
|
+
doc = YAML.dump(doc, line_width: 1_000_000) unless doc.is_a?(String)
|
69
|
+
if output.nil?
|
70
|
+
$stdout.puts doc
|
71
|
+
else
|
72
|
+
fp = Pathname.new output
|
73
|
+
fp.open('w') do |f|
|
74
|
+
f.puts doc
|
75
|
+
end
|
76
|
+
end
|
77
|
+
0
|
78
|
+
rescue StandardError => e
|
79
|
+
aargh([ e, "Failed to write output: #{output || 'stdout'}" ], error_return)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright © 2024-2025 Ismo Kärkkäinen
|
4
|
+
# Licensed under Universal Permissive License. See LICENSE.txt.
|
5
|
+
|
6
|
+
require_relative 'task'
|
7
|
+
require_relative 'gen'
|
8
|
+
require 'find'
|
9
|
+
require 'yaml'
|
10
|
+
|
11
|
+
module OpenAPISourceTools
|
12
|
+
# Configuration file find and load convenience functions.
|
13
|
+
# See the first 3 methods. The rest are intended to be internal helpers.
|
14
|
+
module ConfigLoader
|
15
|
+
|
16
|
+
# A function to find all files with a given prefix.
|
17
|
+
# Prefix is taken from Gen.config if nil.
|
18
|
+
# Returns an array of ConfigFileInfo objects.
|
19
|
+
def self.find_files(name_prefix:, extensions: [ '.*' ])
|
20
|
+
name_prefix = Gen.config if name_prefix.nil?
|
21
|
+
raise ArgumentError, 'name_prefix or config must be set' if name_prefix.nil?
|
22
|
+
root, name_prefix = prepare_prefix(name_prefix, Gen.wd)
|
23
|
+
file_paths = find_filenames(root, name_prefix)
|
24
|
+
splitter = path_splitter(Gen.separator)
|
25
|
+
out = file_paths.map { |fp| convert_path_end(fp, splitter, root.size + 1, extensions) }
|
26
|
+
out.sort!
|
27
|
+
end
|
28
|
+
|
29
|
+
# A function to read all YAML files in an array of ConfigFileInfo objects.
|
30
|
+
# Returns the same as contents_array.
|
31
|
+
def self.read_contents(config_file_infos)
|
32
|
+
config_file_infos.each do |cfi|
|
33
|
+
c = YAML.safe_load_file(cfi.path)
|
34
|
+
# Check allows e.g. copyright and license files be named with config prefix
|
35
|
+
# for clarity, but ignored during config loading.
|
36
|
+
next if cfi.keys.empty? && !c.is_a?(Hash)
|
37
|
+
cfi.bury_content(c)
|
38
|
+
rescue Psych::SyntaxError
|
39
|
+
next # Was not YAML. Other files can be named using config prefix.
|
40
|
+
end
|
41
|
+
contents_array(config_file_infos)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Maps an array of ConfigFileInfo objects to an array of their contents.
|
45
|
+
def self.contents_array(config_file_infos)
|
46
|
+
config_file_infos.map(&:content).reject(&:nil?)
|
47
|
+
end
|
48
|
+
|
49
|
+
class ConfigFileInfo
|
50
|
+
include Comparable
|
51
|
+
|
52
|
+
attr_reader :root, :keys, :path, :content
|
53
|
+
|
54
|
+
def initialize(pieces, path)
|
55
|
+
@keys = []
|
56
|
+
@root = nil
|
57
|
+
pieces.each do |p|
|
58
|
+
if p.is_a?(String)
|
59
|
+
if @root.nil?
|
60
|
+
@root = p
|
61
|
+
else
|
62
|
+
@keys.push(p)
|
63
|
+
end
|
64
|
+
else
|
65
|
+
break if p == :extension
|
66
|
+
end
|
67
|
+
end
|
68
|
+
@path = path
|
69
|
+
@content = nil
|
70
|
+
end
|
71
|
+
|
72
|
+
def bury_content(content)
|
73
|
+
# Turns chain of keys into nested Hashes.
|
74
|
+
@keys.reverse.each do |key|
|
75
|
+
c = { key => content }
|
76
|
+
content = c
|
77
|
+
end
|
78
|
+
@content = content
|
79
|
+
end
|
80
|
+
|
81
|
+
def <=>(other)
|
82
|
+
d = @root <=> other.root
|
83
|
+
return d unless d.zero?
|
84
|
+
d = @keys.size <=> other.keys.size
|
85
|
+
return d unless d.zero?
|
86
|
+
d = @keys <=> other.keys
|
87
|
+
return d unless d.zero?
|
88
|
+
@path <=> other.path
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.prepare_prefix(name_prefix, root)
|
93
|
+
name_prefix_dir = File.dirname(name_prefix)
|
94
|
+
root = File.realpath(name_prefix_dir, root) unless name_prefix_dir == '.'
|
95
|
+
name_prefix = File.basename(name_prefix)
|
96
|
+
if name_prefix == '.' # Just being nice. Daft argument.
|
97
|
+
name_prefix = File.basename(root)
|
98
|
+
root = File.dirname(root)
|
99
|
+
end
|
100
|
+
[root, name_prefix]
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.find_filenames(root, name_prefix)
|
104
|
+
full_prefix = File.join(root, name_prefix)
|
105
|
+
file_paths = []
|
106
|
+
Find.find(root) do |path|
|
107
|
+
next if path.size < full_prefix.size
|
108
|
+
is_dir = File.directory?(path)
|
109
|
+
if path.start_with?(full_prefix)
|
110
|
+
file_paths.push(path) unless is_dir
|
111
|
+
elsif is_dir
|
112
|
+
Find.prune
|
113
|
+
end
|
114
|
+
end
|
115
|
+
file_paths
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.path_splitter(separator)
|
119
|
+
parts = [ Regexp.quote('/') ]
|
120
|
+
parts.push(Regexp.quote(separator)) if separator.is_a?(String) && !separator.empty?
|
121
|
+
Regexp.new("(#{parts.join('|')})")
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.remove_extension(file_path, extensions)
|
125
|
+
extensions.each do |e|
|
126
|
+
if e == '.*'
|
127
|
+
idx = file_path.rindex('.')
|
128
|
+
next if idx.nil? # No . anywhere.
|
129
|
+
ext = file_path[idx..]
|
130
|
+
next unless ext.index('/').nil? # Last . is before file name.
|
131
|
+
return [ file_path[0...idx], ext ]
|
132
|
+
elsif file_path.end_with?(e)
|
133
|
+
return [ file_path[0..-(1 + e.size)], e ]
|
134
|
+
end
|
135
|
+
end
|
136
|
+
[ file_path, nil ]
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.convert_path_end(path, splitter, prefix_size, extensions)
|
140
|
+
relevant, ext = remove_extension(path[prefix_size..], extensions)
|
141
|
+
pieces = relevant.split(splitter).map do |piece|
|
142
|
+
case piece
|
143
|
+
when '' then nil
|
144
|
+
when '/' then :dir
|
145
|
+
when Gen.separator then :separator
|
146
|
+
else
|
147
|
+
piece
|
148
|
+
end
|
149
|
+
end
|
150
|
+
unless ext.nil?
|
151
|
+
pieces.push(:extension)
|
152
|
+
pieces.push(ext)
|
153
|
+
end
|
154
|
+
pieces.compact!
|
155
|
+
ConfigFileInfo.new(pieces, path)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|