openapi-sourcetools 0.6.0 → 0.7.1
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 +4 -5
- data/bin/openapi-addparameters +3 -4
- data/bin/openapi-addresponses +4 -5
- data/bin/openapi-addschemas +33 -10
- data/bin/openapi-checkschemas +10 -11
- data/bin/openapi-frequencies +2 -2
- data/bin/openapi-generate +2 -3
- data/bin/openapi-merge +15 -9
- data/bin/openapi-modifypaths +123 -0
- data/bin/openapi-processpaths +2 -2
- data/lib/apiobjects.rb +36 -63
- data/lib/common.rb +5 -0
- data/lib/docs.rb +33 -0
- data/lib/gen.rb +40 -23
- data/lib/generate.rb +1 -0
- data/lib/helper.rb +1 -3
- data/lib/loaders.rb +46 -1
- data/lib/output.rb +58 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 71267174b8f95f5a510b93590f8fcdb8daf2737df19db6d2d7ebf48410444cf8
|
4
|
+
data.tar.gz: cf682ab07dcc557c9f3a2b9785f46aded117c4b50d840a6e8dd0b31e80840a63
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: efe4f67e9903c4b65e404fe349e668ff659e538ddff7bc7e28a59bd7f98167e16faabb4876d5b45e1142562ad6e6c4e22c7471e1f374c3debc4e204855a7e169
|
7
|
+
data.tar.gz: 3ab4a1cf50426d6405cb1281f4c159d43480ead4f11414f8642b6e0a5d7cb4eca4f3769fb43cff4c5f1884ef5a72d88fdcc9b9c09bbdeb80f4624b008cfba059
|
data/bin/openapi-addheaders
CHANGED
@@ -32,7 +32,6 @@ def main
|
|
32
32
|
path = %w[components headers]
|
33
33
|
components = Components.new(path, 'Header')
|
34
34
|
|
35
|
-
ENV['POSIXLY_CORRECT'] = '1'
|
36
35
|
parser = OptionParser.new do |opts|
|
37
36
|
opts.summary_indent = ' '
|
38
37
|
opts.summary_width = 26
|
@@ -42,7 +41,7 @@ def main
|
|
42
41
|
opts.on('-i', '--input FILE', 'Read API spec from FILE, not stdin.') do |f|
|
43
42
|
input_name = f
|
44
43
|
end
|
45
|
-
opts.on('-o', '--output FILE', 'Output to FILE, not stdout
|
44
|
+
opts.on('-o', '--output FILE', 'Output to FILE, not stdout.') do |f|
|
46
45
|
output_name = f
|
47
46
|
end
|
48
47
|
components.add_options(opts)
|
@@ -57,7 +56,7 @@ replaces the original with reference.
|
|
57
56
|
exit 0
|
58
57
|
end
|
59
58
|
end
|
60
|
-
parser.
|
59
|
+
parser.order!
|
61
60
|
|
62
61
|
doc = load_source(input_name)
|
63
62
|
return 2 if doc.nil?
|
@@ -67,7 +66,7 @@ replaces the original with reference.
|
|
67
66
|
replace_headers(doc.fetch('paths', {}), components)
|
68
67
|
bury(doc, path, components.items) unless components.items.empty?
|
69
68
|
|
70
|
-
dump_result(output_name,
|
69
|
+
dump_result(output_name, doc, 3)
|
71
70
|
end
|
72
71
|
|
73
|
-
exit(main) if
|
72
|
+
exit(main) if defined?($unit_test).nil?
|
data/bin/openapi-addparameters
CHANGED
@@ -32,7 +32,6 @@ def main
|
|
32
32
|
path = %w[components parameters]
|
33
33
|
components = Components.new(path, 'Parameter')
|
34
34
|
|
35
|
-
ENV['POSIXLY_CORRECT'] = '1'
|
36
35
|
parser = OptionParser.new do |opts|
|
37
36
|
opts.summary_indent = ' '
|
38
37
|
opts.summary_width = 26
|
@@ -42,7 +41,7 @@ def main
|
|
42
41
|
opts.on('-i', '--input FILE', 'Read API spec from FILE, not stdin.') do |f|
|
43
42
|
input_name = f
|
44
43
|
end
|
45
|
-
opts.on('-o', '--output FILE', 'Output to FILE, not stdout
|
44
|
+
opts.on('-o', '--output FILE', 'Output to FILE, not stdout.') do |f|
|
46
45
|
output_name = f
|
47
46
|
end
|
48
47
|
components.add_options(opts)
|
@@ -57,7 +56,7 @@ replaces the original with reference.
|
|
57
56
|
exit 0
|
58
57
|
end
|
59
58
|
end
|
60
|
-
parser.
|
59
|
+
parser.order!
|
61
60
|
|
62
61
|
doc = load_source(input_name)
|
63
62
|
return 2 if doc.nil?
|
@@ -66,7 +65,7 @@ replaces the original with reference.
|
|
66
65
|
replace_parameters(doc.fetch('paths', {}), components)
|
67
66
|
bury(doc, path, components.items) unless components.items.empty?
|
68
67
|
|
69
|
-
dump_result(output_name,
|
68
|
+
dump_result(output_name, doc, 3)
|
70
69
|
end
|
71
70
|
|
72
71
|
exit(main) if defined?($unit_test).nil?
|
data/bin/openapi-addresponses
CHANGED
@@ -32,7 +32,6 @@ def main
|
|
32
32
|
path = %w[components responses]
|
33
33
|
components = Components.new(path, 'Response')
|
34
34
|
|
35
|
-
ENV['POSIXLY_CORRECT'] = '1'
|
36
35
|
parser = OptionParser.new do |opts|
|
37
36
|
opts.summary_indent = ' '
|
38
37
|
opts.summary_width = 26
|
@@ -42,7 +41,7 @@ def main
|
|
42
41
|
opts.on('-i', '--input FILE', 'Read API spec from FILE, not stdin.') do |f|
|
43
42
|
input_name = f
|
44
43
|
end
|
45
|
-
opts.on('-o', '--output FILE', 'Output to FILE, not stdout
|
44
|
+
opts.on('-o', '--output FILE', 'Output to FILE, not stdout.') do |f|
|
46
45
|
output_name = f
|
47
46
|
end
|
48
47
|
components.add_options(opts)
|
@@ -57,7 +56,7 @@ replaces the original with reference.
|
|
57
56
|
exit 0
|
58
57
|
end
|
59
58
|
end
|
60
|
-
parser.
|
59
|
+
parser.order!
|
61
60
|
|
62
61
|
doc = load_source(input_name)
|
63
62
|
return 2 if doc.nil?
|
@@ -66,7 +65,7 @@ replaces the original with reference.
|
|
66
65
|
replace_responses(doc.fetch('paths', {}), components)
|
67
66
|
bury(doc, path, components.items) unless components.items.empty?
|
68
67
|
|
69
|
-
dump_result(output_name,
|
68
|
+
dump_result(output_name, doc, 3)
|
70
69
|
end
|
71
70
|
|
72
|
-
exit(main) if
|
71
|
+
exit(main) if defined?($unit_test).nil?
|
data/bin/openapi-addschemas
CHANGED
@@ -17,7 +17,7 @@ def remove_subitem(obj, path)
|
|
17
17
|
parent.delete(path.last) || {}
|
18
18
|
end
|
19
19
|
|
20
|
-
def replace_inlines(obj, components,
|
20
|
+
def replace_inlines(obj, components, top_level_name = nil)
|
21
21
|
if obj.is_a?(Array)
|
22
22
|
obj.each do |o|
|
23
23
|
return false unless replace_inlines(o, components)
|
@@ -25,8 +25,11 @@ def replace_inlines(obj, components, top_level = false)
|
|
25
25
|
return true
|
26
26
|
end
|
27
27
|
return true unless obj.is_a?(Hash)
|
28
|
-
|
29
|
-
|
28
|
+
if obj.key?('$ref')
|
29
|
+
components.store_anchor(obj)
|
30
|
+
# Here would be the place to get rid of other keys if so desired.
|
31
|
+
return true
|
32
|
+
end
|
30
33
|
# Is inlined, process parts recursively.
|
31
34
|
items = obj['allOf']
|
32
35
|
items = obj['anyOf'] if items.nil?
|
@@ -49,17 +52,34 @@ def replace_inlines(obj, components, top_level = false)
|
|
49
52
|
return false unless replace_inlines(props[name], components)
|
50
53
|
end
|
51
54
|
end
|
52
|
-
|
55
|
+
r = components.ref_string(top_level_name) || components.reference(obj)
|
56
|
+
components.store_anchor(obj, r)
|
57
|
+
obj.replace({ '$ref' => r }) if top_level_name.nil?
|
53
58
|
true
|
54
59
|
end
|
55
60
|
|
61
|
+
def replace_anchor_refs(obj, components)
|
62
|
+
if obj.is_a?(Array)
|
63
|
+
obj.each do |o|
|
64
|
+
replace_anchor_refs(o, components)
|
65
|
+
end
|
66
|
+
return
|
67
|
+
end
|
68
|
+
return unless obj.is_a?(Hash)
|
69
|
+
r = obj['$ref']
|
70
|
+
obj['$ref'] = components.anchor_ref_replacement(r) unless r.nil?
|
71
|
+
obj.values.each do |value|
|
72
|
+
# Loops over $ref value but that is a string so nothing done to it.
|
73
|
+
replace_anchor_refs(value, components)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
56
77
|
def main
|
57
78
|
input_name = nil
|
58
79
|
output_name = nil
|
59
80
|
path = %w[components schemas]
|
60
81
|
components = Components.new(path, 'Schema')
|
61
82
|
|
62
|
-
ENV['POSIXLY_CORRECT'] = '1'
|
63
83
|
parser = OptionParser.new do |opts|
|
64
84
|
opts.summary_indent = ' '
|
65
85
|
opts.summary_width = 26
|
@@ -69,7 +89,7 @@ def main
|
|
69
89
|
opts.on('-i', '--input FILE', 'Read API spec from FILE, not stdin.') do |f|
|
70
90
|
input_name = f
|
71
91
|
end
|
72
|
-
opts.on('-o', '--output FILE', 'Output to FILE, not stdout
|
92
|
+
opts.on('-o', '--output FILE', 'Output to FILE, not stdout.') do |f|
|
73
93
|
output_name = f
|
74
94
|
end
|
75
95
|
components.add_options(opts)
|
@@ -83,7 +103,7 @@ Loads API document in OpenAPI format and adds a schema for each inline type.
|
|
83
103
|
exit 0
|
84
104
|
end
|
85
105
|
end
|
86
|
-
parser.
|
106
|
+
parser.order!
|
87
107
|
|
88
108
|
doc = load_source(input_name)
|
89
109
|
return 2 if doc.nil?
|
@@ -93,12 +113,15 @@ Loads API document in OpenAPI format and adds a schema for each inline type.
|
|
93
113
|
components.items = remove_subitem(doc, path)
|
94
114
|
# Get rid of inlined types within existing schemas first.
|
95
115
|
components.items.keys.sort!.each do |k|
|
96
|
-
|
116
|
+
components.add_schema_name(k)
|
117
|
+
return 4 unless replace_inlines(components.items[k], components, k)
|
97
118
|
end
|
98
119
|
return 4 unless replace_inlines(doc, components)
|
120
|
+
components.alter_anchors
|
99
121
|
bury(doc, path, components.items) unless components.items.empty?
|
122
|
+
replace_anchor_refs(doc, components)
|
100
123
|
|
101
|
-
dump_result(output_name,
|
124
|
+
dump_result(output_name, doc, 3)
|
102
125
|
end
|
103
126
|
|
104
|
-
exit(main) if
|
127
|
+
exit(main) if defined?($unit_test).nil?
|
data/bin/openapi-checkschemas
CHANGED
@@ -142,7 +142,6 @@ def main
|
|
142
142
|
count = true
|
143
143
|
keep = false
|
144
144
|
|
145
|
-
ENV['POSIXLY_CORRECT'] = '1'
|
146
145
|
parser = OptionParser.new do |opts|
|
147
146
|
opts.summary_indent = ' '
|
148
147
|
opts.summary_width = 26
|
@@ -152,22 +151,22 @@ def main
|
|
152
151
|
opts.on('-i', '--input FILE', 'Read API spec from FILE, not stdin.') do |f|
|
153
152
|
input_name = f
|
154
153
|
end
|
155
|
-
opts.on('-o', '--output FILE', 'Output to FILE, not stdout
|
154
|
+
opts.on('-o', '--output FILE', 'Output to FILE, not stdout.') do |f|
|
156
155
|
output_name = f
|
157
156
|
end
|
158
|
-
opts.on('-e', '--[no-]equivalent', "Report equivalent schemas, default #{equivalent}") do |b|
|
157
|
+
opts.on('-e', '--[no-]equivalent', "Report equivalent schemas, default #{yesno(equivalent)}") do |b|
|
159
158
|
equivalent = b
|
160
159
|
end
|
161
|
-
opts.on('-t', '--[no-]typematch', "Report typematch schemas, default #{typematch}") do |b|
|
160
|
+
opts.on('-t', '--[no-]typematch', "Report typematch schemas, default #{yesno(typematch)}") do |b|
|
162
161
|
typematch = b
|
163
162
|
end
|
164
|
-
opts.on('-r', '--[no-]reference', "Report schema references, default #{references}") do |b|
|
163
|
+
opts.on('-r', '--[no-]reference', "Report schema references, default #{yesno(references)}") do |b|
|
165
164
|
references = b
|
166
165
|
end
|
167
|
-
opts.on('-c', '--[no-]count', "Report schema reference counts, default #{count}") do |b|
|
166
|
+
opts.on('-c', '--[no-]count', "Report schema reference counts, default #{yesno(count)}") do |b|
|
168
167
|
count = b
|
169
168
|
end
|
170
|
-
opts.on('-k', '--[no-]keep', "Keep all schema references/counts, default #{keep}") do |b|
|
169
|
+
opts.on('-k', '--[no-]keep', "Keep all schema references/counts, default #{yesno(keep)}") do |b|
|
171
170
|
keep = b
|
172
171
|
end
|
173
172
|
components.add_options(opts)
|
@@ -182,17 +181,17 @@ names differ but types match.
|
|
182
181
|
|
183
182
|
Search is performed only at top schema level. References to equivalent types
|
184
183
|
are not considered equivalent when references themselves are not equivalent.
|
185
|
-
Any allOf, anyOf, oneOf checks merely check the
|
184
|
+
Any allOf, anyOf, oneOf checks merely check the references. Hence two different
|
186
185
|
allOf schemas may in practice result in equivalent types and that is not
|
187
186
|
detected.
|
188
187
|
|
189
188
|
Implicit expectation is the openapi-addschemas has been used to process the
|
190
|
-
input, as inlined types in requests for example are ignored.
|
189
|
+
input, as inlined types in requests, for example, are ignored.
|
191
190
|
)
|
192
191
|
exit 0
|
193
192
|
end
|
194
193
|
end
|
195
|
-
parser.
|
194
|
+
parser.order!
|
196
195
|
|
197
196
|
doc = load_source(input_name)
|
198
197
|
return 2 if doc.nil?
|
@@ -214,4 +213,4 @@ input, as inlined types in requests for example are ignored.
|
|
214
213
|
dump_result(output_name, YAML.dump(report, line_width: 80), 3)
|
215
214
|
end
|
216
215
|
|
217
|
-
exit(main) if
|
216
|
+
exit(main) if defined?($unit_test).nil?
|
data/bin/openapi-frequencies
CHANGED
@@ -37,7 +37,7 @@ into each path object matching count as "freq".
|
|
37
37
|
exit 0
|
38
38
|
end
|
39
39
|
end
|
40
|
-
parser.
|
40
|
+
parser.order!
|
41
41
|
|
42
42
|
return aargh('Path log file name must be given.', 1) if paths_name.nil?
|
43
43
|
|
@@ -75,4 +75,4 @@ into each path object matching count as "freq".
|
|
75
75
|
dump_result(output_name, YAML.dump(doc), 3)
|
76
76
|
end
|
77
77
|
|
78
|
-
exit(main) if
|
78
|
+
exit(main) if defined?($unit_test).nil?
|
data/bin/openapi-generate
CHANGED
@@ -16,7 +16,6 @@ def main
|
|
16
16
|
input_name = nil
|
17
17
|
output_dir = nil
|
18
18
|
|
19
|
-
ENV['POSIXLY_CORRECT'] = '1'
|
20
19
|
parser = OptionParser.new do |opts|
|
21
20
|
opts.summary_indent = ' '
|
22
21
|
opts.summary_width = 26
|
@@ -32,7 +31,7 @@ def main
|
|
32
31
|
opts.on('-h', '--help', 'Print this help and exit.') do
|
33
32
|
$stdout.puts %(#{opts}
|
34
33
|
Loads API document in OpenAPI format and generator names. Built-in generator
|
35
|
-
loaders accept the following:
|
34
|
+
or additional document loaders accept the following:
|
36
35
|
#{Loaders.document.strip}
|
37
36
|
|
38
37
|
During load each generator can add and modify tasks via Gen module:
|
@@ -43,7 +42,7 @@ After all generators have loaded succesfully, tasks are run.
|
|
43
42
|
exit 0
|
44
43
|
end
|
45
44
|
end
|
46
|
-
parser.
|
45
|
+
parser.order!
|
47
46
|
|
48
47
|
return aargh('Generator names must be given.', 1) if ARGV.empty?
|
49
48
|
|
data/bin/openapi-merge
CHANGED
@@ -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
|
@@ -155,4 +161,4 @@ allowed only to append to the merged document, not re-define anything in it.
|
|
155
161
|
dump_result(output_file, YAML.dump(merged), 3)
|
156
162
|
end
|
157
163
|
|
158
|
-
exit(main) if
|
164
|
+
exit(main) if defined?($unit_test).nil?
|
@@ -0,0 +1,123 @@
|
|
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/common'
|
8
|
+
require 'optparse'
|
9
|
+
require 'yaml'
|
10
|
+
|
11
|
+
def path2pieces(s)
|
12
|
+
s.split('/').reject { |p| p.empty? }
|
13
|
+
end
|
14
|
+
|
15
|
+
def pieces2path(p)
|
16
|
+
"/#{p.join('/')}"
|
17
|
+
end
|
18
|
+
|
19
|
+
def add(s, path)
|
20
|
+
s.empty? ? path : (s + path)
|
21
|
+
end
|
22
|
+
|
23
|
+
def remove_complete_prefix(prefix, path)
|
24
|
+
return nil if prefix.empty? || path.size < prefix.size
|
25
|
+
prefix.size.times do |idx|
|
26
|
+
return nil unless prefix[idx] == path[idx]
|
27
|
+
end
|
28
|
+
path[prefix.size...path.size]
|
29
|
+
end
|
30
|
+
|
31
|
+
def remove(s, path)
|
32
|
+
remove_complete_prefix(s, path) || path
|
33
|
+
end
|
34
|
+
|
35
|
+
def replace(o, s, path)
|
36
|
+
p = remove_complete_prefix(o, path)
|
37
|
+
p.nil? ? path : (s + p)
|
38
|
+
end
|
39
|
+
|
40
|
+
def add_op(s)
|
41
|
+
s = path2pieces(s)
|
42
|
+
Proc.new { |path| add(s, path) }
|
43
|
+
end
|
44
|
+
|
45
|
+
def remove_op(s)
|
46
|
+
s = path2pieces(s)
|
47
|
+
Proc.new { |path| remove(s, path) }
|
48
|
+
end
|
49
|
+
|
50
|
+
def replace_op(orig, s)
|
51
|
+
o = path2pieces(orig)
|
52
|
+
s = path2pieces(s)
|
53
|
+
Proc.new { |path| replace(o, s, path) }
|
54
|
+
end
|
55
|
+
|
56
|
+
def perform_operations(paths, operations)
|
57
|
+
out = {}
|
58
|
+
paths.each do |path, value|
|
59
|
+
operations.each do |op|
|
60
|
+
path = pieces2path(op.call(path2pieces(path)))
|
61
|
+
end
|
62
|
+
out[path] = value
|
63
|
+
end
|
64
|
+
out
|
65
|
+
end
|
66
|
+
|
67
|
+
def main
|
68
|
+
input_name = nil
|
69
|
+
output_name = nil
|
70
|
+
operations = []
|
71
|
+
orig = nil
|
72
|
+
|
73
|
+
parser = OptionParser.new do |opts|
|
74
|
+
opts.summary_indent = ' '
|
75
|
+
opts.summary_width = 26
|
76
|
+
opts.banner = 'Usage: openapi-modifypaths [options]'
|
77
|
+
opts.separator ''
|
78
|
+
opts.separator 'Options:'
|
79
|
+
opts.on('-i', '--input FILE', 'Read API spec from FILE, not stdin.') do |f|
|
80
|
+
exit(aargh("Expected string to replace PREFIX.", 1)) unless orig.nil?
|
81
|
+
input_name = f
|
82
|
+
end
|
83
|
+
opts.on('-o', '--output FILE', 'Output to FILE, not stdout.') do |f|
|
84
|
+
exit(aargh("Expected string to replace PREFIX.", 1)) unless orig.nil?
|
85
|
+
output_name = f
|
86
|
+
end
|
87
|
+
opts.on('-a', '--add STR', 'Add prefix STR to all paths.') do |s|
|
88
|
+
exit(aargh("Expected string to replace PREFIX.", 1)) unless orig.nil?
|
89
|
+
operations.push(add_op(s))
|
90
|
+
end
|
91
|
+
opts.on('-d', '--delete PREFIX', 'Delete PREFIX when present.') do |s|
|
92
|
+
exit(aargh("Expected string to replace PREFIX.", 1)) unless orig.nil?
|
93
|
+
operations.push(remove_op(s))
|
94
|
+
end
|
95
|
+
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
|
+
orig = s
|
98
|
+
end
|
99
|
+
opts.on('-h', '--help', 'Print this help and exit.') do
|
100
|
+
$stdout.puts %(#{opts}
|
101
|
+
|
102
|
+
Loads API document in OpenAPI format and changes paths according to options.
|
103
|
+
STR and PREFIX are expected to be parts of a path surrounded by /.
|
104
|
+
)
|
105
|
+
exit 0
|
106
|
+
end
|
107
|
+
end
|
108
|
+
parser.order! do |s|
|
109
|
+
exit(aargh("String without option: #{s}", 1)) if orig.nil?
|
110
|
+
operations.push(replace_op(orig, s))
|
111
|
+
orig = nil
|
112
|
+
end
|
113
|
+
|
114
|
+
doc = load_source(input_name)
|
115
|
+
return 2 if doc.nil?
|
116
|
+
|
117
|
+
p = perform_operations(doc.fetch('paths', {}), operations)
|
118
|
+
doc['paths'] = p unless p.empty?
|
119
|
+
|
120
|
+
dump_result(output_name, doc, 3)
|
121
|
+
end
|
122
|
+
|
123
|
+
exit(main) if defined?($unit_test).nil?
|
data/bin/openapi-processpaths
CHANGED
@@ -38,7 +38,7 @@ later stage tools. Checks for paths that may be ambiguous.
|
|
38
38
|
exit 0
|
39
39
|
end
|
40
40
|
end
|
41
|
-
parser.
|
41
|
+
parser.order!
|
42
42
|
|
43
43
|
doc = load_source(input_name)
|
44
44
|
return 2 if doc.nil?
|
@@ -81,4 +81,4 @@ later stage tools. Checks for paths that may be ambiguous.
|
|
81
81
|
dump_result(output_name, YAML.dump(doc), 3)
|
82
82
|
end
|
83
83
|
|
84
|
-
exit(main) if
|
84
|
+
exit(main) if defined?($unit_test).nil?
|
data/lib/apiobjects.rb
CHANGED
@@ -33,15 +33,17 @@ def reference(obj, schemas, schema_path, ignored_keys = Set.new(%w[summary descr
|
|
33
33
|
end
|
34
34
|
|
35
35
|
class Components
|
36
|
-
attr_reader :path, :prefix
|
36
|
+
attr_reader :path, :prefix, :anchor2ref, :schema_names
|
37
37
|
attr_accessor :items, :ignored_keys
|
38
38
|
|
39
|
-
def initialize(path, prefix, ignored_keys = %w[summary description examples example])
|
40
|
-
@items = {}
|
39
|
+
def initialize(path, prefix, ignored_keys = %w[summary description examples example $anchor])
|
41
40
|
path = "#/#{path.join('/')}/" if path.is_a?(Array)
|
42
41
|
path = "#{path}/" unless path.end_with?('/')
|
43
42
|
@path = path
|
44
43
|
@prefix = prefix
|
44
|
+
@anchor2ref = {}
|
45
|
+
@schema_names = Set.new
|
46
|
+
@items = {}
|
45
47
|
@ignored_keys = Set.new(ignored_keys)
|
46
48
|
end
|
47
49
|
|
@@ -58,7 +60,12 @@ class Components
|
|
58
60
|
%(All fields are used in object equality comparisons except:\n#{@ignored_keys.to_a.sort!.join("\n")})
|
59
61
|
end
|
60
62
|
|
63
|
+
def add_schema_name(name)
|
64
|
+
@schema_names.add(name)
|
65
|
+
end
|
66
|
+
|
61
67
|
def ref_string(name)
|
68
|
+
return nil if name.nil?
|
62
69
|
"#{@path}#{name}"
|
63
70
|
end
|
64
71
|
|
@@ -73,42 +80,39 @@ class Components
|
|
73
80
|
cand = "#{@prefix}#{n}x"
|
74
81
|
next if @items.key?(cand)
|
75
82
|
@items[cand] = obj.merge
|
83
|
+
@schema_names.add(cand)
|
76
84
|
return ref_string(cand)
|
77
85
|
end
|
78
86
|
end
|
79
|
-
end
|
80
|
-
|
81
|
-
|
82
|
-
class PathOperation
|
83
|
-
attr_accessor :path, :operation, :info, :parameters
|
84
|
-
attr_accessor :servers, :security, :tags
|
85
|
-
attr_accessor :summary, :description
|
86
|
-
end
|
87
87
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
88
|
+
def store_anchor(obj, ref = nil)
|
89
|
+
anchor_name = obj['$anchor']
|
90
|
+
return if anchor_name.nil?
|
91
|
+
ref = obj['$ref'] if ref.nil?
|
92
|
+
raise Exception, 'ref is nil and no $ref or it is nil' if ref.nil?
|
93
|
+
@anchor2ref[anchor_name] = ref
|
94
|
+
end
|
95
95
|
|
96
|
-
def
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
96
|
+
def alter_anchors
|
97
|
+
replacements = {}
|
98
|
+
@anchor2ref.each do |a, r|
|
99
|
+
next if @schema_names.member?(a)
|
100
|
+
replacements[a] = ref_string(a)
|
101
|
+
@schema_names.add(a)
|
102
|
+
end
|
103
|
+
replacements.each do |a, r|
|
104
|
+
@anchor2ref[a] = r
|
105
|
+
end
|
106
|
+
end
|
107
107
|
|
108
|
+
def anchor_ref_replacement(ref)
|
109
|
+
@anchor2ref[ref[1...ref.size]] || ref
|
110
|
+
end
|
108
111
|
end
|
109
112
|
|
110
|
-
|
111
113
|
class ServerPath
|
114
|
+
# Probably moves to a separate file once processpaths and frequencies receive
|
115
|
+
# some attention.
|
112
116
|
include Comparable
|
113
117
|
|
114
118
|
attr_accessor :parts
|
@@ -161,6 +165,8 @@ class ServerPath
|
|
161
165
|
end
|
162
166
|
end
|
163
167
|
|
168
|
+
# The rest probably ends up in a gem that orders schemas and does nothing else.
|
169
|
+
|
164
170
|
# Adds all refs found in the array to refs with given required state.
|
165
171
|
def gather_array_refs(refs, items, required)
|
166
172
|
items.each do |s|
|
@@ -298,36 +304,3 @@ class SchemaOrderer
|
|
298
304
|
chosen
|
299
305
|
end
|
300
306
|
end
|
301
|
-
|
302
|
-
class HeaderOrderer
|
303
|
-
end
|
304
|
-
|
305
|
-
class ResponseOrderer
|
306
|
-
end
|
307
|
-
|
308
|
-
class PathOrderer
|
309
|
-
end
|
310
|
-
|
311
|
-
class Tasker
|
312
|
-
attr_reader :doc
|
313
|
-
def initialize(doc)
|
314
|
-
@doc = doc
|
315
|
-
end
|
316
|
-
end
|
317
|
-
|
318
|
-
class SchemaTasker < Tasker
|
319
|
-
# initialize with doc
|
320
|
-
# Orderer set somehow.
|
321
|
-
# Method ot create one task per schema.
|
322
|
-
# Method to create one task for all schemas.
|
323
|
-
end
|
324
|
-
|
325
|
-
class HeaderTasker < Tasker
|
326
|
-
# initialize with doc
|
327
|
-
# Orderer set somehow.
|
328
|
-
# Method ot create one task per schema.
|
329
|
-
# Method to create one task for all schemas.
|
330
|
-
end
|
331
|
-
|
332
|
-
|
333
|
-
|
data/lib/common.rb
CHANGED
@@ -12,6 +12,10 @@ def aargh(message, return_value = nil)
|
|
12
12
|
return_value
|
13
13
|
end
|
14
14
|
|
15
|
+
def yesno(boolean)
|
16
|
+
boolean ? 'yes' : 'no'
|
17
|
+
end
|
18
|
+
|
15
19
|
def bury(doc, path, value)
|
16
20
|
(path.size - 1).times do |k|
|
17
21
|
p = path[k]
|
@@ -54,6 +58,7 @@ rescue StandardError => e
|
|
54
58
|
end
|
55
59
|
|
56
60
|
def dump_result(output, doc, error_return)
|
61
|
+
doc = YAML.dump(doc, line_width: 1_000_000) unless doc.is_a?(String)
|
57
62
|
if output.nil?
|
58
63
|
$stdout.puts doc
|
59
64
|
else
|
data/lib/docs.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright © 2024 Ismo Kärkkäinen
|
4
|
+
# Licensed under Universal Permissive License. See LICENSE.txt.
|
5
|
+
|
6
|
+
require_relative 'common'
|
7
|
+
|
8
|
+
|
9
|
+
class Docs
|
10
|
+
attr_reader :docs
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@docs = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def method_missing(method_name, *args)
|
17
|
+
name = method_name.to_s
|
18
|
+
if name.end_with?('=')
|
19
|
+
name = name[0...(name.size - 1)]
|
20
|
+
super unless @docs.key?(name)
|
21
|
+
@docs[name] = args.first
|
22
|
+
return args.first
|
23
|
+
end
|
24
|
+
super unless @docs.key?(name)
|
25
|
+
@docs[name]
|
26
|
+
end
|
27
|
+
|
28
|
+
def add(name, content)
|
29
|
+
return false if docs.key?(name)
|
30
|
+
@docs[name] = content
|
31
|
+
true
|
32
|
+
end
|
33
|
+
end
|
data/lib/gen.rb
CHANGED
@@ -5,56 +5,73 @@
|
|
5
5
|
|
6
6
|
require_relative 'task'
|
7
7
|
require_relative 'helper'
|
8
|
+
require_relative 'docs'
|
9
|
+
require_relative 'output'
|
8
10
|
|
9
11
|
|
10
12
|
module Gen
|
11
|
-
def self.
|
12
|
-
attr_reader(symbol)
|
13
|
-
module_function(symbol)
|
13
|
+
def self.add_doc(symbol, docstr)
|
14
14
|
return if docstr.nil?
|
15
|
-
@docsrc = [] unless
|
15
|
+
@docsrc = [] unless instance_variable_defined?('@docsrc')
|
16
16
|
@docsrc.push("- #{symbol.to_s} : #{docstr}")
|
17
17
|
end
|
18
18
|
|
19
|
-
def self.
|
19
|
+
def self.read_attr(symbol, default)
|
20
|
+
return if symbol.nil?
|
21
|
+
attr_reader(symbol)
|
22
|
+
module_function(symbol)
|
23
|
+
instance_variable_set("@#{symbol.to_s}", default)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.mod_attr2_reader(symbol, symbol2, docstr = nil, default = nil)
|
27
|
+
read_attr(symbol, default)
|
28
|
+
read_attr(symbol2, default)
|
29
|
+
add_doc(symbol, docstr)
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.mod_attr_reader(symbol, docstr = nil, default = nil)
|
33
|
+
mod_attr2_reader(symbol, nil, docstr, default)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.rw_attr(symbol, default)
|
20
37
|
attr_accessor(symbol)
|
21
38
|
module_function(symbol)
|
22
39
|
s = symbol.to_s
|
23
40
|
module_function((s + '=').to_sym)
|
24
|
-
|
25
|
-
|
26
|
-
|
41
|
+
instance_variable_set("@#{s}", default)
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.mod_attr2_accessor(symbol, symbol2, docstr = nil, default = nil)
|
45
|
+
rw_attr(symbol, default)
|
46
|
+
rw_attr(symbol2, default) unless symbol2.nil?
|
47
|
+
add_doc(symbol, docstr)
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.mod_attr_accessor(symbol, docstr = nil, default = nil)
|
51
|
+
mod_attr2_accessor(symbol, nil, docstr, default)
|
27
52
|
end
|
28
53
|
|
29
54
|
mod_attr_reader :doc, 'OpenAPI document.'
|
30
55
|
mod_attr_reader :outdir, 'Output directory name.'
|
56
|
+
mod_attr_reader :d, 'Other documents object.', Docs.new
|
31
57
|
mod_attr_accessor :in_name, 'OpenAPI document name, nil if stdin.'
|
32
58
|
mod_attr_accessor :in_basename, 'OpenAPI document basename, nil if stdin.'
|
33
|
-
mod_attr_accessor :tasks, 'Tasks array.'
|
34
|
-
mod_attr_accessor :g, 'Hash for storing values visible to all tasks.'
|
59
|
+
mod_attr_accessor :tasks, 'Tasks array.', []
|
60
|
+
mod_attr_accessor :g, 'Hash for storing values visible to all tasks.', {}
|
35
61
|
mod_attr_accessor :a, 'Intended for instance with defined attributes.'
|
36
62
|
mod_attr_accessor :h, 'Instance of class with helper methods.'
|
37
|
-
|
63
|
+
mod_attr2_accessor :task, :t, 'Current task instance.'
|
38
64
|
mod_attr_accessor :task_index, 'Current task index.'
|
39
|
-
mod_attr_accessor :loaders, 'Array of generator loader methods.'
|
65
|
+
mod_attr_accessor :loaders, 'Array of generator loader methods.', []
|
66
|
+
mod_attr2_accessor :output, :o, 'Output-related methods.', Output.new
|
40
67
|
|
41
68
|
def self.setup(document_content, input_name, output_directory)
|
42
69
|
@doc = document_content
|
43
70
|
@outdir = output_directory
|
44
|
-
|
45
|
-
@in_name = nil
|
46
|
-
@in_basename = nil
|
47
|
-
else
|
71
|
+
unless input_name.nil?
|
48
72
|
@in_name = File.basename(input_name)
|
49
73
|
@in_basename = File.basename(input_name, '.*')
|
50
74
|
end
|
51
|
-
@tasks = []
|
52
|
-
@g = {}
|
53
|
-
@a = nil
|
54
|
-
@h = nil
|
55
|
-
@t = nil
|
56
|
-
@task_index = nil
|
57
|
-
@loaders = []
|
58
75
|
add_task(task: HelperTask.new)
|
59
76
|
end
|
60
77
|
|
data/lib/generate.rb
CHANGED
data/lib/helper.rb
CHANGED
@@ -10,7 +10,7 @@ class Helper
|
|
10
10
|
attr_reader :doc, :parents
|
11
11
|
attr_accessor :parent_parameters
|
12
12
|
|
13
|
-
# Stores the
|
13
|
+
# Stores the nearest Hash for each Hash.
|
14
14
|
def store_parents(obj, parent = nil)
|
15
15
|
if obj.is_a?(Hash)
|
16
16
|
@parents[obj.object_id] = parent
|
@@ -26,8 +26,6 @@ class Helper
|
|
26
26
|
|
27
27
|
def initialize(doc)
|
28
28
|
@doc = doc
|
29
|
-
# For each hash in doc, set parent?
|
30
|
-
# Build an object_id to parent object mapping and use parent method?
|
31
29
|
@parents = {}
|
32
30
|
store_parents(@doc)
|
33
31
|
end
|
data/lib/loaders.rb
CHANGED
@@ -3,6 +3,9 @@
|
|
3
3
|
# Copyright © 2024 Ismo Kärkkäinen
|
4
4
|
# Licensed under Universal Permissive License. See LICENSE.txt.
|
5
5
|
|
6
|
+
|
7
|
+
# Original loader functions. These are accessible via Gen.loaders. New loaders
|
8
|
+
# should be added there.
|
6
9
|
module Loaders
|
7
10
|
|
8
11
|
GEM_PREFIX = 'gem:'
|
@@ -37,14 +40,56 @@ module Loaders
|
|
37
40
|
true
|
38
41
|
end
|
39
42
|
|
43
|
+
YAML_PREFIX = 'yaml:'
|
44
|
+
YAML_EXTS = [ '.yaml', '.yml' ]
|
45
|
+
|
46
|
+
def self.yaml_loader(name)
|
47
|
+
d = name.downcase
|
48
|
+
if d.start_with?(YAML_PREFIX)
|
49
|
+
name = name.slice(YAML_PREFIX.size...name.size)
|
50
|
+
else
|
51
|
+
return false if (YAML_EXTS.index { |s| d.end_with?(s) }).nil?
|
52
|
+
end
|
53
|
+
n, sep, f = name.partition(':')
|
54
|
+
raise StandardError, "No name given." if n.empty?
|
55
|
+
raise StandardError, "No filename given." if f.empty?
|
56
|
+
doc = YAML.safe_load(File.read(f))
|
57
|
+
raise StandardError, "#{name} #{n} exists already." unless Gen.d.add(n, doc)
|
58
|
+
true
|
59
|
+
rescue Errno::ENOENT
|
60
|
+
raise StandardError, "Not found: #{f}\n#{e.to_s}"
|
61
|
+
rescue Exception => e
|
62
|
+
raise StandardError, "Failed to read as YAML: #{f}\n#{e.to_s}"
|
63
|
+
end
|
64
|
+
|
65
|
+
BIN_PREFIX = 'bin:'
|
66
|
+
|
67
|
+
def self.bin_loader(name)
|
68
|
+
return false unless name.downcase.start_with?(BIN_PREFIX)
|
69
|
+
n, sep, f = name.slice(BIN_PREFIX.size...name.size).partition(':')
|
70
|
+
raise StandardError, "No name given." if n.empty?
|
71
|
+
raise StandardError, "No filename given." if f.empty?
|
72
|
+
doc = IO.binread(f)
|
73
|
+
raise StandardError, "#{name} #{n} exists already." unless Gen.d.add(n, doc)
|
74
|
+
true
|
75
|
+
rescue Errno::ENOENT
|
76
|
+
raise StandardError, "Not found: #{f}\n#{e.to_s}"
|
77
|
+
rescue Exception => e
|
78
|
+
raise StandardError, "Failed to read #{f}\n#{e.to_s}"
|
79
|
+
end
|
80
|
+
|
40
81
|
def self.loaders
|
41
|
-
|
82
|
+
pre = @preloaders
|
83
|
+
[ method(:gem_loader), method(:ruby_loader), method(:yaml_loader), method(:bin_loader) ]
|
42
84
|
end
|
43
85
|
|
44
86
|
def self.document
|
45
87
|
%(
|
46
88
|
- #{Loaders::GEM_PREFIX}gem_name : requires the gem.
|
47
89
|
- ruby_file#{Loaders::RUBY_EXT} : changes to Ruby file directory and requires the file.
|
90
|
+
- #{Loaders::YAML_PREFIX}name:filename : Loads YAML file into Gen.d.name.
|
91
|
+
- name:filename.{#{(Loaders::YAML_EXTS.map { |s| s[1...s.size] }).join('|')}} : Loads YAML file into Gen.d.name.
|
92
|
+
- #{Loaders::BIN_PREFIX}name:filename : Loads binary file into Gen.d.name.
|
48
93
|
)
|
49
94
|
end
|
50
95
|
|
data/lib/output.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright © 2024 Ismo Kärkkäinen
|
4
|
+
# Licensed under Universal Permissive License. See LICENSE.txt.
|
5
|
+
|
6
|
+
require_relative 'task'
|
7
|
+
|
8
|
+
|
9
|
+
class Output
|
10
|
+
# For indentation.
|
11
|
+
attr_accessor :indent_character, :indent_step
|
12
|
+
attr_accessor :tab, :tab_replaces_count
|
13
|
+
attr_accessor :last_indent
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@indent_character = ' '
|
17
|
+
@indent_step = 4
|
18
|
+
@tab = "\t"
|
19
|
+
@tab_replaces_count = 0
|
20
|
+
@last_indent = 0
|
21
|
+
end
|
22
|
+
|
23
|
+
def join(blocks, separator = "\n")
|
24
|
+
indented = []
|
25
|
+
blocks.flatten!
|
26
|
+
indent = 0
|
27
|
+
blocks.each do |block|
|
28
|
+
if block.nil?
|
29
|
+
indent = 0
|
30
|
+
elsif block.is_a?(Integer)
|
31
|
+
indent += block
|
32
|
+
elsif block.is_a?(TrueClass)
|
33
|
+
indent += @indent_step
|
34
|
+
elsif block.is_a?(FalseClass)
|
35
|
+
indent -= @indent_step
|
36
|
+
else
|
37
|
+
block = block.to_s unless block.is_a?(String)
|
38
|
+
if indent.zero?
|
39
|
+
indented.push(block)
|
40
|
+
next
|
41
|
+
end
|
42
|
+
if 0 < @tab_replaces_count
|
43
|
+
tabs = @tab * (indent / @tab_replaces_count)
|
44
|
+
chars = @indent_character * (indent % @tab_replaces_count)
|
45
|
+
else
|
46
|
+
tabs = ''
|
47
|
+
chars = @indent_character * indent
|
48
|
+
end
|
49
|
+
lines = block.lines(chomp: true)
|
50
|
+
lines.each do |line|
|
51
|
+
indented.push("#{tabs}#{chars}#{line}")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
@last_indent = indent
|
56
|
+
indented.join(separator)
|
57
|
+
end
|
58
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: openapi-sourcetools
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ismo Kärkkäinen
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-08-28 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: |2
|
14
14
|
|
@@ -25,6 +25,7 @@ executables:
|
|
25
25
|
- openapi-frequencies
|
26
26
|
- openapi-generate
|
27
27
|
- openapi-merge
|
28
|
+
- openapi-modifypaths
|
28
29
|
- openapi-processpaths
|
29
30
|
extensions: []
|
30
31
|
extra_rdoc_files: []
|
@@ -38,13 +39,16 @@ files:
|
|
38
39
|
- bin/openapi-frequencies
|
39
40
|
- bin/openapi-generate
|
40
41
|
- bin/openapi-merge
|
42
|
+
- bin/openapi-modifypaths
|
41
43
|
- bin/openapi-processpaths
|
42
44
|
- lib/apiobjects.rb
|
43
45
|
- lib/common.rb
|
46
|
+
- lib/docs.rb
|
44
47
|
- lib/gen.rb
|
45
48
|
- lib/generate.rb
|
46
49
|
- lib/helper.rb
|
47
50
|
- lib/loaders.rb
|
51
|
+
- lib/output.rb
|
48
52
|
- lib/task.rb
|
49
53
|
homepage: https://xn--ismo-krkkinen-gfbd.fi/openapi-sourcetools/index.html
|
50
54
|
licenses:
|