openapi-sourcetools 0.4.3 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dab74fc29224544168525f22fafd82566f0ccce13c331ff67d5c30e7b1cd6f6b
4
- data.tar.gz: c43c779d03c612704ffa06558d7c1ccf225e64417a189dc0d889155760453fe2
3
+ metadata.gz: 9320a1d6d4202c453fb7065f00bc534206b1c780560ae443cd30293f27827595
4
+ data.tar.gz: af45eabe9a8f392c705398957cf276acb39923655207ffda12ee8d93e1c072a0
5
5
  SHA512:
6
- metadata.gz: af3fa46f714c20698c08b0d94a1edbafab6d256940a52dbda84eb947aac175816518c9b5f4818dae8e2e750d3e5fb78ee603e3bae0b5b336717316f1cf3cc494
7
- data.tar.gz: 7f654330b1f7c4fe90f028c69bb113b808682bdbabff8858e48eebb0ed2e8e2ea2af470cf7b6fe175efcb96721792bc380321dc25a48e521b7243207a8bda5b3
6
+ metadata.gz: 1f81cae3adb06d42d3bf5361615a0a21ca42346d08b1f8b83462116668d2ad61ec05ac707e723d910bbe55eb29699129ba13570b44504581e92597242ab3d810
7
+ data.tar.gz: 656959f866321cd68814fed2ef1c9121fe1db36674cb7fbd49f9f2a652e5f43a29d6b1d491a2197278577084d1328e01f99fd1eff8fca1c7ef7d6299ed512b17
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2019 Ismo Kärkkäinen
1
+ Copyright (c) 2021-2024 Ismo Kärkkäinen
2
2
 
3
3
  The Universal Permissive License (UPL), Version 1.0
4
4
 
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Copyright © 2024 Ismo Kärkkäinen
5
+ # Licensed under Universal Permissive License. See LICENSE.txt.
6
+
7
+ require_relative '../lib/apiobjects'
8
+ require_relative '../lib/common'
9
+ require 'optparse'
10
+ require 'yaml'
11
+
12
+
13
+ def replace_headers(obj, components)
14
+ return unless obj.is_a?(Hash)
15
+ obj.each do |k, v|
16
+ if k == 'headers'
17
+ v.keys.sort!.each do |name|
18
+ r = v[name]
19
+ next unless r.is_a?(Hash) # Could complain.
20
+ next if r.key?('$ref')
21
+ v[name] = { '$ref' => components.reference(r) }
22
+ end
23
+ else
24
+ replace_headers(v, components)
25
+ end
26
+ end
27
+ end
28
+
29
+ def main
30
+ input_name = nil
31
+ output_name = nil
32
+ path = %w[components headers]
33
+ components = Components.new(path, 'Header')
34
+
35
+ ENV['POSIXLY_CORRECT'] = '1'
36
+ parser = OptionParser.new do |opts|
37
+ opts.summary_indent = ' '
38
+ opts.summary_width = 26
39
+ opts.banner = 'Usage: openapi-addheaders [options]'
40
+ opts.separator ''
41
+ opts.separator 'Options:'
42
+ opts.on('-i', '--input FILE', 'Read API spec from FILE, not stdin.') do |f|
43
+ input_name = f
44
+ end
45
+ opts.on('-o', '--output FILE', 'Output to FILE, not stdout .') do |f|
46
+ output_name = f
47
+ end
48
+ components.add_options(opts)
49
+ opts.on('-h', '--help', 'Print this help and exit.') do
50
+ $stdout.puts %(#{opts}
51
+
52
+ Loads API document in OpenAPI format and moves headers under components and
53
+ replaces the original with reference.
54
+
55
+ #{components.help}
56
+ )
57
+ exit 0
58
+ end
59
+ end
60
+ parser.parse!
61
+
62
+ doc = load_source(input_name)
63
+ return 2 if doc.nil?
64
+
65
+ components.items = doc.dig(*path) || {}
66
+ replace_headers(doc.dig('components', 'responses') || {}, components)
67
+ replace_headers(doc.fetch('paths', {}), components)
68
+ bury(doc, path, components.items) unless components.items.empty?
69
+
70
+ dump_result(output_name, YAML.dump(doc, line_width: 1_000_000), 3)
71
+ end
72
+
73
+ exit(main) if (defined? $unit_test).nil?
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Copyright © 2024 Ismo Kärkkäinen
5
+ # Licensed under Universal Permissive License. See LICENSE.txt.
6
+
7
+ require_relative '../lib/apiobjects'
8
+ require_relative '../lib/common'
9
+ require 'optparse'
10
+ require 'yaml'
11
+
12
+ def replace_parameter(p, components)
13
+ return p unless p.is_a?(Hash) # Could complain.
14
+ return p if p.key?('$ref')
15
+ { '$ref' => components.reference(p) }
16
+ end
17
+
18
+ def replace_parameters(obj, components)
19
+ return unless obj.is_a?(Hash)
20
+ obj.each do |k, v|
21
+ if k == 'parameters'
22
+ obj[k] = v.map { |p| replace_parameter(p, components) }
23
+ else
24
+ replace_parameters(v, components)
25
+ end
26
+ end
27
+ end
28
+
29
+ def main
30
+ input_name = nil
31
+ output_name = nil
32
+ path = %w[components parameters]
33
+ components = Components.new(path, 'Parameter')
34
+
35
+ ENV['POSIXLY_CORRECT'] = '1'
36
+ parser = OptionParser.new do |opts|
37
+ opts.summary_indent = ' '
38
+ opts.summary_width = 26
39
+ opts.banner = 'Usage: openapi-addparameters [options]'
40
+ opts.separator ''
41
+ opts.separator 'Options:'
42
+ opts.on('-i', '--input FILE', 'Read API spec from FILE, not stdin.') do |f|
43
+ input_name = f
44
+ end
45
+ opts.on('-o', '--output FILE', 'Output to FILE, not stdout .') do |f|
46
+ output_name = f
47
+ end
48
+ components.add_options(opts)
49
+ opts.on('-h', '--help', 'Print this help and exit.') do
50
+ $stdout.puts %(#{opts}
51
+
52
+ Loads API document in OpenAPI format and moves parameters under components and
53
+ replaces the original with reference.
54
+
55
+ #{components.help}
56
+ )
57
+ exit 0
58
+ end
59
+ end
60
+ parser.parse!
61
+
62
+ doc = load_source(input_name)
63
+ return 2 if doc.nil?
64
+
65
+ components.items = doc.dig(*path) || {}
66
+ replace_parameters(doc.fetch('paths', {}), components)
67
+ bury(doc, path, components.items) unless components.items.empty?
68
+
69
+ dump_result(output_name, YAML.dump(doc, line_width: 1_000_000), 3)
70
+ end
71
+
72
+ exit(main) if defined?($unit_test).nil?
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Copyright © 2024 Ismo Kärkkäinen
5
+ # Licensed under Universal Permissive License. See LICENSE.txt.
6
+
7
+ require_relative '../lib/apiobjects'
8
+ require_relative '../lib/common'
9
+ require 'optparse'
10
+ require 'yaml'
11
+
12
+
13
+ def replace_responses(obj, components)
14
+ return unless obj.is_a?(Hash)
15
+ obj.each do |k, v|
16
+ if k == 'responses'
17
+ v.keys.sort!.each do |code|
18
+ r = v[code]
19
+ next unless r.is_a?(Hash) # Could complain.
20
+ next if r.key?('$ref')
21
+ v[code] = { '$ref' => components.reference(r) }
22
+ end
23
+ else
24
+ replace_responses(v, components)
25
+ end
26
+ end
27
+ end
28
+
29
+ def main
30
+ input_name = nil
31
+ output_name = nil
32
+ path = %w[components responses]
33
+ components = Components.new(path, 'Response')
34
+
35
+ ENV['POSIXLY_CORRECT'] = '1'
36
+ parser = OptionParser.new do |opts|
37
+ opts.summary_indent = ' '
38
+ opts.summary_width = 26
39
+ opts.banner = 'Usage: openapi-addresponses [options]'
40
+ opts.separator ''
41
+ opts.separator 'Options:'
42
+ opts.on('-i', '--input FILE', 'Read API spec from FILE, not stdin.') do |f|
43
+ input_name = f
44
+ end
45
+ opts.on('-o', '--output FILE', 'Output to FILE, not stdout .') do |f|
46
+ output_name = f
47
+ end
48
+ components.add_options(opts)
49
+ opts.on('-h', '--help', 'Print this help and exit.') do
50
+ $stdout.puts %(#{opts}
51
+
52
+ Loads API document in OpenAPI format and moves responses under components and
53
+ replaces the original with reference.
54
+
55
+ #{components.help}
56
+ )
57
+ exit 0
58
+ end
59
+ end
60
+ parser.parse!
61
+
62
+ doc = load_source(input_name)
63
+ return 2 if doc.nil?
64
+
65
+ components.items = doc.dig(*path) || {}
66
+ replace_responses(doc.fetch('paths', {}), components)
67
+ bury(doc, path, components.items) unless components.items.empty?
68
+
69
+ dump_result(output_name, YAML.dump(doc, line_width: 1_000_000), 3)
70
+ end
71
+
72
+ exit(main) if (defined? $unit_test).nil?
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Copyright © 2024 Ismo Kärkkäinen
5
+ # Licensed under Universal Permissive License. See LICENSE.txt.
6
+
7
+ require_relative '../lib/apiobjects'
8
+ require_relative '../lib/common'
9
+ require 'optparse'
10
+ require 'yaml'
11
+
12
+
13
+ def remove_subitem(obj, path)
14
+ return obj.delete(path.first) || {} if path.size == 1
15
+ parent = obj.dig(*path.take(path.size - 1))
16
+ return {} if parent.nil?
17
+ parent.delete(path.last) || {}
18
+ end
19
+
20
+ def replace_inlines(obj, components, top_level = false)
21
+ if obj.is_a?(Array)
22
+ obj.each do |o|
23
+ return false unless replace_inlines(o, components)
24
+ end
25
+ return true
26
+ end
27
+ return true unless obj.is_a?(Hash)
28
+ return true if obj.key?('$ref')
29
+ # Prior return would be the place to get rid of other keys if so desired.
30
+ # Is inlined, process parts recursively.
31
+ items = obj['allOf']
32
+ items = obj['anyOf'] if items.nil?
33
+ items = obj['oneOf'] if items.nil?
34
+ # All above need special treatment in code generation templates.
35
+ return replace_inlines(items, components) unless items.nil?
36
+ t = obj['type']
37
+ if t.nil? # Some kind of intermediate-level object.
38
+ (obj.keys.sort! { |a, b| a.to_s <=> b.to_s }).each do |key|
39
+ return false unless replace_inlines(obj[key], components)
40
+ end
41
+ return true
42
+ end
43
+ case t
44
+ when 'array'
45
+ return false unless replace_inlines(obj['items'], components)
46
+ when 'object'
47
+ props = obj.fetch('properties', {})
48
+ props.keys.sort!.each do |name|
49
+ return false unless replace_inlines(props[name], components)
50
+ end
51
+ end
52
+ obj.replace({ '$ref' => components.reference(obj) }) unless top_level
53
+ true
54
+ end
55
+
56
+ def main
57
+ input_name = nil
58
+ output_name = nil
59
+ path = %w[components schemas]
60
+ components = Components.new(path, 'Schema')
61
+
62
+ ENV['POSIXLY_CORRECT'] = '1'
63
+ parser = OptionParser.new do |opts|
64
+ opts.summary_indent = ' '
65
+ opts.summary_width = 26
66
+ opts.banner = 'Usage: openapi-addschemas [options]'
67
+ opts.separator ''
68
+ opts.separator 'Options:'
69
+ opts.on('-i', '--input FILE', 'Read API spec from FILE, not stdin.') do |f|
70
+ input_name = f
71
+ end
72
+ opts.on('-o', '--output FILE', 'Output to FILE, not stdout .') do |f|
73
+ output_name = f
74
+ end
75
+ components.add_options(opts)
76
+ opts.on('-h', '--help', 'Print this help and exit.') do
77
+ $stdout.puts %(#{opts}
78
+
79
+ Loads API document in OpenAPI format and adds a schema for each inline type.
80
+
81
+ #{components.help}
82
+ )
83
+ exit 0
84
+ end
85
+ end
86
+ parser.parse!
87
+
88
+ doc = load_source(input_name)
89
+ return 2 if doc.nil?
90
+
91
+ # Find schema object and remove it temporarily to prevent being
92
+ # processed again.
93
+ components.items = remove_subitem(doc, path)
94
+ # Get rid of inlined types within existing schemas first.
95
+ components.items.keys.sort!.each do |k|
96
+ return 4 unless replace_inlines(components.items[k], components, true)
97
+ end
98
+ return 4 unless replace_inlines(doc, components)
99
+ bury(doc, path, components.items) unless components.items.empty?
100
+
101
+ dump_result(output_name, YAML.dump(doc, line_width: 1_000_000), 3)
102
+ end
103
+
104
+ exit(main) if (defined? $unit_test).nil?
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Copyright © 2024 Ismo Kärkkäinen
5
+ # Licensed under Universal Permissive License. See LICENSE.txt.
6
+
7
+ require_relative '../lib/apiobjects'
8
+ require_relative '../lib/common'
9
+ require 'optparse'
10
+ require 'yaml'
11
+
12
+
13
+ def typesame(a, b, ignored_keys)
14
+ return same(a, b, ignored_keys) unless a['type'] == 'object'
15
+ return same(a, b, ignored_keys) unless b['type'] == 'object'
16
+ # For each property in a, find out unused mathing property in b that is
17
+ # also either required or not.
18
+ # Requiring property affects e.g. C++ by allowing direct membership or
19
+ # when optional, a pointer that can have nullptr value. Or class instance
20
+ # or null in TypeScript so I consider required to affect type.
21
+ pa = a.fetch('properties', {})
22
+ ra = a.fetch('required', [])
23
+ pb = b.fetch('properties', {}).merge
24
+ rb = b.fetch('required', [])
25
+ pa.each do |aname, aspec|
26
+ bnames = pb.keys.sort!
27
+ if ra.include?(aname)
28
+ bnames.select! { |n| rb.include?(n) }
29
+ else
30
+ bnames.reject! { |n| rb.include?(n) }
31
+ end
32
+ matched = false
33
+ bnames.each do |bname|
34
+ bspec = pb[bname]
35
+ # Intended use is after openapi-addschemas so same should work as well.
36
+ if typesame(aspec, bspec, ignored_keys)
37
+ pb.delete(bname)
38
+ matched = true
39
+ break
40
+ end
41
+ end
42
+ return false unless matched
43
+ end
44
+ pb.empty? # All matched and nothing left in pb.
45
+ end
46
+
47
+ def keys2array(keys)
48
+ keys.to_h { |k| [ k, [] ] }
49
+ end
50
+
51
+ def gather_equivalencies(components)
52
+ order = components.items.keys.sort!
53
+ result = keys2array(order)
54
+ (order.size - 1).times do |k|
55
+ item = components.items[order[k]]
56
+ ((k + 1)...order.size).each do |n|
57
+ if same(item, components.items[order[n]], components.ignored_keys)
58
+ result[order[k]].push(order[n])
59
+ result[order[n]].push(order[k])
60
+ end
61
+ end
62
+ end
63
+ result.delete_if { |_k, v| v.empty? }
64
+ end
65
+
66
+ def gather_typematches(components)
67
+ order = components.items.keys.sort!
68
+ result = keys2array(order)
69
+ (order.size - 1).times do |k|
70
+ item = components.items[order[k]]
71
+ ((k + 1)...order.size).each do |n|
72
+ if typesame(item, components.items[order[n]], components.ignored_keys)
73
+ result[order[k]].push(order[n])
74
+ result[order[n]].push(order[k])
75
+ end
76
+ end
77
+ end
78
+ result.delete_if { |_k, v| v.empty? }
79
+ end
80
+
81
+ def gather_refs(obj, prefix, past, refs)
82
+ if obj.is_a?(Hash)
83
+ obj.each do |k, v|
84
+ if v.is_a?(String) && v.start_with?(prefix)
85
+ p = past.join('/')
86
+ if refs.key?(v)
87
+ refs[v].push(p)
88
+ else
89
+ refs[v] = [ p ]
90
+ end
91
+ else
92
+ past.push(k)
93
+ gather_refs(v, prefix, past, refs)
94
+ past.pop
95
+ end
96
+ end
97
+ elsif obj.is_a?(Array)
98
+ obj.size.times do |n|
99
+ past.push(n)
100
+ gather_refs(obj[n], prefix, past, refs)
101
+ past.pop
102
+ end
103
+ end
104
+ end
105
+
106
+ def gather_references(doc, prefix)
107
+ refs = {}
108
+ gather_refs(doc, prefix, [], refs)
109
+ out = {}
110
+ refs.keys.sort!.each do |k|
111
+ r = refs[k]
112
+ next if r.empty?
113
+ out[k] = r.sort!
114
+ end
115
+ out
116
+ end
117
+
118
+ def drop_untouched(refs, report, prefix)
119
+ touched = Set.new
120
+ %w[equivalent typematch].each do |s|
121
+ r = report[s]
122
+ next if r.nil?
123
+ r.keys.each { |k| touched.add("#{prefix}#{k}") }
124
+ end
125
+ refs.delete_if { |k, _v| !touched.member?(k) }
126
+ end
127
+
128
+ def count_refs(refs)
129
+ counts = {}
130
+ refs.each { |k, v| counts[k] = v.size }
131
+ counts
132
+ end
133
+
134
+ def main
135
+ input_name = nil
136
+ output_name = nil
137
+ path = %w[components schemas]
138
+ components = Components.new(path, '')
139
+ equivalent = true
140
+ typematch = false
141
+ references = false
142
+ count = true
143
+ keep = false
144
+
145
+ ENV['POSIXLY_CORRECT'] = '1'
146
+ parser = OptionParser.new do |opts|
147
+ opts.summary_indent = ' '
148
+ opts.summary_width = 26
149
+ opts.banner = 'Usage: openapi-checkschemas [options]'
150
+ opts.separator ''
151
+ opts.separator 'Options:'
152
+ opts.on('-i', '--input FILE', 'Read API spec from FILE, not stdin.') do |f|
153
+ input_name = f
154
+ end
155
+ opts.on('-o', '--output FILE', 'Output to FILE, not stdout .') do |f|
156
+ output_name = f
157
+ end
158
+ opts.on('-e', '--[no-]equivalent', "Report equivalent schemas, default #{equivalent}") do |b|
159
+ equivalent = b
160
+ end
161
+ opts.on('-t', '--[no-]typematch', "Report typematch schemas, default #{typematch}") do |b|
162
+ typematch = b
163
+ end
164
+ opts.on('-r', '--[no-]reference', "Report schema references, default #{references}") do |b|
165
+ references = b
166
+ end
167
+ opts.on('-c', '--[no-]count', "Report schema reference counts, default #{count}") do |b|
168
+ count = b
169
+ end
170
+ opts.on('-k', '--[no-]keep', "Keep all schema references/counts, default #{keep}") do |b|
171
+ keep = b
172
+ end
173
+ components.add_options(opts)
174
+ opts.on('-h', '--help', 'Print this help and exit.') do
175
+ $stdout.puts %(#{opts}
176
+
177
+ Loads API document in OpenAPI format and checks if any schemas are duplicates
178
+ of another and to what degree. Equivalency may be an exact match, or property
179
+ names differ but types match.
180
+
181
+ #{components.help}
182
+
183
+ Search is performed only at top schema level. References to equivalent types
184
+ are not considered equivalent when references themselves are not equivalent.
185
+ Any allOf, anyOf, oneOf checks merely check the refernces. Hence two different
186
+ allOf schemas may in practice result in equivalent types and that is not
187
+ detected.
188
+
189
+ Implicit expectation is the openapi-addschemas has been used to process the
190
+ input, as inlined types in requests for example are ignored.
191
+ )
192
+ exit 0
193
+ end
194
+ end
195
+ parser.parse!
196
+
197
+ doc = load_source(input_name)
198
+ return 2 if doc.nil?
199
+
200
+ components.items = doc.dig(*path) || {}
201
+ report = {}
202
+ report['equivalent'] = gather_equivalencies(components) if equivalent
203
+ report['typematch'] = gather_typematches(components) if typematch
204
+ if references || count
205
+ refs = gather_references(doc, components.path)
206
+ if equivalent || typematch # There is something to limit references to.
207
+ refs = drop_untouched(refs, report, components.path) unless keep
208
+ end
209
+ report['reference'] = refs if references
210
+ report['count'] = count_refs(refs) if count
211
+ end
212
+ # Filter reference counts that are not for type in equivalent or typematch.
213
+ # Could check for pretty-printed output format here.
214
+ dump_result(output_name, YAML.dump(report, line_width: 80), 3)
215
+ end
216
+
217
+ exit(main) if (defined? $unit_test).nil?