dim-toolkit 2.1.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 +7 -0
- data/bin/dim +9 -0
- data/lib/dim/commands/check.rb +13 -0
- data/lib/dim/commands/export.rb +145 -0
- data/lib/dim/commands/format.rb +198 -0
- data/lib/dim/commands/stats.rb +64 -0
- data/lib/dim/consistency.rb +187 -0
- data/lib/dim/dimmain.rb +28 -0
- data/lib/dim/encoding.rb +4 -0
- data/lib/dim/exit_helper.rb +23 -0
- data/lib/dim/exporter/csv.rb +25 -0
- data/lib/dim/exporter/exporterInterface.rb +37 -0
- data/lib/dim/exporter/json.rb +32 -0
- data/lib/dim/exporter/rst.rb +142 -0
- data/lib/dim/ext/psych.rb +63 -0
- data/lib/dim/ext/string.rb +85 -0
- data/lib/dim/globals.rb +12 -0
- data/lib/dim/helpers/attribute_helper.rb +126 -0
- data/lib/dim/helpers/file_helper.rb +25 -0
- data/lib/dim/loader.rb +581 -0
- data/lib/dim/options.rb +116 -0
- data/lib/dim/requirement.rb +236 -0
- data/lib/dim/ver.rb +7 -0
- data/lib/dim.rb +1 -0
- data/license.txt +205 -0
- data/version.txt +1 -0
- metadata +98 -0
data/lib/dim/loader.rb
ADDED
@@ -0,0 +1,581 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'pathname'
|
3
|
+
|
4
|
+
require_relative 'encoding'
|
5
|
+
require_relative 'globals'
|
6
|
+
require_relative 'ext/psych'
|
7
|
+
require_relative 'requirement'
|
8
|
+
require_relative 'consistency'
|
9
|
+
require_relative 'exit_helper'
|
10
|
+
require_relative 'helpers/attribute_helper'
|
11
|
+
require_relative 'helpers/file_helper'
|
12
|
+
|
13
|
+
using Dim::Refinements
|
14
|
+
|
15
|
+
module Dim
|
16
|
+
class Loader
|
17
|
+
include Helpers::AttributeHelper
|
18
|
+
|
19
|
+
attr_reader :requirements, :config, :property_table, :properties, :original_data, :module_data, :metadata, :dim_file, :all_attributes, :custom_attributes
|
20
|
+
|
21
|
+
# YAML standard:
|
22
|
+
# invalid C0 control characters: 0x00 - 0x1F (except TAB 0x09, LF 0x0A and CR 0x0D)
|
23
|
+
# invalid control character DEL 0x7F
|
24
|
+
# invalid C1 control characters: 0x80 - 0x9F (except NEL 0x85)
|
25
|
+
#
|
26
|
+
# in addition NEL will be also replaced which seems to be misused in CRS documents
|
27
|
+
@@invalid_ccs = {
|
28
|
+
"\x00" => '[NUL]', "\x01" => '[SOH]', "\x02" => '[STX]', "\x03" => '[ETX]',
|
29
|
+
"\x04" => '[EOT]', "\x05" => '[ENQ]', "\x06" => '[ACK]', "\x07" => '[BEL]',
|
30
|
+
"\x08" => '[BS]', "\x0B" => '[VT]',
|
31
|
+
"\x0C" => '[FF]', "\x0E" => '[SO]', "\x0F" => '[SI]',
|
32
|
+
"\x10" => '[DLE]', "\x11" => '[DC1]', "\x12" => '[DC2]', "\x13" => '[DC3]',
|
33
|
+
"\x14" => '[DC4]', "\x15" => '[NAK]', "\x16" => '[SYN]', "\x17" => '[ETB]',
|
34
|
+
"\x18" => '[CAN]', "\x19" => '[EM]', "\x1A" => '[SUB]', "\x1B" => '[ESC]',
|
35
|
+
"\x1C" => '[FS]', "\x1D" => '[GS]', "\x1E" => '[RS]', "\x1F" => '[US]',
|
36
|
+
"\x7F" => '[DEL]',
|
37
|
+
"\u0080" => '[PAD]', "\u0081" => '[HOP]', "\u0082" => '[BPH]', "\u0083" => '[NBH]',
|
38
|
+
"\u0084" => '[IND]', "\u0085" => '[NEL]', "\u0086" => '[SSA]', "\u0087" => '[ESA]',
|
39
|
+
"\u0088" => '[HTS]', "\u0089" => '[HTJ]', "\u008A" => '[VTS]', "\u008B" => '[PLD]',
|
40
|
+
"\u008C" => '[PLU]', "\u008D" => '[RI]', "\u008E" => '[SS2]', "\u008F" => '[SS3]',
|
41
|
+
"\u0090" => '[DCS]', "\u0091" => '[PU1]', "\u0092" => '[PU2]', "\u0093" => '[STS]',
|
42
|
+
"\u0094" => '[CCH]', "\u0095" => '[MW]', "\u0096" => '[SPA]', "\u0097" => '[EPA]',
|
43
|
+
"\u0098" => '[SOS]', "\u0099" => '[SGCI]', "\u009A" => '[SCI]', "\u009B" => '[CSI]',
|
44
|
+
"\u009C" => '[ST]', "\u009D" => '[OSC]', "\u009E" => '[PM]', "\u009F" => '[APC]'
|
45
|
+
}
|
46
|
+
|
47
|
+
def initialize
|
48
|
+
@requirements = {}
|
49
|
+
@module_data = {}
|
50
|
+
@metadata = {}
|
51
|
+
@config = {}
|
52
|
+
@properties = {}
|
53
|
+
@property_table = {}
|
54
|
+
@original_data = {}
|
55
|
+
@all_attributes = Requirement::SYNTAX.dup
|
56
|
+
@custom_attributes = {}
|
57
|
+
end
|
58
|
+
|
59
|
+
def filter(str)
|
60
|
+
@requirements.keep_if { |_id, r| r.filter(str) }
|
61
|
+
end
|
62
|
+
|
63
|
+
def load(file: nil, attributes_file: nil, allow_missing: false, no_check_enclosed: false, silent: true, input_filenames: [])
|
64
|
+
::Psych::Nodes::Scalar.add_patch
|
65
|
+
::Psych::Visitors::ToRuby.add_patch
|
66
|
+
|
67
|
+
input_filenames = *input_filenames
|
68
|
+
# If output format is vim then we do not need to read the file,
|
69
|
+
# content will be read from the stdin and printed out to stdout. This is to enhance the
|
70
|
+
# formatting in the vim.
|
71
|
+
unless OPTIONS[:output_format] == 'stdout'
|
72
|
+
if input_filenames.length > 0 and file
|
73
|
+
Dim::ExitHelper.exit(code: 1, msg: 'use either file or input_filenames argument (deprecated) for load method')
|
74
|
+
end
|
75
|
+
if input_filenames.length > 1
|
76
|
+
Dim::ExitHelper.exit(code: 1,
|
77
|
+
msg: 'input_filenames argument (deprecated) of load method must have at maximum one entry')
|
78
|
+
end
|
79
|
+
file = input_filenames[0] if input_filenames.length == 1
|
80
|
+
unless file
|
81
|
+
Dim::ExitHelper.exit(code: 1,
|
82
|
+
msg: 'neither file nor input_filenames argument (deprecated) argument specified for load method')
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
unless silent
|
87
|
+
if allow_missing && OPTIONS[:subcommand] != 'format'
|
88
|
+
puts "Warning: Using 'allow-missing' might influence metrics when some references are ignored!\n"
|
89
|
+
end
|
90
|
+
puts 'Start Loading...'
|
91
|
+
end
|
92
|
+
|
93
|
+
Dim::ExitHelper.exit(code: 1, filename: file, msg: 'does not exist') unless File.exist?(file.to_s) || OPTIONS[:output_format] == 'stdout'
|
94
|
+
|
95
|
+
@dim_file = OPTIONS[:output_format] == 'stdout' ? $stdin.read.chomp : File.binread(file).chomp
|
96
|
+
|
97
|
+
if attributes_file
|
98
|
+
fetch_attributes!(folder: File.dirname(attributes_file), filename: attributes_file.split('/').last, silent: silent)
|
99
|
+
elsif !dim_file.match(/^Config:/)
|
100
|
+
folder = search_attributes_file(file.to_s)
|
101
|
+
fetch_attributes!(folder: folder, filename: 'attributes.dim', silent: silent) if folder
|
102
|
+
end
|
103
|
+
|
104
|
+
if dim_file.match(/^Config:/)
|
105
|
+
load_config(config_filename: file.to_s, silent: silent)
|
106
|
+
else
|
107
|
+
load_pattern(
|
108
|
+
config_filename: nil,
|
109
|
+
pattern: file.to_s,
|
110
|
+
origin: '',
|
111
|
+
silent: silent,
|
112
|
+
category: 'unspecified',
|
113
|
+
disable_naming_convention_check: false,
|
114
|
+
no_check_enclosed: no_check_enclosed
|
115
|
+
)
|
116
|
+
end
|
117
|
+
|
118
|
+
puts 'Checking consistency...' unless silent
|
119
|
+
checker = Dim::Consistency.new(self)
|
120
|
+
checker.check(allow_missing: allow_missing)
|
121
|
+
puts 'Done.' unless silent
|
122
|
+
ensure
|
123
|
+
::Psych::Nodes::Scalar.revert_patch
|
124
|
+
::Psych::Visitors::ToRuby.revert_patch
|
125
|
+
end
|
126
|
+
|
127
|
+
def load_config(config_filename:, silent: true, no_check_enclosed: false)
|
128
|
+
puts "Loading [config] #{config_filename}..." unless silent
|
129
|
+
@config = open_yml_file(config_filename, '')
|
130
|
+
|
131
|
+
allowed_attributes = %w[Config Properties Attributes]
|
132
|
+
|
133
|
+
@config.each_key do |k|
|
134
|
+
next unless allowed_attributes.none? { |a| a == k }
|
135
|
+
|
136
|
+
Dim::ExitHelper.exit(code: 1, filename: config_filename, msg: "top level key in config file must be #{allowed_attributes.map do |a|
|
137
|
+
"\"#{a}\""
|
138
|
+
end.join(', ')}, found \"#{k}\"")
|
139
|
+
end
|
140
|
+
|
141
|
+
if @config.key?('Attributes')
|
142
|
+
if @config['Attributes'].is_a?(String)
|
143
|
+
fetch_attributes!(folder: File.dirname(config_filename), filename: @config['Attributes'], silent: silent)
|
144
|
+
else
|
145
|
+
Dim::ExitHelper.exit(code: 1, filename: config_filename, msg: "'Attributes' must be a string")
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
if @config.key?('Properties')
|
150
|
+
if @config['Properties'].is_a?(String)
|
151
|
+
resolve_properties(folder: File.dirname(config_filename), properties_filename: @config['Properties'])
|
152
|
+
else
|
153
|
+
Dim::ExitHelper.exit(code: 1, filename: config_filename, msg: "'Properties' must be a string")
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# COP: Move to constants
|
158
|
+
allowed_keys = %w[files category originator disable_naming_convention_check]
|
159
|
+
allowed_categories = ALLOWED_CATEGORIES.values
|
160
|
+
|
161
|
+
if @config['Config'].is_a?(Array)
|
162
|
+
config_values = @config['Config']
|
163
|
+
else
|
164
|
+
Dim::ExitHelper.exit(code: 1, filename: config_filename, msg: "'Config' must be an array")
|
165
|
+
end
|
166
|
+
|
167
|
+
config_values.each do |value|
|
168
|
+
validate_and_load_config(value, config_filename)
|
169
|
+
|
170
|
+
unless value.is_a?(Hash) && value.keys.sort_by(&:length).eql?(allowed_keys.sort_by(&:length))
|
171
|
+
Dim::ExitHelper.exit(code: 1, filename: config_filename,
|
172
|
+
msg: "each hash in 'Config' array must have key/value pairs for #{allowed_keys.join(', ')}.")
|
173
|
+
end
|
174
|
+
|
175
|
+
if value['category'].is_a?(String)
|
176
|
+
value['category'].strip!
|
177
|
+
unless allowed_categories.include?(value['category'])
|
178
|
+
Dim::ExitHelper.exit(code: 1, filename: config_filename,
|
179
|
+
msg: "attribute \"category\" of '#{value['originator']}' reqs is '#{value['category']}' but must be one of #{allowed_categories.join(', ')}.")
|
180
|
+
end
|
181
|
+
else
|
182
|
+
Dim::ExitHelper.exit(code: 1, filename: config_filename,
|
183
|
+
msg: "attribute \"category\" of '#{value['originator']}' reqs must be a string")
|
184
|
+
end
|
185
|
+
|
186
|
+
if value['files'].is_a?(Array) && value['files'].all? { |a| a.is_a?(String) } || value['files'].is_a?(String)
|
187
|
+
value['files'] = *value['files']
|
188
|
+
else
|
189
|
+
Dim::ExitHelper.exit(code: 1, filename: config_filename,
|
190
|
+
msg: "attribute \"files\" of '#{value['originator']}' reqs must be a string or an array of strings.")
|
191
|
+
end
|
192
|
+
value['files'].each do |pattern|
|
193
|
+
pattern.gsub!(%r{\A\./}, '')
|
194
|
+
if pattern.empty?
|
195
|
+
Dim::ExitHelper.exit(code: 1, filename: config_filename,
|
196
|
+
msg: "attribute \"files\" of '#{value['originator']}' must not have an empty string")
|
197
|
+
end
|
198
|
+
p = Pathname.new(pattern)
|
199
|
+
if p.absolute?
|
200
|
+
Dim::ExitHelper.exit(code: 1, filename: config_filename,
|
201
|
+
msg: "'#{pattern}' must not be an absolute path")
|
202
|
+
end
|
203
|
+
if p.each_filename.any? { |e| e == '..' }
|
204
|
+
Dim::ExitHelper.exit(code: 1, filename: config_filename,
|
205
|
+
msg: "'#{pattern}' must not include '..'")
|
206
|
+
end
|
207
|
+
load_pattern(
|
208
|
+
config_filename: config_filename,
|
209
|
+
pattern: pattern,
|
210
|
+
origin: value['originator'],
|
211
|
+
silent: silent,
|
212
|
+
category: value['category'],
|
213
|
+
disable_naming_convention_check: value['disable_naming_convention_check'],
|
214
|
+
no_check_enclosed: no_check_enclosed
|
215
|
+
)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
puts 'Done.' unless silent
|
219
|
+
end
|
220
|
+
|
221
|
+
def extract_type(attr)
|
222
|
+
return {} if attr.empty?
|
223
|
+
|
224
|
+
hscan = attr.scan(/\Ah(\d+)\s.+/)
|
225
|
+
if hscan.length == 1
|
226
|
+
level = hscan[0][0].to_i
|
227
|
+
return { 'type' => "heading_#{level}", 'text' => attr[2 + hscan[0][0].length..-1].strip }
|
228
|
+
else
|
229
|
+
iscan = attr.scan(/\Ainfo\s.+/)
|
230
|
+
return { 'type' => 'information', 'text' => attr[5..-1].strip } if iscan.length == 1
|
231
|
+
end
|
232
|
+
nil
|
233
|
+
end
|
234
|
+
|
235
|
+
def load_file(filename:, origin:, silent:, category:, disable_naming_convention_check: false, no_check_enclosed: false)
|
236
|
+
puts "Loading [#{origin.empty? ? "unknown" : origin}] #{filename}..." unless silent
|
237
|
+
binary_data = OPTIONS[:output_format] == 'stdout' ? @dim_file : File.binread(filename).force_encoding('UTF-8')
|
238
|
+
|
239
|
+
# this looks expensive but measurement showed it's close to zero
|
240
|
+
@@invalid_ccs.each { |k, v| binary_data = binary_data.gsub(k, v) }
|
241
|
+
|
242
|
+
if binary_data.valid_encoding?
|
243
|
+
begin
|
244
|
+
psych_doc = YAML.parse(binary_data)
|
245
|
+
rescue Psych::SyntaxError => e
|
246
|
+
Dim::ExitHelper.exit(code: 1, filename: filename, msg: e.message)
|
247
|
+
end
|
248
|
+
else
|
249
|
+
begin
|
250
|
+
psych_doc = YAML.parse(binary_data.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
|
251
|
+
filename: filename)
|
252
|
+
rescue Psych::SyntaxError => e
|
253
|
+
Dim::ExitHelper.exit(code: 1, filename: filename, msg: e.message)
|
254
|
+
end
|
255
|
+
end
|
256
|
+
unless psych_doc
|
257
|
+
puts "Warning: empty file detected; skipped loading of #{filename}"
|
258
|
+
|
259
|
+
return
|
260
|
+
end
|
261
|
+
reqs = psych_doc.to_ruby
|
262
|
+
|
263
|
+
unless reqs.is_a?(Hash)
|
264
|
+
Dim::ExitHelper.exit(code: 1, filename: filename,
|
265
|
+
msg: 'top level must be a hash with keys "module", "enclosed", "metadata" and/or unique ids'
|
266
|
+
)
|
267
|
+
end
|
268
|
+
|
269
|
+
# TODO: Remove module backward compatibility in future version
|
270
|
+
if reqs.key?('document') && reqs.key?('module')
|
271
|
+
Dim::ExitHelper.exit(
|
272
|
+
code: 1,
|
273
|
+
filename: filename,
|
274
|
+
msg: 'module and document found in the file; please rename module to document'
|
275
|
+
)
|
276
|
+
end
|
277
|
+
|
278
|
+
if reqs.key?('document')
|
279
|
+
if !reqs['document'].is_a?(String) || reqs['document'].empty?
|
280
|
+
Dim::ExitHelper.exit(code: 1, filename: filename, msg: 'document must be a non-empty string')
|
281
|
+
end
|
282
|
+
document = reqs['document']
|
283
|
+
# TODO: Remove module backward compatibility in future version
|
284
|
+
reqs['module'] = document
|
285
|
+
elsif reqs.key?('module')
|
286
|
+
if !(reqs['module'].is_a? String) || reqs['module'].empty?
|
287
|
+
Dim::ExitHelper.exit(code: 1, filename: filename, msg: 'module name must be a non-empty string')
|
288
|
+
end
|
289
|
+
document = reqs['module']
|
290
|
+
reqs['document'] = document
|
291
|
+
else
|
292
|
+
Dim::ExitHelper.exit(code: 1, filename: filename, msg: 'Document name is missing; please add document name')
|
293
|
+
end
|
294
|
+
|
295
|
+
validate_srs_name(document, disable_naming_convention_check, category, filename)
|
296
|
+
|
297
|
+
if @module_data.key?(document)
|
298
|
+
if @module_data[document][:origin] != origin
|
299
|
+
Dim::ExitHelper.exit(code: 1, filename: filename, msg:
|
300
|
+
"files of the same module must have the same owner:\n" +
|
301
|
+
"- #{@module_data[document][:files].first[0]} (#{@module_data[document][:origin]})\n" +
|
302
|
+
"- #{filename} (#{origin})")
|
303
|
+
elsif @module_data[document][:category] != category
|
304
|
+
Dim::ExitHelper.exit(code: 1, filename: filename, msg:
|
305
|
+
"files of the same module must have the same category:\n" +
|
306
|
+
"- #{@module_data[document][:files].first[0]} (#{@module_data[document][:category]})\n" +
|
307
|
+
"- #{filename} (#{category})")
|
308
|
+
end
|
309
|
+
else
|
310
|
+
@module_data[document] = { origin: origin, category: category, files: {} }
|
311
|
+
end
|
312
|
+
|
313
|
+
if @module_data[document][:files].key?(filename)
|
314
|
+
Dim::ExitHelper.exit(code: 1, filename: filename, msg: "file #{filename} already loaded")
|
315
|
+
end
|
316
|
+
|
317
|
+
@module_data[document][:files][filename] = []
|
318
|
+
@metadata[document] ||= ''
|
319
|
+
|
320
|
+
if reqs.key?('metadata')
|
321
|
+
unless reqs['metadata'].is_a?(String)
|
322
|
+
Dim::ExitHelper.exit(code: 1, filename: filename, msg: 'metadata must be a string')
|
323
|
+
end
|
324
|
+
unless reqs['metadata'].empty?
|
325
|
+
unless @metadata[document].empty?
|
326
|
+
Dim::ExitHelper.exit(code: 1, filename: filename, msg: 'only one metadata per module allowed')
|
327
|
+
end
|
328
|
+
@metadata[document] = reqs['metadata'].strip
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
if reqs.key?('enclosed') && !no_check_enclosed
|
333
|
+
ecl = reqs['enclosed']
|
334
|
+
ecl = [ecl] if ecl.is_a?(String)
|
335
|
+
if !ecl.is_a?(Array) || ecl.empty? || ecl.any? { |s| !s.is_a?(String) || s.empty? }
|
336
|
+
Dim::ExitHelper.exit(code: 1, filename: filename,
|
337
|
+
msg: '"enclosed" must be a non-empty string or an array of non-empty strings')
|
338
|
+
end
|
339
|
+
# Remove superfluous ./ from path
|
340
|
+
ecl = ecl.map do |path|
|
341
|
+
if path.match?(/\\/)
|
342
|
+
puts "Warning: Backward slashes detected in filepath #{path}. Use '/' over '\\' in filepath"
|
343
|
+
path.gsub!('\\', '/')
|
344
|
+
end
|
345
|
+
Pathname.new(path).cleanpath.to_s
|
346
|
+
end
|
347
|
+
dir_file = File.dirname(filename)
|
348
|
+
ecl.each do |l|
|
349
|
+
p = Pathname.new(l)
|
350
|
+
Dim::ExitHelper.exit(code: 1, filename: filename, msg: "'#{l}' must not be an absolute path") if p.absolute?
|
351
|
+
if p.each_filename.any? do |name|
|
352
|
+
name == '..'
|
353
|
+
end
|
354
|
+
Dim::ExitHelper.exit(code: 1, filename: filename,
|
355
|
+
msg: "'#{l}' must not include '..'")
|
356
|
+
end
|
357
|
+
|
358
|
+
src = File.join(dir_file, l)
|
359
|
+
src_globbed = Dir.glob(src)
|
360
|
+
if src_globbed.empty?
|
361
|
+
Dim::ExitHelper.exit(code: 1, filename: filename,
|
362
|
+
msg: "\"#{l}\" in \"enclosed\" does not refer to any existing file")
|
363
|
+
end
|
364
|
+
end
|
365
|
+
@module_data[document][:files][filename] += ecl
|
366
|
+
end
|
367
|
+
|
368
|
+
line_numbers = psych_doc.line_numbers
|
369
|
+
|
370
|
+
reqs.each do |id, attr|
|
371
|
+
next if %w[module enclosed metadata document].include? id
|
372
|
+
|
373
|
+
if @requirements.key?(id)
|
374
|
+
Dim::ExitHelper.exit(code: 1, filename: filename, msg: "id \"#{id}\" found more than once")
|
375
|
+
end
|
376
|
+
|
377
|
+
if id.include?(',')
|
378
|
+
Dim::ExitHelper.exit(code: 1, filename: filename, msg: "Disallowed ',' found in id \"#{id}\"")
|
379
|
+
end
|
380
|
+
|
381
|
+
validate_srs_name(id, disable_naming_convention_check, category, filename, 'ID')
|
382
|
+
|
383
|
+
if attr.is_a?(String)
|
384
|
+
attr.strip!
|
385
|
+
res = extract_type(attr)
|
386
|
+
if res
|
387
|
+
attr = res
|
388
|
+
else
|
389
|
+
Dim::ExitHelper.exit(code: 1, filename: filename,
|
390
|
+
msg: "Invalid short-form in requirement \"#{id}\", valid forms are \"h<level> <text>\" or \"info <text>\"")
|
391
|
+
end
|
392
|
+
elsif attr.is_a?(Hash)
|
393
|
+
attr.keys.select do |k|
|
394
|
+
@all_attributes.key?(k) && %i[multi split].include?(@all_attributes[k][:format_style])
|
395
|
+
end.each do |k|
|
396
|
+
attr[k] = attr[k].cleanUniqString if attr[k].is_a?(String)
|
397
|
+
end
|
398
|
+
attr['tags'] = attr['tags'].cleanUniqString if attr['tags'].is_a?(String)
|
399
|
+
else
|
400
|
+
Dim::ExitHelper.exit(code: 1, filename: filename, msg: "attributes for id \"#{id}\" must be key-value pairs")
|
401
|
+
end
|
402
|
+
unless attr.key?('verification_methods')
|
403
|
+
attr['verification_methods'] = attr['test_setups'] if attr.key?('test_setups')
|
404
|
+
end
|
405
|
+
attr.each do |key, value|
|
406
|
+
unless value.is_a?(String)
|
407
|
+
Dim::ExitHelper.exit(code: 1, filename: filename,
|
408
|
+
msg: "value of attribute \"#{key}\" must be String not #{value.class}")
|
409
|
+
end
|
410
|
+
attr[key].gsub!("\u00A0", ' ')
|
411
|
+
attr[key].strip!
|
412
|
+
end
|
413
|
+
|
414
|
+
r = Requirement.new(id, document, filename, attr, origin, self, category, line_numbers[id], @all_attributes)
|
415
|
+
reqs[id] = attr
|
416
|
+
@requirements[id] = r
|
417
|
+
end
|
418
|
+
|
419
|
+
@original_data[filename] = Marshal.load(Marshal.dump(reqs))
|
420
|
+
end
|
421
|
+
|
422
|
+
def load_pattern(config_filename:, pattern:, origin:, silent:, category:, disable_naming_convention_check: false, no_check_enclosed: false)
|
423
|
+
if pattern.match?(/\\/)
|
424
|
+
puts "Warning: Backward slashes detected in pattern #{pattern}. Use '/' over '\\'"
|
425
|
+
pattern.gsub!('\\', '/')
|
426
|
+
end
|
427
|
+
pattern_search = config_filename ? File.join(File.dirname(config_filename), pattern) : pattern
|
428
|
+
fs = Dir.glob(pattern_search).sort
|
429
|
+
if fs.empty? && !silent
|
430
|
+
puts "Info: no matches for \"#{pattern}\" in \"#{config_filename}\""
|
431
|
+
end
|
432
|
+
fs.each do |f|
|
433
|
+
load_file(filename: f, origin: origin, silent: silent, category: category, disable_naming_convention_check: disable_naming_convention_check, no_check_enclosed: no_check_enclosed)
|
434
|
+
end
|
435
|
+
return unless OPTIONS[:output_format] == 'stdout'
|
436
|
+
|
437
|
+
load_file(filename: '', origin: origin, silent: silent, category: category, disable_naming_convention_check: disable_naming_convention_check, no_check_enclosed: no_check_enclosed)
|
438
|
+
end
|
439
|
+
|
440
|
+
def resolve_properties(folder:, properties_filename:)
|
441
|
+
@properties = open_yml_file(folder, properties_filename, allow_empty_file: true)
|
442
|
+
unless @properties
|
443
|
+
puts "Warning: empty file detected; skipped loading of #{properties_filename}"
|
444
|
+
return
|
445
|
+
end
|
446
|
+
|
447
|
+
@properties.each do |document, value|
|
448
|
+
value.each do |attr, property_value|
|
449
|
+
next unless @all_attributes.key?(attr)
|
450
|
+
|
451
|
+
unless property_value.is_a?(String)
|
452
|
+
Dim::ExitHelper.exit(code: 1, filename: properties_filename,
|
453
|
+
msg: "The value for key #{attr} in properties files must be a string")
|
454
|
+
end
|
455
|
+
|
456
|
+
if @all_attributes.dig(attr, :allowed).nil? ||
|
457
|
+
!property_value.cleanArray.select do |val|
|
458
|
+
!@all_attributes[attr][:allowed].include?(val)
|
459
|
+
end.any?
|
460
|
+
@property_table[document] ||= {}
|
461
|
+
@property_table[document][attr] = property_value.strip
|
462
|
+
else
|
463
|
+
Dim::ExitHelper.exit(code: 1, filename: properties_filename,
|
464
|
+
msg: "The properties file includes an invalid #{attr} value '#{property_value}' for module: #{document}.")
|
465
|
+
end
|
466
|
+
end
|
467
|
+
end
|
468
|
+
end
|
469
|
+
|
470
|
+
def fetch_attributes!(folder:, filename:, silent:)
|
471
|
+
puts "Loading [attributes] #{File.join(folder, filename)}" unless silent
|
472
|
+
|
473
|
+
@custom_attributes.merge!(resolve_attributes(folder: folder, filename: filename))
|
474
|
+
@all_attributes.merge!(@custom_attributes)
|
475
|
+
end
|
476
|
+
|
477
|
+
def search_attributes_file(file)
|
478
|
+
path = Pathname.new(file).parent.realpath
|
479
|
+
if path.root?
|
480
|
+
nil
|
481
|
+
else
|
482
|
+
return path if Dir.new(path).children.include?('attributes.dim')
|
483
|
+
search_attributes_file(path)
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
487
|
+
def validate_and_load_config(value, config_filename)
|
488
|
+
disable_naming_convention_check = value['disable_naming_convention_check']
|
489
|
+
unless disable_naming_convention_check
|
490
|
+
value['disable_naming_convention_check'] = false
|
491
|
+
return
|
492
|
+
end
|
493
|
+
|
494
|
+
if value['category'] != ALLOWED_CATEGORIES[:software]
|
495
|
+
warn("Warning: disable_naming_convention_check attribute will only take effect when category is software")
|
496
|
+
end
|
497
|
+
|
498
|
+
if disable_naming_convention_check == 'yes'
|
499
|
+
value['disable_naming_convention_check'] = true
|
500
|
+
return
|
501
|
+
elsif disable_naming_convention_check == 'no'
|
502
|
+
value['disable_naming_convention_check'] = false
|
503
|
+
return
|
504
|
+
end
|
505
|
+
|
506
|
+
Dim::ExitHelper.exit(
|
507
|
+
code: 1,
|
508
|
+
filename: config_filename,
|
509
|
+
msg: 'disable_naming_convention_check in config must be either boolean value or a string "yes" or "no"'
|
510
|
+
)
|
511
|
+
end
|
512
|
+
|
513
|
+
def validate_srs_name(name, disable_naming_convention_check, category, filename, attr = 'module')
|
514
|
+
return if category != ALLOWED_CATEGORIES[:software] || disable_naming_convention_check
|
515
|
+
|
516
|
+
# raise error if not starting with SRS_
|
517
|
+
unless name.match?(/^(SRS_)/)
|
518
|
+
Dim::ExitHelper.exit(
|
519
|
+
code: 1,
|
520
|
+
filename: filename,
|
521
|
+
msg: "#{attr} #{name} in software requirement must start with \"SRS_\""
|
522
|
+
)
|
523
|
+
end
|
524
|
+
|
525
|
+
_srs, feature, aspect, *rest = name.split('_')
|
526
|
+
|
527
|
+
# Raise error if more than two _ detected
|
528
|
+
unless rest.empty?
|
529
|
+
Dim::ExitHelper.exit(
|
530
|
+
code: 1,
|
531
|
+
filename: filename,
|
532
|
+
msg: "#{attr} #{name} in software requirement must contain exactly two \"_\""
|
533
|
+
)
|
534
|
+
end
|
535
|
+
|
536
|
+
if feature.to_s.empty?
|
537
|
+
Dim::ExitHelper.exit(
|
538
|
+
code: 1,
|
539
|
+
filename: filename,
|
540
|
+
msg: "invalid #{attr} #{name} in software requirement; missing feature after \"SRS_\""
|
541
|
+
)
|
542
|
+
end
|
543
|
+
|
544
|
+
# Raise error if feature or aspect is non alphanumeric
|
545
|
+
if feature.match?(SRS_NAME_REGEX)
|
546
|
+
Dim::ExitHelper.exit(
|
547
|
+
code: 1,
|
548
|
+
filename: filename,
|
549
|
+
msg: "feature in #{attr} #{name} in software requirement contains non-alphanumeric characters"
|
550
|
+
)
|
551
|
+
end
|
552
|
+
|
553
|
+
if attr == 'module' && !aspect.to_s.empty?
|
554
|
+
Dim::ExitHelper.exit(
|
555
|
+
code: 1,
|
556
|
+
filename: filename,
|
557
|
+
msg: "invalid module #{name} in software requirement; must contain exactly one \"_\""
|
558
|
+
)
|
559
|
+
end
|
560
|
+
|
561
|
+
# Check naming convention for aspect only in case of IDs
|
562
|
+
return if attr == 'module'
|
563
|
+
|
564
|
+
if aspect.to_s.empty?
|
565
|
+
Dim::ExitHelper.exit(
|
566
|
+
code: 1,
|
567
|
+
filename: filename,
|
568
|
+
msg: "invalid ID #{name} in software requirement; missing aspect/ratio after \"SRS_feature\""
|
569
|
+
)
|
570
|
+
end
|
571
|
+
|
572
|
+
if aspect.match?(SRS_NAME_REGEX)
|
573
|
+
Dim::ExitHelper.exit(
|
574
|
+
code: 1,
|
575
|
+
filename: filename,
|
576
|
+
msg: "aspect in ID #{name} in software requirement contains non-alphanumeric characters"
|
577
|
+
)
|
578
|
+
end
|
579
|
+
end
|
580
|
+
end
|
581
|
+
end
|