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.
@@ -1,105 +1,78 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- # Copyright © 2021 Ismo Kärkkäinen
4
+ # Copyright © 2021-2024 Ismo Kärkkäinen
5
5
  # Licensed under Universal Permissive License. See LICENSE.txt.
6
6
 
7
- require_relative '../lib/common.rb'
7
+ require_relative '../lib/common'
8
8
  require 'optparse'
9
9
  require 'yaml'
10
- require 'json'
11
10
 
11
+ def main
12
+ input_name = nil
13
+ output_name = nil
14
+ paths_name = nil
12
15
 
13
- default_env(:in, '')
14
- default_env(:out, '')
15
- default_env(:format, 'YAML')
16
- default_env(:paths, '')
17
-
18
- parser = OptionParser.new do |opts|
19
- opts.summary_indent = ' '
20
- opts.summary_width = 24
21
- opts.banner = 'Usage: openapi-frequencies [options]'
22
- opts.separator ''
23
- opts.separator 'Options (equivalent environment variable and value in parentheses):'
24
- opts.on('-i', '--input FILE', 'Read source from FILE, not stdin (IN=FILE).') do |f|
25
- env(:in, f)
26
- end
27
- opts.on('-o', '--output FILE', 'Output result to FILE, not stdout (OUT=FILE).') do |f|
28
- env(:out, f)
29
- end
30
- opts.on('-p', '--paths FILE', 'Path log file name (PATHS=FILE).') do |f|
31
- env(:paths, f)
32
- end
33
- opts.on('--yaml', 'Output format is YAML (default) (FORMAT=YAML).') do
34
- env(:format, 'YAML')
35
- end
36
- opts.on('--json', 'Output format is JSON. (FORMAT=JSON)') do
37
- env(:format, 'JSON')
38
- end
39
- opts.on('-h', '--help', 'Print this help and exit.') do
40
- $stdout.puts %(#{opts}
16
+ parser = OptionParser.new do |opts|
17
+ opts.summary_indent = ' '
18
+ opts.summary_width = 24
19
+ opts.banner = 'Usage: openapi-frequencies [options]'
20
+ opts.separator ''
21
+ opts.separator 'Options:'
22
+ opts.on('-i', '--input FILE', 'Read source from FILE, not stdin.') do |f|
23
+ input_name = f
24
+ end
25
+ opts.on('-o', '--output FILE', 'Output result to FILE, not stdout.') do |f|
26
+ output_name = f
27
+ end
28
+ opts.on('-p', '--paths FILE', 'Path log file name.') do |f|
29
+ paths_name = f
30
+ end
31
+ opts.on('-h', '--help', 'Print this help and exit.') do
32
+ $stdout.puts %(#{opts}
41
33
 
42
- Matches given file with paths and adds frequency information.
34
+ Matches given OpenAPI document file paths with paths from file and adds
35
+ into each path object matching count as "freq".
43
36
  )
44
- exit 0
37
+ exit 0
38
+ end
45
39
  end
46
- end
47
- parser.parse!
40
+ parser.parse!
48
41
 
49
- aargh("Format neither JSON nor YAML: #{env(:format)}", 1) unless %w[JSON YAML].include? env(:format)
50
- fn = env(:paths)
51
- aargh("Path log file name must be given.") if fn.empty?
42
+ return aargh('Path log file name must be given.', 1) if paths_name.nil?
52
43
 
53
- def read_source(filename)
54
- YAML.safe_load(filename.empty? ? $stdin : File.read(filename))
55
- rescue Errno::ENOENT => e
56
- aargh("Could not read #{filename.empty? ? 'stdin' : filename}", 2)
57
- rescue StandardError => e
58
- aargh(e.to_s, 3)
59
- end
60
- doc = read_source(env(:in))
44
+ doc = load_source(input_name)
45
+ return 2 if doc.nil?
61
46
 
62
- begin
63
- # Expect one path per line with nothing else. Query is allowed and ignored.
64
- f = File.new(fn, 'rt')
65
- f.each_line do |line|
66
- p = ServerPath.new(split_path(line))
67
- doc['paths'].each_value do |info|
68
- next unless (p <=> info['parts']) == 0
69
- info['freq'] = info.fetch('freq', 0) + 1
70
- # Lookalikes are the others that can be matched, since path has no
71
- # variables so any change in fixed parts (not a lookalike) will be a
72
- # mismatch with path.
73
- info['lookalike'].each do |path|
74
- cand = doc['paths'][path]
75
- next unless (p <=> cand['parts']) == 0
76
- cand['freq'] = cand.fetch('freq', 0) + 1
47
+ begin
48
+ # Expect one path per line with nothing else. Query is allowed and ignored.
49
+ f = File.new(paths_name, 'rt')
50
+ f.each_line do |line|
51
+ p = ServerPath.new(split_path(line))
52
+ doc['paths'].each_value do |info|
53
+ next unless (p <=> info['parts']).zero?
54
+ info['freq'] = info.fetch('freq', 0) + 1
55
+ # Lookalikes are the others that can be matched, since path has no
56
+ # variables so any change in fixed parts (not a lookalike) will be a
57
+ # mismatch with path.
58
+ info['lookalike'].each do |path|
59
+ cand = doc['paths'][path]
60
+ next unless (p <=> cand['parts']).zero?
61
+ cand['freq'] = cand.fetch('freq', 0) + 1
62
+ end
63
+ break
77
64
  end
78
- break
79
65
  end
66
+ f.close
67
+ rescue Errno::ENOENT
68
+ return aargh("Could not read path log file: #{paths_name}", 4)
69
+ rescue NoMethodError
70
+ return aargh('Is input file an output of openapi-processpaths?', 5)
71
+ rescue StandardError => e
72
+ return aargh(e.to_s, 6)
80
73
  end
81
- f.close
82
- rescue Errno::ENOENT => e
83
- aargh("Could not read path log file: #{fn}", 4)
84
- rescue NoMethodError
85
- aargh('Is input file an output of openapi-processpaths?', 5)
86
- rescue StandardError => e
87
- aargh(e.to_s, 6)
88
- end
89
74
 
90
- output = env(:out)
91
- if output.empty?
92
- output = $stdout
93
- else
94
- begin
95
- output = File.open(output, 'w')
96
- rescue StandardError
97
- aargh("Failed to open for writing: #{output}", 7)
98
- end
75
+ dump_result(output_name, YAML.dump(doc), 3)
99
76
  end
100
77
 
101
- case env(:format)
102
- when 'JSON' then output.puts JSON.generate(doc)
103
- when 'YAML' then output.puts YAML.dump(doc)
104
- end
105
- output.close
78
+ exit(main) if (defined? $unit_test).nil?
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Copyright © 2021-2024 Ismo Kärkkäinen
5
+ # Licensed under Universal Permissive License. See LICENSE.txt.
6
+
7
+ require_relative '../lib/common'
8
+ require_relative '../lib/loaders'
9
+ require_relative '../lib/gen'
10
+ require_relative '../lib/generate'
11
+ require 'optparse'
12
+ require 'yaml'
13
+
14
+
15
+ def main
16
+ input_name = nil
17
+ output_dir = nil
18
+
19
+ ENV['POSIXLY_CORRECT'] = '1'
20
+ parser = OptionParser.new do |opts|
21
+ opts.summary_indent = ' '
22
+ opts.summary_width = 26
23
+ opts.banner = 'Usage: openapi-generate [options] generator-names...'
24
+ opts.separator ''
25
+ opts.separator 'Options:'
26
+ opts.on('-i', '--input FILE', 'Read processed API from FILE, not stdin.') do |f|
27
+ input_name = f
28
+ end
29
+ opts.on('-o', '--outdir DIR', 'Output directory.') do |d|
30
+ output_dir = d
31
+ end
32
+ opts.on('-h', '--help', 'Print this help and exit.') do
33
+ $stdout.puts %(#{opts}
34
+ Loads API document in OpenAPI format and generator names. Built-in generator
35
+ loaders accept the following:
36
+ #{Loaders.document.strip}
37
+
38
+ During load each generator can add and modify tasks via Gen module:
39
+ #{Gen.document.strip}
40
+
41
+ After all generators have loaded succesfully, tasks are run.
42
+ )
43
+ exit 0
44
+ end
45
+ end
46
+ parser.parse!
47
+
48
+ return aargh('Generator names must be given.', 1) if ARGV.empty?
49
+
50
+ a = load_source(input_name)
51
+ return 2 if a.nil?
52
+ return aargh("Not a directory: #{output_dir}", 3) unless File.directory?(output_dir)
53
+ gen = Generator.new(a, input_name, output_dir)
54
+ ec = gen.load(ARGV)
55
+ return ec unless ec.zero?
56
+ gen.run
57
+ end
58
+
59
+ exit(main) if defined?($unit_test).nil?
data/bin/openapi-merge CHANGED
@@ -1,63 +1,14 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- # Copyright © 2021 Ismo Kärkkäinen
4
+ # Copyright © 2021-2024 Ismo Kärkkäinen
5
5
  # Licensed under Universal Permissive License. See LICENSE.txt.
6
6
 
7
- require_relative '../lib/common.rb'
7
+ require_relative '../lib/common'
8
8
  require 'optparse'
9
9
  require 'yaml'
10
- require 'json'
11
10
  require 'set'
12
11
 
13
-
14
- default_env(:out, '')
15
- default_env(:format, 'YAML')
16
- default_env(:prune, true);
17
-
18
- ENV['POSIXLY_CORRECT'] = '1'
19
- parser = OptionParser.new do |opts|
20
- opts.summary_indent = ' '
21
- opts.summary_width = 26
22
- opts.banner = 'Usage: openapi-merge [options] sources...'
23
- opts.separator ''
24
- opts.separator 'Options (equivalent environment variable and value in parentheses):'
25
- opts.on('-o', '--output FILE', 'Output result to FILE, not stdout (OUT=FILE).') do |f|
26
- env(:out, f)
27
- end
28
- opts.on('-k', '--keep', 'Keep all unreferenced objects under components.') do
29
- env(:prune, false)
30
- end
31
- opts.on('-p', '--prune', 'Prune all unreferenced objects under components (default).') do
32
- env(:prune, true)
33
- end
34
- opts.on('--yaml', 'Output format is YAML (default).') do
35
- env(:format, 'YAML')
36
- end
37
- opts.on('--json', 'Output format is JSON.') do
38
- env(:format, 'JSON')
39
- end
40
- opts.on('-h', '--help', 'Print this help and exit.') do
41
- $stdout.puts %(#{opts}
42
-
43
- Source files are combined to form one API specification document. Sources are
44
- allowed only to append to the merged document, not re-define anything in it.
45
- )
46
- exit 0
47
- end
48
- end
49
- parser.parse!
50
-
51
- aargh("Format neither JSON nor YAML: #{env(:format)}", 1) unless %w[JSON YAML].include? env(:format)
52
-
53
- def read_source(filename)
54
- YAML.safe_load(File.read(filename))
55
- rescue Errno::ENOENT => e
56
- aargh("Could not read #{filename}", 2)
57
- rescue StandardError => e
58
- aargh(e.to_s, 3)
59
- end
60
-
61
12
  def raise_se(message)
62
13
  raise StandardError, message
63
14
  end
@@ -77,12 +28,12 @@ end
77
28
 
78
29
  def add_undefined(merged, incoming, filename, path, max_depths)
79
30
  incoming.each_pair do |key, value|
80
- unless merged.has_key? key
31
+ unless merged.key? key
81
32
  merged[key] = value
82
33
  next
83
34
  end
84
35
  m = merged[key]
85
- raise_se "Path #{path_combo(path, false)} merged type #{m.class} differs from type #{value.class} in #{filename}" unless m.class == value.class
36
+ raise_se("Path #{path_combo(path, false)} merged type #{m.class} differs from type #{value.class} in #{filename}") unless m.instance_of?(value.class)
86
37
  raise_se("Re-definition of #{key} #{path_combo(path)} in #{filename}") if too_deep(path, max_depths)
87
38
  if m.is_a? Hash # paths or similar
88
39
  path.push key
@@ -97,22 +48,6 @@ def add_undefined(merged, incoming, filename, path, max_depths)
97
48
  raise_se "Re-definition of #{key} #{path_combo(path)} in #{filename}"
98
49
  end
99
50
  end
100
- rescue StandardError => e
101
- aargh(e.to_s, 3)
102
- end
103
-
104
- max_depths = Hash.new(0)
105
- max_depths['openapi'] = 1
106
- max_depths['info'] = 1
107
- max_depths['servers'] = 1
108
- max_depths['paths'] = 2 # Allows get, post, options, etc. from different files.
109
- max_depths['webhooks'] = 2
110
- max_depths['components'] = 2
111
- max_depths['security'] = 1
112
- max_depths['tags'] = 1
113
- merged = {}
114
- ARGV.each do |filename|
115
- add_undefined(merged, read_source(filename), filename, [], max_depths)
116
51
  end
117
52
 
118
53
  def gather_refs(doc, found)
@@ -129,7 +64,7 @@ def gather_refs(doc, found)
129
64
  end
130
65
  end
131
66
 
132
- if env(:prune)
67
+ def prune(merged)
133
68
  prev_refs = Set.new
134
69
  loop do # May have references from deleted so repeat until nothing deleted.
135
70
  refs = Set.new
@@ -153,7 +88,7 @@ if env(:prune)
153
88
  sub = used
154
89
  p.each_index do |k|
155
90
  if k + 1 < p.size
156
- sub[p[k]] = {} unless sub.has_key? p[k]
91
+ sub[p[k]] = {} unless sub.key? p[k]
157
92
  sub = sub[p[k]]
158
93
  else
159
94
  sub[p[k]] = item
@@ -166,19 +101,58 @@ if env(:prune)
166
101
  end
167
102
  end
168
103
 
169
- output = env(:out)
170
- if output.empty?
171
- output = $stdout
172
- else
173
- begin
174
- output = File.open(output, 'w')
175
- rescue StandardError
176
- aargh("Failed to open for writing: #{output}", 1)
104
+ def main
105
+ output_file = nil
106
+ keep = false
107
+
108
+ ENV['POSIXLY_CORRECT'] = '1'
109
+ parser = OptionParser.new do |opts|
110
+ opts.summary_indent = ' '
111
+ opts.summary_width = 26
112
+ opts.banner = 'Usage: openapi-merge [options] sources...'
113
+ opts.separator ''
114
+ opts.separator 'Options:'
115
+ opts.on('-o', '--output FILE', 'Output result to FILE, not stdout.') do |f|
116
+ output_file = f
117
+ end
118
+ opts.on('-k', '--keep', 'Keep all unreferenced objects under components.') do
119
+ keep = true
120
+ end
121
+ opts.on('-p', '--prune', 'Prune all unreferenced objects under components (default).') do
122
+ keep = false
123
+ end
124
+ opts.on('-h', '--help', 'Print this help and exit.') do
125
+ $stdout.puts %(#{opts}
126
+
127
+ Source files are combined to form one API specification document. Sources are
128
+ allowed only to append to the merged document, not re-define anything in it.
129
+ )
130
+ exit 0
131
+ end
177
132
  end
178
- end
133
+ parser.parse!
134
+
135
+ max_depths = Hash.new(0)
136
+ max_depths['openapi'] = 1
137
+ max_depths['info'] = 1
138
+ max_depths['servers'] = 1
139
+ max_depths['paths'] = 2 # Allows get, post, etc. from different files.
140
+ max_depths['webhooks'] = 2
141
+ max_depths['components'] = 2
142
+ max_depths['security'] = 1
143
+ max_depths['tags'] = 1
144
+ merged = {}
145
+ ARGV.each do |filename|
146
+ s = load_source(filename)
147
+ return 2 if s.nil?
148
+ add_undefined(merged, s, filename, [], max_depths)
149
+ rescue StandardError => e
150
+ return aargh(e.to_s, 4)
151
+ end
152
+
153
+ prune(merged) unless keep
179
154
 
180
- case env(:format)
181
- when 'JSON' then output.puts JSON.generate(merged)
182
- when 'YAML' then output.puts YAML.dump(merged)
155
+ dump_result(output_file, YAML.dump(merged), 3)
183
156
  end
184
- output.close
157
+
158
+ exit(main) if (defined? $unit_test).nil?
@@ -1,111 +1,84 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- # Copyright © 2021 Ismo Kärkkäinen
4
+ # Copyright © 2021-2024 Ismo Kärkkäinen
5
5
  # Licensed under Universal Permissive License. See LICENSE.txt.
6
6
 
7
- require_relative '../lib/common.rb'
7
+ require_relative '../lib/common'
8
8
  require 'optparse'
9
9
  require 'yaml'
10
- require 'json'
11
10
 
12
11
 
13
- default_env(:in, '')
14
- default_env(:out, '')
15
- default_env(:format, 'YAML')
16
- default_env(:error, true)
12
+ def main
13
+ input_name = nil
14
+ output_name = nil
15
+ error = true
17
16
 
18
- parser = OptionParser.new do |opts|
19
- opts.summary_indent = ' '
20
- opts.summary_width = 24
21
- opts.banner = 'Usage: openapi-processpaths [options]'
22
- opts.separator ''
23
- opts.separator 'Options (equivalent environment variable and value in parentheses):'
24
- opts.on('-i', '--input FILE', 'Read source from FILE, not stdin (IN=FILE).') do |f|
25
- env(:in, f)
26
- end
27
- opts.on('-o', '--output FILE', 'Output result to FILE, not stdout (OUT=FILE).') do |f|
28
- env(:out, f)
29
- end
30
- opts.on('--warn', 'Only warn of paths with same fixed parts (ERROR=0).') do
31
- env(:error, false)
32
- end
33
- opts.on('--yaml', 'Output format is YAML (default) (FORMAT=YAML).') do
34
- env(:format, 'YAML')
35
- end
36
- opts.on('--json', 'Output format is JSON. (FORMAT=JSON)') do
37
- env(:format, 'JSON')
38
- end
39
- opts.on('-h', '--help', 'Print this help and exit.') do
40
- $stdout.puts %(#{opts}
17
+ parser = OptionParser.new do |opts|
18
+ opts.summary_indent = ' '
19
+ opts.summary_width = 24
20
+ opts.banner = 'Usage: openapi-processpaths [options]'
21
+ opts.separator ''
22
+ opts.separator 'Options:'
23
+ opts.on('-i', '--input FILE', 'Read source from FILE, not stdin.') do |f|
24
+ input_name = f
25
+ end
26
+ opts.on('-o', '--output FILE', 'Output result to FILE, not stdout.') do |f|
27
+ output_name = f
28
+ end
29
+ opts.on('--warn', 'Only warn of paths with same fixed parts.') do
30
+ error = false
31
+ end
32
+ opts.on('-h', '--help', 'Print this help and exit.') do
33
+ $stdout.puts %(#{opts}
41
34
 
42
35
  Processes API specification document path objects into form that is expected by
43
36
  later stage tools. Checks for paths that may be ambiguous.
44
37
  )
45
- exit 0
38
+ exit 0
39
+ end
46
40
  end
47
- end
48
- parser.parse!
41
+ parser.parse!
49
42
 
50
- aargh("Format neither JSON nor YAML: #{env(:format)}", 1) unless %w[JSON YAML].include? env(:format)
51
-
52
- def read_source(filename)
53
- YAML.safe_load(filename.empty? ? $stdin : File.read(filename))
54
- rescue Errno::ENOENT => e
55
- aargh("Could not read #{filename.empty? ? 'stdin' : filename}", 2)
56
- rescue StandardError => e
57
- aargh(e.to_s, 3)
58
- end
59
- doc = read_source(env(:in))
43
+ doc = load_source(input_name)
44
+ return 2 if doc.nil?
60
45
 
61
- processed = {}
62
- doc.fetch('paths', {}).each_pair do |path, value|
63
- parts = split_path(path, true)
64
- processed[path] = {
65
- 'parts' => parts,
66
- 'orig' => value,
67
- 'lookalike' => [],
68
- path: ServerPath.new(parts)
69
- }
70
- end
71
-
72
- # Find lookalike sets.
73
- lookalikes = false
74
- paths = processed.keys.sort
75
- paths.each_index do |k|
76
- pk = paths[k]
77
- a = processed[pk]
78
- (0...k).each do |n|
79
- pn = paths[n]
80
- b = processed[pn]
81
- next unless (a[:path].compare(b[:path])) == 0
82
- a['lookalike'].push pn
83
- b['lookalike'].push pk
84
- $stderr.puts("Similar: #{pn} #{pk}")
85
- lookalikes = true
46
+ processed = {}
47
+ doc.fetch('paths', {}).each_pair do |path, value|
48
+ parts = split_path(path, true)
49
+ processed[path] = {
50
+ 'parts' => parts,
51
+ 'orig' => value,
52
+ 'lookalike' => [],
53
+ path: ServerPath.new(parts)
54
+ }
86
55
  end
87
- end
88
- aargh('Similar paths found.', 7) if lookalikes && env(:error)
89
56
 
90
- # Clean-up temporary fields.
91
- processed.each_value do |v|
92
- v.keys.each { |k| v.delete(k) if k.is_a? Symbol }
93
- end
94
- doc['paths'] = processed
57
+ # Find lookalike sets.
58
+ lookalikes = false
59
+ paths = processed.keys.sort!
60
+ paths.size.times do |k|
61
+ pk = paths[k]
62
+ a = processed[pk]
63
+ k.times do |n|
64
+ pn = paths[n]
65
+ b = processed[pn]
66
+ next unless a[:path].compare(b[:path]).zero?
67
+ a['lookalike'].push pn
68
+ b['lookalike'].push pk
69
+ $stderr.puts("Similar: #{pn} #{pk}")
70
+ lookalikes = true
71
+ end
72
+ end
73
+ return aargh('Similar paths found.', 4) if lookalikes && error
95
74
 
96
- output = env(:out)
97
- if output.empty?
98
- output = $stdout
99
- else
100
- begin
101
- output = File.open(output, 'w')
102
- rescue StandardError
103
- aargh("Failed to open for writing: #{output}", 6)
75
+ # Remove temporary fields.
76
+ processed.each_value do |v|
77
+ v.keys.each { |k| v.delete(k) if k.is_a? Symbol }
104
78
  end
105
- end
79
+ doc['paths'] = processed
106
80
 
107
- case env(:format)
108
- when 'JSON' then output.puts JSON.generate(doc)
109
- when 'YAML' then output.puts YAML.dump(doc)
81
+ dump_result(output_name, YAML.dump(doc), 3)
110
82
  end
111
- output.close
83
+
84
+ exit(main) if (defined? $unit_test).nil?