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.
data/lib/apiobjects.rb DELETED
@@ -1,333 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative './common'
4
- require 'set'
5
-
6
- def same(a, b, ignored_keys = Set.new(%w[summary description]))
7
- return a == b unless a.is_a?(Hash) && b.is_a?(Hash)
8
- keys = Set.new(a.keys + b.keys) - ignored_keys
9
- keys.to_a.each do |k|
10
- return false unless a.key?(k) && b.key?(k)
11
- return false unless same(a[k], b[k], ignored_keys)
12
- end
13
- true
14
- end
15
-
16
- def ref_string(name, schema_path)
17
- "#{schema_path}/#{name}"
18
- end
19
-
20
- def reference(obj, schemas, schema_path, ignored_keys = Set.new(%w[summary description]), prefix = 'Schema')
21
- # Check if identical schema has been added and if so, return the $ref string.
22
- schemas.keys.sort.each do |k|
23
- return ref_string(k, schema_path) if same(obj, schemas[k], ignored_keys)
24
- end
25
- # One of the numbers will not match existing keys. More number than keys.
26
- (schemas.size + 1).times do |n|
27
- # 'x' is to simplify find and replace (Schema1x vs Schema1 and Schema10)
28
- k = "#{prefix}#{n}x"
29
- next if schemas.key?(k)
30
- schemas[k] = obj.merge
31
- return ref_string(k, schema_path)
32
- end
33
- end
34
-
35
- class Components
36
- attr_reader :path, :prefix
37
- attr_accessor :items, :ignored_keys
38
-
39
- def initialize(path, prefix, ignored_keys = %w[summary description examples example])
40
- @items = {}
41
- path = "#/#{path.join('/')}/" if path.is_a?(Array)
42
- path = "#{path}/" unless path.end_with?('/')
43
- @path = path
44
- @prefix = prefix
45
- @ignored_keys = Set.new(ignored_keys)
46
- end
47
-
48
- def add_options(opts)
49
- opts.on('--use FIELD', 'Use FIELD in comparisons.') do |f|
50
- @ignored_keys.delete(f)
51
- end
52
- opts.on('--ignore FIELD', 'Ignore FIELD in comparisons.') do |f|
53
- @ignored_keys.add(f)
54
- end
55
- end
56
-
57
- def help
58
- %(All fields are used in object equality comparisons except:\n#{@ignored_keys.to_a.sort!.join("\n")})
59
- end
60
-
61
- def ref_string(name)
62
- "#{@path}#{name}"
63
- end
64
-
65
- def reference(obj)
66
- # Check if identical schema has been added. If so, return the $ref string.
67
- @items.each do |k, v|
68
- return ref_string(k) if same(obj, v, @ignored_keys)
69
- end
70
- # One of the numbers will not match existing keys. More number than keys.
71
- (@items.size + 1).times do |n|
72
- # 'x' is to simplify find and replace (Schema1x vs Schema1 and Schema10)
73
- cand = "#{@prefix}#{n}x"
74
- next if @items.key?(cand)
75
- @items[cand] = obj.merge
76
- return ref_string(cand)
77
- end
78
- 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
-
88
- # One could have a convenience
89
- # method that determines how many bytes the value needs, and if it needs to be
90
- # signed.
91
-
92
- # When creating types for schemas or otherwise, the type name can be added
93
- # into the item and that way be used as an indicator that the type has been
94
- # declared or needs a declaration.
95
-
96
- def make_path_operations(apidoc)
97
- # Check openapi
98
- # Store info as is for reference
99
- # Store servers as is for default value for PathOperation
100
- # Process components. Lazy manner, only when referenced.
101
- # Store security as is for default value for PathOperation.
102
- # Store tags as mapping from name to object for use with PathOperation.
103
- # Process paths:
104
- # Store parameters as is for default value for PathOperation.
105
- # All other fields, check if it looks like OperationObject and create a
106
- # PathOperation using it. For others, store as is for default value.
107
-
108
- end
109
-
110
-
111
- class ServerPath
112
- include Comparable
113
-
114
- attr_accessor :parts
115
-
116
- def initialize(parts)
117
- @parts = parts
118
- end
119
-
120
- def <=>(other) # Variables are after fixed strings.
121
- pp = other.is_a?(Array) ? other : p.parts
122
- parts.each_index do |k|
123
- return 1 if pp.size <= k # Longer comes after shorter.
124
- pk = parts[k]
125
- ppk = pp[k]
126
- if pk.is_a? String
127
- if ppk.is_a? String
128
- c = pk <=> ppk
129
- else
130
- return -1
131
- end
132
- else
133
- if ppk.is_a? String
134
- return 1
135
- else
136
- c = pk.fetch('var', '') <=> ppk.fetch('var', '')
137
- end
138
- end
139
- return c unless c.zero?
140
- end
141
- (parts.size < pp.size) ? -1 : 0
142
- end
143
-
144
- def compare(p, range = nil) # Not fit for sorting. Variable equals anything.
145
- pp = p.is_a?(Array) ? p : p.parts
146
- if range.nil?
147
- range = 0...parts.size
148
- elsif range.is_a? Number
149
- range = range...(range + 1)
150
- end
151
- range.each do |k|
152
- return 1 if pp.size <= k # Longer comes after shorter.
153
- ppk = pp[k]
154
- next unless ppk.is_a? String
155
- pk = parts[k]
156
- next unless pk.is_a? String
157
- c = pk <=> ppk
158
- return c unless c.zero?
159
- end
160
- (parts.size < pp.size) ? -1 : 0
161
- end
162
- end
163
-
164
- # Adds all refs found in the array to refs with given required state.
165
- def gather_array_refs(refs, items, required)
166
- items.each do |s|
167
- r = s['$ref']
168
- next if r.nil?
169
- refs[r] = required || refs.fetch(r, false)
170
- end
171
- end
172
-
173
- # For any key '$ref' adds to refs whether referred type is required.
174
- # Requires that there are no in-lined schemas, openapi-addschemas has been run.
175
- def gather_refs(refs, schema)
176
- # This implies types mixed together according to examples. Needs mixed type.
177
- # AND. Also, mixing may fail. Adds a new schema, do here.
178
- items = schema['allOf']
179
- return gather_array_refs(refs, items, true) unless items.nil?
180
- # As long as one schema is fulfilled, it is ok. OR, first that fits.
181
- items = schema['anyOf'] if items.nil?
182
- # oneOf implies selection between different types. No multiple matches. XOR.
183
- # Needs to ensure that later types do not match.
184
- # Should check if there is enough difference to ensure single match.
185
- # Use separate program run after addschemas to create allOf mixed schema
186
- # and verify the others can be dealt with.
187
- items = schema['oneOf'] if items.nil?
188
- return gather_array_refs(refs, items, false) unless items.nil?
189
- # Defaults below handle it if "type" is not "object".
190
- reqs = schema.fetch('required', [])
191
- schema.fetch('properties', {}).each do |name, spec|
192
- r = spec['$ref']
193
- next if r.nil?
194
- refs[r] = reqs.include?(name) || refs.fetch(r, false)
195
- end
196
- end
197
-
198
- class SchemaInfo
199
- attr_accessor :ref, :schema, :direct_refs, :name, :post_refs
200
-
201
- def initialize(ref, name, schema)
202
- @ref = ref
203
- @name = name
204
- @schema = schema
205
- @direct_refs = {}
206
- gather_refs(@direct_refs, schema)
207
- end
208
-
209
- def set_post_refs(seen)
210
- @post_refs = Set.new(@direct_refs.keys) - seen
211
- end
212
-
213
- def to_s
214
- v = @direct_refs.keys.sort.map { |k| "#{k}:#{@direct_refs[k] ? 'req' : 'opt'}" }
215
- "#{@ref}: #{v.join(' ')}"
216
- end
217
- end
218
-
219
- def var_or_method_value(x, name)
220
- if name.start_with?('@')
221
- n = name
222
- else
223
- n = "@#{name}"
224
- end
225
- return x.instance_variable_get(n) if x.instance_variable_defined?(n)
226
- return x.public_send(name) if x.respond_to?(name)
227
- raise ArgumentError, "#{name} is not #{x.class} instance variable nor public method"
228
- end
229
-
230
- class SchemaOrderer
231
- attr_accessor :schemas, :order, :orderer
232
-
233
- def initialize(path, schema_specs)
234
- @schemas = {}
235
- schema_specs.each do |name, schema|
236
- r = "#{path}#{name}"
237
- @schemas[r] = SchemaInfo.new(r, name, schema)
238
- end
239
- end
240
-
241
- def sort!(orderer = 'required_first')
242
- case orderer
243
- when 'required_first' then @order = required_first
244
- when '<=>' then @order = @schemas.values.sort { |a, b| a <=> b }
245
- else
246
- @order = @schemas.values.sort do |a, b|
247
- va = var_or_method_value(a, orderer)
248
- vb = var_or_method_value(b, orderer)
249
- va <=> vb
250
- end
251
- end
252
- @orderer = orderer
253
- seen = Set.new
254
- @order.each do |si|
255
- si.set_post_refs(seen)
256
- seen.add(si.ref)
257
- end
258
- @order
259
- end
260
-
261
- def required_first
262
- chosen = []
263
- until chosen.size == @schemas.size
264
- used = Set.new(chosen.map { |si| si.ref })
265
- avail = @schemas.values.select { |si| !used.member?(si.ref) }
266
- best = nil
267
- avail.each do |si|
268
- prereq = chosen.count { |x| x.direct_refs.fetch(si.ref, false) }
269
- fulfilled = chosen.count { |x| si.direct_refs.fetch(x.ref, false) }
270
- postreq = si.direct_refs.size - (prereq + fulfilled)
271
- better = false
272
- if best.nil?
273
- better = true
274
- else
275
- # Minimize preceding types requiring this.
276
- if prereq < best.first
277
- better = true
278
- elsif prereq == best.first
279
- # Minimize remaining unfulfilled requires.
280
- if postreq < best[1]
281
- better = true
282
- elsif postreq == best[1]
283
- # Check mutual direct requirements.
284
- best_req_si = best.last.direct_refs.fetch(si.ref, false)
285
- si_req_best = si.direct_refs.fetch(best.last.ref, false)
286
- if best_req_si
287
- better = true unless si_req_best
288
- end
289
- # Order by name if no other difference.
290
- better = si.ref < best.last.ref unless better
291
- end
292
- end
293
- end
294
- best = [ prereq, postreq, si ] if better
295
- end
296
- chosen.push(best.last)
297
- end
298
- chosen
299
- end
300
- 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 DELETED
@@ -1,110 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Copyright © 2021-2024 Ismo Kärkkäinen
4
- # Licensed under Universal Permissive License. See LICENSE.txt.
5
-
6
- require 'pathname'
7
-
8
-
9
- def aargh(message, return_value = nil)
10
- message = message.map(&:to_s).join("\n") if message.is_a? Array
11
- $stderr.puts message
12
- return_value
13
- end
14
-
15
- def bury(doc, path, value)
16
- (path.size - 1).times do |k|
17
- p = path[k]
18
- doc[p] = {} unless doc.key?(p)
19
- doc = doc[p]
20
- end
21
- doc[path.last] = value
22
- end
23
-
24
- module Out
25
- attr_reader :count
26
-
27
- def put(message)
28
- aargh(message)
29
- count += 1
30
- end
31
- end
32
-
33
- def split_path(p, spec = false)
34
- parts = []
35
- p = p.strip
36
- unless spec
37
- q = p.index('?')
38
- p.slice!(0...q) unless q.nil?
39
- end
40
- p.split('/').each do |s|
41
- next if s.empty?
42
- s = { var: s } if spec && s.include?('{')
43
- parts.push(s)
44
- end
45
- parts
46
- end
47
-
48
- def load_source(input)
49
- YAML.safe_load(input.nil? ? $stdin : File.read(input))
50
- rescue Errno::ENOENT
51
- aargh "Could not load #{input || 'stdin'}"
52
- rescue StandardError => e
53
- aargh "#{e}\nFailed to read #{input || 'stdin'}"
54
- end
55
-
56
- def dump_result(output, doc, error_return)
57
- doc = YAML.dump(doc, line_width: 1_000_000) unless doc.is_a?(String)
58
- if output.nil?
59
- $stdout.puts doc
60
- else
61
- fp = Pathname.new output
62
- fp.open('w') do |f|
63
- f.puts doc
64
- end
65
- end
66
- 0
67
- rescue StandardError => e
68
- aargh([ e, "Failed to write output: #{output || 'stdout'}" ], error_return)
69
- end
70
-
71
- ServerPath = Struct.new(:parts) do
72
- # Variables are after fixed strings.
73
- def <=>(other)
74
- pp = other.is_a?(Array) ? other : other.parts
75
- parts.each_index do |k|
76
- return 1 if pp.size <= k # Longer comes after shorter.
77
- pk = parts[k]
78
- ppk = pp[k]
79
- if pk.is_a? String
80
- return -1 unless ppk.is_a? String
81
- c = pk <=> ppk
82
- else
83
- return 1 if ppk.is_a? String
84
- c = pk.fetch('var', '') <=> ppk.fetch('var', '')
85
- end
86
- return c unless c.zero?
87
- end
88
- (parts.size < pp.size) ? -1 : 0
89
- end
90
-
91
- # Not fit for sorting. Variable equals anything.
92
- def compare(p, range = nil)
93
- pp = p.is_a?(Array) ? p : p.parts
94
- if range.nil?
95
- range = 0...parts.size
96
- elsif range.is_a? Number
97
- range = range...(range + 1)
98
- end
99
- range.each do |k|
100
- return 1 if pp.size <= k # Longer comes after shorter.
101
- ppk = pp[k]
102
- next unless ppk.is_a? String
103
- pk = parts[k]
104
- next unless pk.is_a? String
105
- c = pk <=> ppk
106
- return c unless c.zero?
107
- end
108
- (parts.size < pp.size) ? -1 : 0
109
- end
110
- end
data/lib/docs.rb DELETED
@@ -1,33 +0,0 @@
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/generate.rb DELETED
@@ -1,90 +0,0 @@
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 'common'
8
- require_relative 'loaders'
9
- require_relative 'gen'
10
-
11
-
12
- def executable_bits_on(mode)
13
- mode = mode.to_s(8).split('')
14
- mode.size.times do |k|
15
- m = mode[k].to_i(8)
16
- # Applies to Unix-likes. Other system, check and handle.
17
- m += 1 unless 3 < mode.size - k || m.zero? || m % 2 == 1
18
- mode[k] = m
19
- end
20
- m = 0
21
- mode.each do |v|
22
- m = 8 * m + v
23
- end
24
- m
25
- end
26
-
27
- class Generator
28
- def initialize(document_content, input_name, output_directory)
29
- Gen.setup(document_content, input_name, output_directory)
30
- Gen.loaders = Loaders.loaders
31
- end
32
-
33
- def context_binding
34
- binding
35
- end
36
-
37
- def load(generator_names)
38
- generator_names.each do |name|
39
- idx = Gen.loaders.index { |loader| loader.call(name) }
40
- return aargh("No loader could handle #{name}", 2) if idx.nil?
41
- end
42
- 0
43
- rescue StandardError => e
44
- aargh(e.to_s, 2)
45
- end
46
-
47
- def generate(t)
48
- t.generate(context_binding)
49
- rescue Exception => e
50
- aargh(e.to_s, 4)
51
- end
52
-
53
- def output_name(t, index)
54
- name = t.output_name
55
- name = "#{index}.txt" if name.nil?
56
- File.join(Gen.outdir, name)
57
- end
58
-
59
- def save(name, contents, executable)
60
- f = File.new(name, File::WRONLY | File::CREAT | File::TRUNC)
61
- s = executable ? f.stat : nil
62
- f.write(contents)
63
- f.close
64
- return unless executable
65
- mode = executable_bits_on(s.mode)
66
- File.chmod(mode, name) unless mode == s.mode
67
- end
68
-
69
- def run
70
- # This allows tasks to be added while processing.
71
- # Not intended to be done but might prove handy.
72
- # Also exposes current task index in case new task is added in the middle.
73
- Gen.task_index = 0
74
- while Gen.task_index < Gen.tasks.size
75
- Gen.t = Gen.tasks[Gen.task_index]
76
- out = generate(Gen.t)
77
- Gen.task_index += 1
78
- next if Gen.t.discard # Check first to ignore result if no output.
79
- return out if out.is_a?(Integer)
80
- next if out.empty? # Allows no output but return value still checked.
81
- name = output_name(Gen.t, Gen.task_index - 1)
82
- begin
83
- save(name, out, Gen.t.executable)
84
- rescue StandardError => e
85
- return aargh("Error writing output file: #{name}\n#{e}", 3)
86
- end
87
- end
88
- 0
89
- end
90
- end
data/lib/helper.rb DELETED
@@ -1,94 +0,0 @@
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 Helper
10
- attr_reader :doc, :parents
11
- attr_accessor :parent_parameters
12
-
13
- # Stores the nearest Hash for each Hash.
14
- def store_parents(obj, parent = nil)
15
- if obj.is_a?(Hash)
16
- @parents[obj.object_id] = parent
17
- obj.each do |k, v|
18
- store_parents(v, obj)
19
- end
20
- elsif obj.is_a?(Array)
21
- obj.each do |v|
22
- store_parents(v, parent)
23
- end
24
- end
25
- end
26
-
27
- def initialize(doc)
28
- @doc = doc
29
- @parents = {}
30
- store_parents(@doc)
31
- end
32
-
33
- def parent(object)
34
- @parents[object.object_id]
35
- end
36
-
37
- COMPONENTS = '#/components/'
38
-
39
- def category_and_name(ref_or_obj)
40
- ref = ref_or_obj.is_a?(Hash) ? ref_or_obj['$ref'] : ref_or_obj
41
- return nil unless ref.is_a?(String)
42
- return nil unless ref.start_with?(Helper::COMPONENTS)
43
- idx = ref.index('/', Helper::COMPONENTS.size)
44
- return nil if idx.nil?
45
- category = ref[Helper::COMPONENTS.size...idx]
46
- [ category, ref[(idx + 1)...ref.size] ]
47
- end
48
-
49
- def dereference(ref_or_obj)
50
- cn = category_and_name(ref_or_obj)
51
- return nil if cn.nil?
52
- cs = @doc.dig('components', cn.first) || {}
53
- cs[cn.last]
54
- end
55
-
56
- def basename(ref_or_obj)
57
- cn = category_and_name(ref_or_obj)
58
- return nil if cn.nil?
59
- cn.last
60
- end
61
-
62
- def parameters(operation_object, empty_unless_local = false)
63
- return [] if empty_unless_local && !operation_object.key?('parameters')
64
- cps = @doc.dig('components', 'parameters') || {}
65
- uniqs = {}
66
- path_item_object = parent(operation_object)
67
- [path_item_object, operation_object].each do |p|
68
- p.fetch('parameters', []).each do |param|
69
- r = basename(param)
70
- r = cps[r] if r.is_a?(String)
71
- uniqs["#{r['name']}:#{r['in']}"] = param
72
- end
73
- end
74
- uniqs.keys.sort!.map { |k| uniqs[k] }
75
- end
76
- end
77
-
78
-
79
- class HelperTask
80
- include TaskInterface
81
-
82
- def generate(context_binding)
83
- Gen.h = Helper.new(Gen.doc)
84
- end
85
-
86
- def output_name
87
- nil
88
- end
89
-
90
- def discard
91
- true
92
- end
93
- end
94
-