openapi-sourcetools 0.7.1 → 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.
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright © 2024-2025 Ismo Kärkkäinen
4
+ # Licensed under Universal Permissive License. See LICENSE.txt.
5
+
6
+ require_relative 'common'
7
+ require 'erb'
8
+
9
+
10
+ module OpenAPISourceTools
11
+
12
+ # Required interface for tasks, with default implementation for some methods.
13
+ module TaskInterface
14
+ def generate(context_binding)
15
+ raise NotImplementedError
16
+ end
17
+
18
+ def output_name
19
+ raise NotImplementedError
20
+ end
21
+
22
+ def discard
23
+ false
24
+ end
25
+
26
+ def executable
27
+ false
28
+ end
29
+
30
+ def system
31
+ false
32
+ end
33
+ end
34
+
35
+ # Loads template or takes it as argument. Renders template using ERB.
36
+ class Task
37
+ include TaskInterface
38
+
39
+ attr_reader :src, :template, :template_name
40
+ attr_accessor :name, :executable, :discard, :x
41
+
42
+ def initialize(src, template, template_name)
43
+ @src = src
44
+ @template = template
45
+ @template_name = template_name
46
+ if @template.nil?
47
+ raise ArgumentError, 'template_name or template must be given' if @template_name.nil?
48
+ begin
49
+ @template = File.read(@template_name)
50
+ rescue Errno::ENOENT
51
+ raise StandardError, "Could not load #{@template_name}"
52
+ rescue StandardError => e
53
+ raise StandardError, "#{e}\nFailed to read #{@template_name}"
54
+ end
55
+ end
56
+ @name = nil
57
+ @executable = false
58
+ @discard = false
59
+ @x = nil
60
+ end
61
+
62
+ # If this is overridden to perform some processing but not to produce output,
63
+ # set @discard = true and return value will be ignored. No other methods are
64
+ # called in that case.
65
+ def internal_generate(context_binding)
66
+ ERB.new(@template).result(context_binding)
67
+ end
68
+
69
+ # You can override this instead of internal_generate if you do not need the
70
+ # exception handling.
71
+ def generate(context_binding)
72
+ n = @template_name.nil? ? '' : "#{@template_name} "
73
+ internal_generate(context_binding)
74
+ rescue SyntaxError => e
75
+ OpenAPISourceTools::Common.aargh("Template #{n}syntax error: #{e.full_message}", 5)
76
+ rescue Exception => e # Some unexpected error.
77
+ OpenAPISourceTools::Common.aargh("Template #{n}error: #{e.full_message}", 6)
78
+ end
79
+
80
+ # This is only called when generate produced output that is not discarded.
81
+ def output_name
82
+ return @name unless @name.nil?
83
+ # Using template name may show where name assignment is missing.
84
+ # Name assignment may also be missing in the task creation stage.
85
+ return File.basename(@template_name) unless @template_name.nil?
86
+ nil
87
+ end
88
+ end
89
+
90
+ # A task that provides contents to be written to a named file.
91
+ # Optionaly the file may be made executable.
92
+ class WriteTask
93
+ include TaskInterface
94
+
95
+ attr_reader :name, :contents, :executable
96
+
97
+ def initialize(name, contents, executable = false)
98
+ raise ArgumentError, 'name and contents must be given' if name.nil? || contents.nil?
99
+ @name = name
100
+ @contents = contents
101
+ @executable = executable
102
+ end
103
+
104
+ def generate(_context_binding)
105
+ @contents
106
+ end
107
+
108
+ def output_name
109
+ @name
110
+ end
111
+ end
112
+
113
+ # Sets Gen.x to empty hash. Inserted after gem tasks.
114
+ class RestoreProcessorStorage
115
+ include TaskInterface
116
+
117
+ attr_accessor :x # Allows setting the current value when setup code finishes.
118
+
119
+ # Sets initial default value.
120
+ def initialize(x = {})
121
+ @x = x
122
+ Gen.x = @x
123
+ end
124
+
125
+ def generate(_context_binding)
126
+ Gen.x = @x # Restore whatever was current when setup code finished.
127
+ end
128
+
129
+ def discard
130
+ true
131
+ end
132
+
133
+ def system
134
+ true
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright © 2025 Ismo Kärkkäinen
4
+ # Licensed under Universal Permissive License. See LICENSE.txt.
5
+
6
+ module OpenAPISourceTools
7
+ NAME = 'openapi-sourcetools'
8
+ VERSION = '0.8.0'
9
+
10
+ def self.info(separator = ': ')
11
+ "#{NAME}#{separator}#{VERSION}"
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright © 2024-2025 Ismo Kärkkäinen
4
+ # Licensed under Universal Permissive License. See LICENSE.txt.
5
+
6
+ require_relative 'sourcetools/task'
7
+ require_relative 'sourcetools/config'
8
+ require_relative 'sourcetools/version'
9
+ # Other modules or classes are exposed via Gen attributes as class instances as needed.
10
+ # Docs is only needed for run-time storage of whatever loaders can handle.
11
+ # Loaders array is exposed and can be added to at run-time.
12
+ # Helper instance is accessible via Gen.h.
13
+ # Output is exposed via Gen.o and Gen.output. Note that if you assign to one,
14
+ # the other will not change.
15
+ # The rest are for internal implementation.
metadata CHANGED
@@ -1,20 +1,40 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openapi-sourcetools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.8.0
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-08-28 00:00:00.000000000 Z
12
- dependencies: []
13
- description: |2
11
+ date: 2025-01-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: deep_merge
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.2'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.2.2
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '1.2'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.2.2
33
+ description: |-
34
+ Tools for handling API specification in OpenAPI format.
35
+ Programs to replace of duplicate definitions with references. Other checks.
14
36
 
15
- Tools for handling API specification in OpenAPI format. Replacement of
16
- duplicate definitions with references. Other checks. Does not validate
17
- the document against OpenAPI format specification.
37
+ Does not validate the document against OpenAPI format specification.
18
38
  email: ismokarkkainen@icloud.com
19
39
  executables:
20
40
  - openapi-addheaders
@@ -41,15 +61,19 @@ files:
41
61
  - bin/openapi-merge
42
62
  - bin/openapi-modifypaths
43
63
  - bin/openapi-processpaths
44
- - lib/apiobjects.rb
45
- - lib/common.rb
46
- - lib/docs.rb
47
- - lib/gen.rb
48
- - lib/generate.rb
49
- - lib/helper.rb
50
- - lib/loaders.rb
51
- - lib/output.rb
52
- - lib/task.rb
64
+ - lib/openapi/sourcetools.rb
65
+ - lib/openapi/sourcetools/apiobjects.rb
66
+ - lib/openapi/sourcetools/common.rb
67
+ - lib/openapi/sourcetools/config.rb
68
+ - lib/openapi/sourcetools/docs.rb
69
+ - lib/openapi/sourcetools/gen.rb
70
+ - lib/openapi/sourcetools/generate.rb
71
+ - lib/openapi/sourcetools/helper.rb
72
+ - lib/openapi/sourcetools/loaders.rb
73
+ - lib/openapi/sourcetools/output.rb
74
+ - lib/openapi/sourcetools/securityschemes.rb
75
+ - lib/openapi/sourcetools/task.rb
76
+ - lib/openapi/sourcetools/version.rb
53
77
  homepage: https://xn--ismo-krkkinen-gfbd.fi/openapi-sourcetools/index.html
54
78
  licenses:
55
79
  - UPL-1.0
@@ -63,14 +87,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
63
87
  requirements:
64
88
  - - ">="
65
89
  - !ruby/object:Gem::Version
66
- version: 3.0.0
90
+ version: 3.2.5
67
91
  required_rubygems_version: !ruby/object:Gem::Requirement
68
92
  requirements:
69
93
  - - ">="
70
94
  - !ruby/object:Gem::Version
71
95
  version: '0'
72
96
  requirements: []
73
- rubygems_version: 3.2.33
97
+ rubygems_version: 3.4.19
74
98
  signing_key:
75
99
  specification_version: 4
76
100
  summary: Tools for creating source code from API specification.
data/lib/apiobjects.rb DELETED
@@ -1,306 +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, :anchor2ref, :schema_names
37
- attr_accessor :items, :ignored_keys
38
-
39
- def initialize(path, prefix, ignored_keys = %w[summary description examples example $anchor])
40
- path = "#/#{path.join('/')}/" if path.is_a?(Array)
41
- path = "#{path}/" unless path.end_with?('/')
42
- @path = path
43
- @prefix = prefix
44
- @anchor2ref = {}
45
- @schema_names = Set.new
46
- @items = {}
47
- @ignored_keys = Set.new(ignored_keys)
48
- end
49
-
50
- def add_options(opts)
51
- opts.on('--use FIELD', 'Use FIELD in comparisons.') do |f|
52
- @ignored_keys.delete(f)
53
- end
54
- opts.on('--ignore FIELD', 'Ignore FIELD in comparisons.') do |f|
55
- @ignored_keys.add(f)
56
- end
57
- end
58
-
59
- def help
60
- %(All fields are used in object equality comparisons except:\n#{@ignored_keys.to_a.sort!.join("\n")})
61
- end
62
-
63
- def add_schema_name(name)
64
- @schema_names.add(name)
65
- end
66
-
67
- def ref_string(name)
68
- return nil if name.nil?
69
- "#{@path}#{name}"
70
- end
71
-
72
- def reference(obj)
73
- # Check if identical schema has been added. If so, return the $ref string.
74
- @items.each do |k, v|
75
- return ref_string(k) if same(obj, v, @ignored_keys)
76
- end
77
- # One of the numbers will not match existing keys. More number than keys.
78
- (@items.size + 1).times do |n|
79
- # 'x' is to simplify find and replace (Schema1x vs Schema1 and Schema10)
80
- cand = "#{@prefix}#{n}x"
81
- next if @items.key?(cand)
82
- @items[cand] = obj.merge
83
- @schema_names.add(cand)
84
- return ref_string(cand)
85
- end
86
- end
87
-
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
-
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
-
108
- def anchor_ref_replacement(ref)
109
- @anchor2ref[ref[1...ref.size]] || ref
110
- end
111
- end
112
-
113
- class ServerPath
114
- # Probably moves to a separate file once processpaths and frequencies receive
115
- # some attention.
116
- include Comparable
117
-
118
- attr_accessor :parts
119
-
120
- def initialize(parts)
121
- @parts = parts
122
- end
123
-
124
- def <=>(other) # Variables are after fixed strings.
125
- pp = other.is_a?(Array) ? other : p.parts
126
- parts.each_index do |k|
127
- return 1 if pp.size <= k # Longer comes after shorter.
128
- pk = parts[k]
129
- ppk = pp[k]
130
- if pk.is_a? String
131
- if ppk.is_a? String
132
- c = pk <=> ppk
133
- else
134
- return -1
135
- end
136
- else
137
- if ppk.is_a? String
138
- return 1
139
- else
140
- c = pk.fetch('var', '') <=> ppk.fetch('var', '')
141
- end
142
- end
143
- return c unless c.zero?
144
- end
145
- (parts.size < pp.size) ? -1 : 0
146
- end
147
-
148
- def compare(p, range = nil) # Not fit for sorting. Variable equals anything.
149
- pp = p.is_a?(Array) ? p : p.parts
150
- if range.nil?
151
- range = 0...parts.size
152
- elsif range.is_a? Number
153
- range = range...(range + 1)
154
- end
155
- range.each do |k|
156
- return 1 if pp.size <= k # Longer comes after shorter.
157
- ppk = pp[k]
158
- next unless ppk.is_a? String
159
- pk = parts[k]
160
- next unless pk.is_a? String
161
- c = pk <=> ppk
162
- return c unless c.zero?
163
- end
164
- (parts.size < pp.size) ? -1 : 0
165
- end
166
- end
167
-
168
- # The rest probably ends up in a gem that orders schemas and does nothing else.
169
-
170
- # Adds all refs found in the array to refs with given required state.
171
- def gather_array_refs(refs, items, required)
172
- items.each do |s|
173
- r = s['$ref']
174
- next if r.nil?
175
- refs[r] = required || refs.fetch(r, false)
176
- end
177
- end
178
-
179
- # For any key '$ref' adds to refs whether referred type is required.
180
- # Requires that there are no in-lined schemas, openapi-addschemas has been run.
181
- def gather_refs(refs, schema)
182
- # This implies types mixed together according to examples. Needs mixed type.
183
- # AND. Also, mixing may fail. Adds a new schema, do here.
184
- items = schema['allOf']
185
- return gather_array_refs(refs, items, true) unless items.nil?
186
- # As long as one schema is fulfilled, it is ok. OR, first that fits.
187
- items = schema['anyOf'] if items.nil?
188
- # oneOf implies selection between different types. No multiple matches. XOR.
189
- # Needs to ensure that later types do not match.
190
- # Should check if there is enough difference to ensure single match.
191
- # Use separate program run after addschemas to create allOf mixed schema
192
- # and verify the others can be dealt with.
193
- items = schema['oneOf'] if items.nil?
194
- return gather_array_refs(refs, items, false) unless items.nil?
195
- # Defaults below handle it if "type" is not "object".
196
- reqs = schema.fetch('required', [])
197
- schema.fetch('properties', {}).each do |name, spec|
198
- r = spec['$ref']
199
- next if r.nil?
200
- refs[r] = reqs.include?(name) || refs.fetch(r, false)
201
- end
202
- end
203
-
204
- class SchemaInfo
205
- attr_accessor :ref, :schema, :direct_refs, :name, :post_refs
206
-
207
- def initialize(ref, name, schema)
208
- @ref = ref
209
- @name = name
210
- @schema = schema
211
- @direct_refs = {}
212
- gather_refs(@direct_refs, schema)
213
- end
214
-
215
- def set_post_refs(seen)
216
- @post_refs = Set.new(@direct_refs.keys) - seen
217
- end
218
-
219
- def to_s
220
- v = @direct_refs.keys.sort.map { |k| "#{k}:#{@direct_refs[k] ? 'req' : 'opt'}" }
221
- "#{@ref}: #{v.join(' ')}"
222
- end
223
- end
224
-
225
- def var_or_method_value(x, name)
226
- if name.start_with?('@')
227
- n = name
228
- else
229
- n = "@#{name}"
230
- end
231
- return x.instance_variable_get(n) if x.instance_variable_defined?(n)
232
- return x.public_send(name) if x.respond_to?(name)
233
- raise ArgumentError, "#{name} is not #{x.class} instance variable nor public method"
234
- end
235
-
236
- class SchemaOrderer
237
- attr_accessor :schemas, :order, :orderer
238
-
239
- def initialize(path, schema_specs)
240
- @schemas = {}
241
- schema_specs.each do |name, schema|
242
- r = "#{path}#{name}"
243
- @schemas[r] = SchemaInfo.new(r, name, schema)
244
- end
245
- end
246
-
247
- def sort!(orderer = 'required_first')
248
- case orderer
249
- when 'required_first' then @order = required_first
250
- when '<=>' then @order = @schemas.values.sort { |a, b| a <=> b }
251
- else
252
- @order = @schemas.values.sort do |a, b|
253
- va = var_or_method_value(a, orderer)
254
- vb = var_or_method_value(b, orderer)
255
- va <=> vb
256
- end
257
- end
258
- @orderer = orderer
259
- seen = Set.new
260
- @order.each do |si|
261
- si.set_post_refs(seen)
262
- seen.add(si.ref)
263
- end
264
- @order
265
- end
266
-
267
- def required_first
268
- chosen = []
269
- until chosen.size == @schemas.size
270
- used = Set.new(chosen.map { |si| si.ref })
271
- avail = @schemas.values.select { |si| !used.member?(si.ref) }
272
- best = nil
273
- avail.each do |si|
274
- prereq = chosen.count { |x| x.direct_refs.fetch(si.ref, false) }
275
- fulfilled = chosen.count { |x| si.direct_refs.fetch(x.ref, false) }
276
- postreq = si.direct_refs.size - (prereq + fulfilled)
277
- better = false
278
- if best.nil?
279
- better = true
280
- else
281
- # Minimize preceding types requiring this.
282
- if prereq < best.first
283
- better = true
284
- elsif prereq == best.first
285
- # Minimize remaining unfulfilled requires.
286
- if postreq < best[1]
287
- better = true
288
- elsif postreq == best[1]
289
- # Check mutual direct requirements.
290
- best_req_si = best.last.direct_refs.fetch(si.ref, false)
291
- si_req_best = si.direct_refs.fetch(best.last.ref, false)
292
- if best_req_si
293
- better = true unless si_req_best
294
- end
295
- # Order by name if no other difference.
296
- better = si.ref < best.last.ref unless better
297
- end
298
- end
299
- end
300
- best = [ prereq, postreq, si ] if better
301
- end
302
- chosen.push(best.last)
303
- end
304
- chosen
305
- end
306
- end
data/lib/common.rb DELETED
@@ -1,114 +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 yesno(boolean)
16
- boolean ? 'yes' : 'no'
17
- end
18
-
19
- def bury(doc, path, value)
20
- (path.size - 1).times do |k|
21
- p = path[k]
22
- doc[p] = {} unless doc.key?(p)
23
- doc = doc[p]
24
- end
25
- doc[path.last] = value
26
- end
27
-
28
- module Out
29
- attr_reader :count
30
-
31
- def put(message)
32
- aargh(message)
33
- count += 1
34
- end
35
- end
36
-
37
- def split_path(p, spec = false)
38
- parts = []
39
- p = p.strip
40
- unless spec
41
- q = p.index('?')
42
- p.slice!(0...q) unless q.nil?
43
- end
44
- p.split('/').each do |s|
45
- next if s.empty?
46
- s = { var: s } if spec && s.include?('{')
47
- parts.push(s)
48
- end
49
- parts
50
- end
51
-
52
- def load_source(input)
53
- YAML.safe_load(input.nil? ? $stdin : File.read(input))
54
- rescue Errno::ENOENT
55
- aargh "Could not load #{input || 'stdin'}"
56
- rescue StandardError => e
57
- aargh "#{e}\nFailed to read #{input || 'stdin'}"
58
- end
59
-
60
- def dump_result(output, doc, error_return)
61
- doc = YAML.dump(doc, line_width: 1_000_000) unless doc.is_a?(String)
62
- if output.nil?
63
- $stdout.puts doc
64
- else
65
- fp = Pathname.new output
66
- fp.open('w') do |f|
67
- f.puts doc
68
- end
69
- end
70
- 0
71
- rescue StandardError => e
72
- aargh([ e, "Failed to write output: #{output || 'stdout'}" ], error_return)
73
- end
74
-
75
- ServerPath = Struct.new(:parts) do
76
- # Variables are after fixed strings.
77
- def <=>(other)
78
- pp = other.is_a?(Array) ? other : other.parts
79
- parts.each_index do |k|
80
- return 1 if pp.size <= k # Longer comes after shorter.
81
- pk = parts[k]
82
- ppk = pp[k]
83
- if pk.is_a? String
84
- return -1 unless ppk.is_a? String
85
- c = pk <=> ppk
86
- else
87
- return 1 if ppk.is_a? String
88
- c = pk.fetch('var', '') <=> ppk.fetch('var', '')
89
- end
90
- return c unless c.zero?
91
- end
92
- (parts.size < pp.size) ? -1 : 0
93
- end
94
-
95
- # Not fit for sorting. Variable equals anything.
96
- def compare(p, range = nil)
97
- pp = p.is_a?(Array) ? p : p.parts
98
- if range.nil?
99
- range = 0...parts.size
100
- elsif range.is_a? Number
101
- range = range...(range + 1)
102
- end
103
- range.each do |k|
104
- return 1 if pp.size <= k # Longer comes after shorter.
105
- ppk = pp[k]
106
- next unless ppk.is_a? String
107
- pk = parts[k]
108
- next unless pk.is_a? String
109
- c = pk <=> ppk
110
- return c unless c.zero?
111
- end
112
- (parts.size < pp.size) ? -1 : 0
113
- end
114
- end