openapi-sourcetools 0.4.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE.txt +1 -1
- data/bin/openapi-addheaders +73 -0
- data/bin/openapi-addparameters +72 -0
- data/bin/openapi-addresponses +72 -0
- data/bin/openapi-addschemas +104 -0
- data/bin/openapi-checkschemas +217 -0
- data/bin/openapi-frequencies +57 -84
- data/bin/openapi-generate +59 -0
- data/bin/openapi-merge +58 -84
- data/bin/openapi-processpaths +61 -88
- data/lib/apiobjects.rb +333 -0
- data/lib/common.rb +53 -27
- data/lib/gen.rb +87 -0
- data/lib/generate.rb +89 -0
- data/lib/helper.rb +96 -0
- data/lib/loaders.rb +51 -0
- data/lib/task.rb +101 -0
- metadata +27 -8
- data/bin/openapi-generatecode +0 -128
data/lib/apiobjects.rb
ADDED
@@ -0,0 +1,333 @@
|
|
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
CHANGED
@@ -1,25 +1,33 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
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
|
4
11
|
$stderr.puts message
|
5
|
-
|
12
|
+
return_value
|
6
13
|
end
|
7
14
|
|
8
|
-
def
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
when '0' then false
|
14
|
-
when '1' then true
|
15
|
-
else
|
16
|
-
v
|
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]
|
17
20
|
end
|
21
|
+
doc[path.last] = value
|
18
22
|
end
|
19
23
|
|
20
|
-
|
21
|
-
|
22
|
-
|
24
|
+
module Out
|
25
|
+
attr_reader :count
|
26
|
+
|
27
|
+
def put(message)
|
28
|
+
aargh(message)
|
29
|
+
count += 1
|
30
|
+
end
|
23
31
|
end
|
24
32
|
|
25
33
|
def split_path(p, spec = false)
|
@@ -37,32 +45,50 @@ def split_path(p, spec = false)
|
|
37
45
|
parts
|
38
46
|
end
|
39
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
|
+
if output.nil?
|
58
|
+
$stdout.puts doc
|
59
|
+
else
|
60
|
+
fp = Pathname.new output
|
61
|
+
fp.open('w') do |f|
|
62
|
+
f.puts doc
|
63
|
+
end
|
64
|
+
end
|
65
|
+
0
|
66
|
+
rescue StandardError => e
|
67
|
+
aargh([ e, "Failed to write output: #{output || 'stdout'}" ], error_return)
|
68
|
+
end
|
69
|
+
|
40
70
|
ServerPath = Struct.new(:parts) do
|
41
|
-
|
42
|
-
|
71
|
+
# Variables are after fixed strings.
|
72
|
+
def <=>(other)
|
73
|
+
pp = other.is_a?(Array) ? other : other.parts
|
43
74
|
parts.each_index do |k|
|
44
75
|
return 1 if pp.size <= k # Longer comes after shorter.
|
45
76
|
pk = parts[k]
|
46
77
|
ppk = pp[k]
|
47
78
|
if pk.is_a? String
|
48
|
-
|
49
|
-
|
50
|
-
else
|
51
|
-
return -1
|
52
|
-
end
|
79
|
+
return -1 unless ppk.is_a? String
|
80
|
+
c = pk <=> ppk
|
53
81
|
else
|
54
|
-
if ppk.is_a? String
|
55
|
-
|
56
|
-
else
|
57
|
-
c = pk.fetch('var', '') <=> ppk.fetch('var', '')
|
58
|
-
end
|
82
|
+
return 1 if ppk.is_a? String
|
83
|
+
c = pk.fetch('var', '') <=> ppk.fetch('var', '')
|
59
84
|
end
|
60
85
|
return c unless c.zero?
|
61
86
|
end
|
62
87
|
(parts.size < pp.size) ? -1 : 0
|
63
88
|
end
|
64
89
|
|
65
|
-
|
90
|
+
# Not fit for sorting. Variable equals anything.
|
91
|
+
def compare(p, range = nil)
|
66
92
|
pp = p.is_a?(Array) ? p : p.parts
|
67
93
|
if range.nil?
|
68
94
|
range = 0...parts.size
|
data/lib/gen.rb
ADDED
@@ -0,0 +1,87 @@
|
|
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
|
+
require_relative 'helper'
|
8
|
+
|
9
|
+
|
10
|
+
module Gen
|
11
|
+
def self.mod_attr_reader(symbol, docstr = nil)
|
12
|
+
attr_reader(symbol)
|
13
|
+
module_function(symbol)
|
14
|
+
return if docstr.nil?
|
15
|
+
@docsrc = [] unless Gen.instance_variable_defined?('@docsrc')
|
16
|
+
@docsrc.push("- #{symbol.to_s} : #{docstr}")
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.mod_attr_accessor(symbol, docstr = nil)
|
20
|
+
attr_accessor(symbol)
|
21
|
+
module_function(symbol)
|
22
|
+
s = symbol.to_s
|
23
|
+
module_function((s + '=').to_sym)
|
24
|
+
return if docstr.nil?
|
25
|
+
@docsrc = [] unless Gen.instance_variable_defined?('@docsrc')
|
26
|
+
@docsrc.push("- #{s} : #{docstr}")
|
27
|
+
end
|
28
|
+
|
29
|
+
mod_attr_reader :doc, 'OpenAPI document.'
|
30
|
+
mod_attr_reader :outdir, 'Output directory name.'
|
31
|
+
mod_attr_accessor :in_name, 'OpenAPI document name, nil if stdin.'
|
32
|
+
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.'
|
35
|
+
mod_attr_accessor :a, 'Intended for instance with defined attributes.'
|
36
|
+
mod_attr_accessor :h, 'Instance of class with helper methods.'
|
37
|
+
mod_attr_accessor :t, 'Current task instance.'
|
38
|
+
mod_attr_accessor :task_index, 'Current task index.'
|
39
|
+
mod_attr_accessor :loaders, 'Array of generator loader methods.'
|
40
|
+
|
41
|
+
def self.setup(document_content, input_name, output_directory)
|
42
|
+
@doc = document_content
|
43
|
+
@outdir = output_directory
|
44
|
+
if input_name.nil?
|
45
|
+
@in_name = nil
|
46
|
+
@in_basename = nil
|
47
|
+
else
|
48
|
+
@in_name = File.basename(input_name)
|
49
|
+
@in_basename = File.basename(input_name, '.*')
|
50
|
+
end
|
51
|
+
@tasks = []
|
52
|
+
@g = {}
|
53
|
+
@a = nil
|
54
|
+
@h = nil
|
55
|
+
@t = nil
|
56
|
+
@task_index = nil
|
57
|
+
@loaders = []
|
58
|
+
add_task(task: HelperTask.new)
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.add_task(task:, name: nil, executable: false, x: nil)
|
62
|
+
@tasks.push(task)
|
63
|
+
# Since this method allows the user to pass their own task type instance,
|
64
|
+
# assign optional values with defaults only when clearly given.
|
65
|
+
@tasks.last.name = name unless name.nil?
|
66
|
+
@tasks.last.executable = executable unless executable == false
|
67
|
+
@tasks.last.x = x unless x.nil?
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.add_write_content(name:, content:, executable: false)
|
71
|
+
add_task(task: WriteTask.new(name, content, executable))
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.add(source:, template: nil, template_name: nil, name: nil, executable: false, x: nil)
|
75
|
+
add_task(task: Task.new(source, template, template_name), name: name, executable: executable, x: x)
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.document
|
79
|
+
@docsrc.join("\n") + %(
|
80
|
+
- add_task(task:, name: nil, executable: false, x: nil) : Adds task object.
|
81
|
+
- add_write_content(name:, content:, executable: false) : Add file write task.
|
82
|
+
- add(source:, template: nil, template_name: nil, name: nil,
|
83
|
+
executable: false, x: nil) :
|
84
|
+
Adds template task with source as object to process.
|
85
|
+
)
|
86
|
+
end
|
87
|
+
end
|
data/lib/generate.rb
ADDED
@@ -0,0 +1,89 @@
|
|
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
|
+
def executable_bits_on(mode)
|
12
|
+
mode = mode.to_s(8).split('')
|
13
|
+
mode.size.times do |k|
|
14
|
+
m = mode[k].to_i(8)
|
15
|
+
# Applies to Unix-likes. Other system, check and handle.
|
16
|
+
m += 1 unless 3 < mode.size - k || m.zero? || m % 2 == 1
|
17
|
+
mode[k] = m
|
18
|
+
end
|
19
|
+
m = 0
|
20
|
+
mode.each do |v|
|
21
|
+
m = 8 * m + v
|
22
|
+
end
|
23
|
+
m
|
24
|
+
end
|
25
|
+
|
26
|
+
class Generator
|
27
|
+
def initialize(document_content, input_name, output_directory)
|
28
|
+
Gen.setup(document_content, input_name, output_directory)
|
29
|
+
Gen.loaders = Loaders.loaders
|
30
|
+
end
|
31
|
+
|
32
|
+
def context_binding
|
33
|
+
binding
|
34
|
+
end
|
35
|
+
|
36
|
+
def load(generator_names)
|
37
|
+
generator_names.each do |name|
|
38
|
+
idx = Gen.loaders.index { |loader| loader.call(name) }
|
39
|
+
return aargh("No loader could handle #{name}", 2) if idx.nil?
|
40
|
+
end
|
41
|
+
0
|
42
|
+
rescue StandardError => e
|
43
|
+
aargh(e.to_s, 2)
|
44
|
+
end
|
45
|
+
|
46
|
+
def generate(t)
|
47
|
+
t.generate(context_binding)
|
48
|
+
rescue Exception => e
|
49
|
+
aargh(e.to_s, 4)
|
50
|
+
end
|
51
|
+
|
52
|
+
def output_name(t, index)
|
53
|
+
name = t.output_name
|
54
|
+
name = "#{index}.txt" if name.nil?
|
55
|
+
File.join(Gen.outdir, name)
|
56
|
+
end
|
57
|
+
|
58
|
+
def save(name, contents, executable)
|
59
|
+
f = File.new(name, File::WRONLY | File::CREAT | File::TRUNC)
|
60
|
+
s = executable ? f.stat : nil
|
61
|
+
f.write(contents)
|
62
|
+
f.close
|
63
|
+
return unless executable
|
64
|
+
mode = executable_bits_on(s.mode)
|
65
|
+
File.chmod(mode, name) unless mode == s.mode
|
66
|
+
end
|
67
|
+
|
68
|
+
def run
|
69
|
+
# This allows tasks to be added while processing.
|
70
|
+
# Not intended to be done but might prove handy.
|
71
|
+
# Also exposes current task index in case new task is added in the middle.
|
72
|
+
Gen.task_index = 0
|
73
|
+
while Gen.task_index < Gen.tasks.size
|
74
|
+
Gen.t = Gen.tasks[Gen.task_index]
|
75
|
+
out = generate(Gen.t)
|
76
|
+
Gen.task_index += 1
|
77
|
+
next if Gen.t.discard # Check first to ignore result if no output.
|
78
|
+
return out if out.is_a?(Integer)
|
79
|
+
next if out.empty? # Allows no output but return value still checked.
|
80
|
+
name = output_name(Gen.t, Gen.task_index - 1)
|
81
|
+
begin
|
82
|
+
save(name, out, Gen.t.executable)
|
83
|
+
rescue StandardError => e
|
84
|
+
return aargh("Error writing output file: #{name}\n#{e}", 3)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
0
|
88
|
+
end
|
89
|
+
end
|