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.
@@ -0,0 +1,25 @@
1
+ require_relative '../globals'
2
+ require_relative '../requirement'
3
+
4
+ module Dim
5
+ class Csv < ExporterInterface
6
+ EXPORTER['csv'] = self
7
+
8
+ def header(content)
9
+ @keys = @loader.all_attributes.keys
10
+ @keys.delete('test_setups')
11
+ content.puts 'Sep=,'
12
+ content.puts "id,document_name,originator,#{@keys.join(',')}"
13
+ end
14
+
15
+ def requirement(content, req)
16
+ vals = [req.id, req.document, req.origin]
17
+ @keys.each { |k| vals << req.data[k] }
18
+ # These values will never be nil.
19
+ # ID cannot be nil in Dim file, so as origin (default is "") and
20
+ # document cannot be missing in Dim files.
21
+ # Which leaves with data and YAML file cannot define nil value.
22
+ content.puts(vals.map { |a| "\"#{a.gsub('"', '""')}\"" }.join(','))
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,37 @@
1
+ module Dim
2
+ # This is how the interface is used by the Dim::Export:
3
+ #
4
+ # initialize()
5
+ #
6
+ # for every module:
7
+ # header()
8
+ # document()
9
+ # metadata()
10
+ # for every requirement in module:
11
+ # requirement()
12
+ # footer()
13
+ #
14
+ # if hasIndex:
15
+ # for every originator/category combination:
16
+ # index()
17
+ class ExporterInterface
18
+ attr_reader :hasIndex
19
+
20
+ def initialize(loader)
21
+ @hasIndex = false
22
+ @loader = loader
23
+ end
24
+
25
+ def header(f); end
26
+
27
+ def document(f, name); end
28
+
29
+ def metadata(f, metadata); end
30
+
31
+ def requirement(f, r); end
32
+
33
+ def footer(f); end
34
+
35
+ def index(f, category, origin, modules); end
36
+ end
37
+ end
@@ -0,0 +1,32 @@
1
+ require 'json'
2
+
3
+ require_relative '../globals'
4
+ require_relative '../requirement'
5
+
6
+ module Dim
7
+ class Json < ExporterInterface
8
+ EXPORTER['json'] = self
9
+
10
+ def header(_f)
11
+ @content = []
12
+ end
13
+
14
+ def requirement(_f, r)
15
+ vals = { 'id' => r.id, 'document_name' => r.document, 'originator' => r.origin }
16
+
17
+ @loader.all_attributes.keys.each do |k|
18
+ next if k == 'test_setups'
19
+
20
+ v = r.data[k]
21
+ v = v.cleanUniqArray.join(',') if k == 'refs'
22
+ vals[k] = v.strip
23
+ end
24
+
25
+ @content << vals
26
+ end
27
+
28
+ def footer(f)
29
+ f.puts(JSON.pretty_generate(@content))
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,142 @@
1
+ require 'json'
2
+
3
+ module Dim
4
+ class Rst < ExporterInterface
5
+ EXPORTER['rst'] = self
6
+
7
+ def initialize(loader)
8
+ super(loader)
9
+ @hasIndex = true
10
+ end
11
+
12
+ def document(f, name)
13
+ raw_html_name = ':raw-html:`' + name + '`'
14
+ f.puts raw_html_name
15
+ f.puts '=' * raw_html_name.length
16
+ @lastHeadingLevel = 0
17
+ @moduleName = name
18
+ end
19
+
20
+ def metadata(f, meta)
21
+ f.puts ''
22
+ f.puts html(meta.strip.escape_html, with_space: false)
23
+ end
24
+
25
+ def level2char(level)
26
+ { 0 => '=',
27
+ 1 => '-',
28
+ 2 => '+',
29
+ 3 => '~',
30
+ 4 => '^',
31
+ 5 => '"' }.fetch(level, '"')
32
+ end
33
+
34
+ def html(elem, with_space: true)
35
+ return if elem.empty?
36
+
37
+ with_space ? " :raw-html:`#{elem}`" : ":raw-html:`#{elem}`"
38
+ end
39
+
40
+ def handle_empty_value(value)
41
+ return '' if value.empty?
42
+
43
+ ' ' + (value.is_a?(Array) ? value.join(', ') : value)
44
+ end
45
+
46
+ def createMultiLanguageElement(r, name)
47
+ lang_elems = r.data.keys.select { |k| k.start_with?("#{name}_") && !r.data[k].empty? }.sort
48
+ if lang_elems.empty?
49
+ return r.data[name].empty? ? '' : r.data[name]
50
+ end
51
+
52
+ str = (r.data[name].empty? ? '-' : r.data[name])
53
+ lang_elems.each do |l|
54
+ str << "<br><br><b>#{l.split('_').map(&:capitalize).join(' ')}: </b>"
55
+ str << r.data[l]
56
+ end
57
+ str
58
+ end
59
+
60
+ def requirement(f, r)
61
+ r.data.each { |k, v| r.data[k] = v.strip.escape_html }
62
+
63
+ if r.data['type'].start_with?('heading')
64
+ (@lastHeadingLevel + 1...r.depth).each do |l|
65
+ str = '<Skipped Heading Level>'
66
+ f.puts ''
67
+ f.puts str
68
+ f.puts level2char(l) * str.length
69
+ end
70
+ f.puts ''
71
+ str = ':raw-html:`' + r.data['text'] + '`'
72
+ f.puts str
73
+ f.puts level2char(r.depth) * str.length
74
+ @lastHeadingLevel = r.depth
75
+ return
76
+ end
77
+
78
+ r.data['tester'].gsub!('<br>', ' ')
79
+ r.data['developer'].gsub!('<br>', ' ')
80
+ text = createMultiLanguageElement(r, 'text')
81
+ comment = createMultiLanguageElement(r, 'comment')
82
+ refs = r.data['refs'].cleanUniqArray.select do |ref|
83
+ !@loader.requirements.has_key?(ref) || !@loader.requirements[ref].type.start_with?('heading')
84
+ end
85
+ tags = r.data['tags'].cleanUniqString
86
+ sources = r.data['sources'].cleanUniqString
87
+
88
+ f.puts ''
89
+ f.puts ".. #{r.data['type']}:: #{r.id}"
90
+ f.puts " :category: #{r.category}"
91
+ f.puts " :status: #{r.data['status']}"
92
+ f.puts " :review_status: #{r.data['review_status']}"
93
+ f.puts " :asil: #{r.data['asil']}"
94
+ f.puts " :cal: #{r.data['cal']}"
95
+ f.puts " :tags:#{handle_empty_value(tags)}"
96
+ f.puts " :comment:#{html(comment)}"
97
+ f.puts " :miscellaneous:#{html(r.data['miscellaneous'])}"
98
+ f.puts " :refs:#{handle_empty_value(refs)}"
99
+ @loader.custom_attributes.each_key do |custom_attribute|
100
+ f.puts " :#{custom_attribute}:#{handle_empty_value(r.data[custom_attribute])}"
101
+ end
102
+ if r.data['type'] == 'requirement'
103
+ vc = createMultiLanguageElement(r, 'verification_criteria')
104
+
105
+ f.puts " :sources:#{handle_empty_value(sources)}"
106
+ f.puts " :feature:#{html(r.data['feature'])}"
107
+ f.puts " :change_request:#{html(r.data['change_request'])}"
108
+ f.puts " :developer:#{handle_empty_value(r.data['developer'])}"
109
+ f.puts " :tester:#{handle_empty_value(r.data['tester'])}"
110
+ f.puts " :verification_methods:#{handle_empty_value(r.data['verification_methods'])}"
111
+ f.puts " :verification_criteria:#{html(vc)}"
112
+ end
113
+
114
+ f.puts "\n #{html(text)}" unless text.empty?
115
+ end
116
+
117
+ def footer(f)
118
+ files = @loader.module_data[@moduleName][:files].values.flatten
119
+ return if files.empty?
120
+
121
+ f.puts ''
122
+ f.puts '.. enclosed::'
123
+ f.puts ''
124
+ files.each do |file|
125
+ f.puts " #{file}"
126
+ end
127
+ end
128
+
129
+ def index(f, category, origin, modules)
130
+ caption = category.capitalize + ' (' + origin + ')'
131
+ f.puts caption
132
+ f.puts '=' * caption.length
133
+ f.puts ''
134
+ f.puts '.. toctree::'
135
+ f.puts ' :maxdepth: 1'
136
+ f.puts ''
137
+ modules.sort.each do |m|
138
+ f.puts " #{m.sanitize}/Requirements"
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,63 @@
1
+ require_relative '../exit_helper'
2
+
3
+ module Dim
4
+ module Refinements
5
+ refine Psych::Nodes::Document do
6
+ def line_numbers
7
+ hash = {}
8
+ children[0].children.each do |node|
9
+ if node.is_a?(Psych::Nodes::Scalar)
10
+ hash[node.value] = node.start_line + 1
11
+ end
12
+ end
13
+ hash
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ module Psych
20
+ module Visitors
21
+ class ToRuby
22
+ alias revive_hash_org revive_hash
23
+
24
+ def patched_revive_hash(hash, o)
25
+ test_hash = {}
26
+ o.children.each_slice(2) do |k, _v|
27
+ key = accept(k)
28
+ if test_hash.has_key?(key)
29
+ line = "line #{k.start_line + 1}: "
30
+ Dim::ExitHelper.exit(code: 1, msg: "#{line}found \"#{key}\" twice which must be unique.")
31
+ end
32
+ test_hash[key] = k
33
+ end
34
+ revive_hash_org hash, o
35
+ end
36
+
37
+ def self.add_patch
38
+ alias revive_hash patched_revive_hash
39
+ end
40
+
41
+ def self.revert_patch
42
+ alias revive_hash revive_hash_org
43
+ end
44
+ end
45
+ end
46
+
47
+ module Nodes
48
+ class Scalar
49
+ alias initialize_org initialize
50
+ def quoted_initialize(value, anchor = nil, tag = nil, plain = true, _quoted = false, style = ANY)
51
+ initialize_org(value, anchor, tag, plain, true, style)
52
+ end
53
+
54
+ def self.add_patch
55
+ alias initialize quoted_initialize
56
+ end
57
+
58
+ def self.revert_patch
59
+ alias initialize initialize_org
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,85 @@
1
+ class String
2
+ def cleanString
3
+ cleanArray.join(', ')
4
+ end
5
+
6
+ def cleanUniqString
7
+ cleanUniqArray.join(', ')
8
+ end
9
+
10
+ def cleanUniqArray
11
+ cleanArray.uniq
12
+ end
13
+
14
+ def cleanArray
15
+ cleanSplit.select { |s| !s.empty? }
16
+ end
17
+
18
+ def cleanSplit
19
+ split(/(?<!\\),/).map(&:strip)
20
+ end
21
+
22
+ def addEnum(e)
23
+ replace((cleanArray << e).uniq.join(', '))
24
+ end
25
+
26
+ def removeEnum(e)
27
+ arr = cleanArray
28
+ arr.delete(e)
29
+ replace(arr.join(', '))
30
+ end
31
+
32
+ def escapeHtmlOutside
33
+ gsub(/&(?!(amp|lt|gt|quot|apos|emsp);)/, '&amp;')
34
+ .gsub('<', '&lt;')
35
+ .gsub('>', '&gt;')
36
+ .gsub('"', '&quot;')
37
+ .gsub("'", '&apos;')
38
+ .gsub("\t", '&emsp;')
39
+ .gsub("\n", '<br>')
40
+ .gsub('`', '&#96;')
41
+ .gsub(/(?<= ) /, '&nbsp')
42
+ end
43
+
44
+ def escape_html_inside
45
+ gsub('`', '&#96;')
46
+ .gsub("\t", '&emsp;')
47
+ .gsub("\n", ' ')
48
+ end
49
+
50
+ def get_next_escape_token(pos)
51
+ ind = index(%r{<\s*(\/?)\s*html\s*>}, pos)
52
+ return [:none, length - pos, -1] if ind.nil?
53
+
54
+ type = Regexp.last_match(1).empty? ? :start : :end
55
+ [type, ind - pos, ind + Regexp.last_match(0).length]
56
+ end
57
+
58
+ def escape_html
59
+ str = ''
60
+ search_pos = 0
61
+ nested = 0
62
+ while true
63
+ next_token, token_pos, after_token_pos = get_next_escape_token(search_pos)
64
+ if nested == 0
65
+ str << self[search_pos, token_pos].escapeHtmlOutside
66
+ nested = 1 if next_token == :start
67
+ else
68
+ str << self[search_pos, token_pos].escape_html_inside
69
+ nested += (next_token == :start ? +1 : -1)
70
+ end
71
+ break if next_token == :none
72
+
73
+ search_pos = after_token_pos
74
+ end
75
+ str.strip
76
+ end
77
+
78
+ def sanitize
79
+ gsub(/[^a-zA-Z0-9.\-_]/, '_')
80
+ end
81
+
82
+ def universal_newline
83
+ encode(encoding, universal_newline: true)
84
+ end
85
+ end
@@ -0,0 +1,12 @@
1
+ OPTIONS ||= {}
2
+ SUBCOMMANDS ||= {}
3
+ EXPORTER ||= {}
4
+ CATEGORY_ORDER = {
5
+ 'input' => 1,
6
+ 'system' => 2,
7
+ 'software' => 3,
8
+ 'architecture' => 4,
9
+ 'module' => 5,
10
+ }.freeze
11
+ ALLOWED_CATEGORIES = CATEGORY_ORDER.keys.each_with_object({}) { |k, obj| obj[k.to_sym] = k }.freeze
12
+ SRS_NAME_REGEX = /[^a-zA-Z0-9-]+/.freeze
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'file_helper'
4
+
5
+ module Dim
6
+ module Helpers
7
+ module AttributeHelper
8
+ include FileHelper
9
+
10
+ CHECK_SINGLE_ENUM = :check_single_enum
11
+ CHECK_MULTI_ENUM = :check_multi_enum
12
+ FORMAT_STYLES = {
13
+ 'list' => 'list',
14
+ 'multi' => 'multi',
15
+ 'single' => 'single',
16
+ 'split' => 'split',
17
+ 'text' => 'text'
18
+ }.freeze
19
+ @filepath = ''
20
+
21
+ def resolve_attributes(folder:, filename:)
22
+ @filepath = "#{folder}/#{filename}"
23
+ attributes = open_yml_file(folder, filename, allow_empty_file: true)
24
+ unless attributes
25
+ puts "Warning: empty file detected; skipped loading of #{filename}"
26
+ return
27
+ end
28
+
29
+ check_for_default_attributes(attributes, filename)
30
+
31
+ attributes.each do |attribute, attr_config|
32
+ attr_config.transform_keys!(&:to_sym)
33
+
34
+ change_type_to_format_style(attr_config)
35
+ validate_format_style(attribute, attr_config)
36
+ add_check_value(attr_config)
37
+ validate_default(attribute, attr_config)
38
+ validate_allowed(attribute, attr_config)
39
+ validate_allowed_for_enum(attribute, attr_config)
40
+ validate_default_for_enum(attribute, attr_config)
41
+
42
+ symbolize_values(attr_config)
43
+ end
44
+
45
+ attributes
46
+ end
47
+
48
+ def check_for_default_attributes(attributes, filename)
49
+ common_values = Requirement::SYNTAX.keys & attributes.keys
50
+ return if common_values.empty?
51
+
52
+ Dim::ExitHelper.exit(
53
+ code: 1,
54
+ filename: @filepath,
55
+ msg: 'Defining standard attributes as a custom attributes is not allowed; ' \
56
+ "#{common_values.join(',')} in #{filename}"
57
+ )
58
+ end
59
+
60
+ # TODO: change "format_style" to "type" in requirements syntax and then remove this conversion
61
+ def change_type_to_format_style(config)
62
+ config[:format_style] = config.delete(:type)
63
+ end
64
+
65
+ def validate_format_style(attribute, config)
66
+ return if FORMAT_STYLES.values.include?(config[:format_style])
67
+
68
+ exit_with_error(config: 'type', config_value: config[:format_style], attribute: attribute)
69
+ end
70
+
71
+ def add_check_value(config)
72
+ config[:check] = CHECK_SINGLE_ENUM if config[:format_style] == FORMAT_STYLES['single']
73
+ config[:check] = CHECK_MULTI_ENUM if config[:format_style] == FORMAT_STYLES['multi']
74
+ end
75
+
76
+ def validate_default(attribute, config)
77
+ return unless config[:default] == 'auto'
78
+
79
+ exit_with_error(config: 'default', config_value: config[:default], attribute: attribute)
80
+ end
81
+
82
+ def validate_allowed(attribute, config)
83
+ return if config[:allowed].nil? || config[:allowed].is_a?(Array)
84
+
85
+ exit_with_error(config: 'allowed', config_value: config[:allowed], attribute: attribute)
86
+ end
87
+
88
+ def validate_allowed_for_enum(attribute, config)
89
+ return unless FORMAT_STYLES.fetch_values('single', 'multi').include?(config[:format_style])
90
+
91
+ return if config[:allowed].is_a?(Array) && config[:allowed].map { |val| val.is_a?(String) }.all?
92
+
93
+ Dim::ExitHelper.exit(
94
+ code: 1,
95
+ filename: @filepath,
96
+ msg: "Allowed value must be list of strings; invalid allowed value for #{attribute}"
97
+ )
98
+ end
99
+
100
+ def validate_default_for_enum(attribute, config)
101
+ return unless FORMAT_STYLES.fetch_values('single', 'multi').include?(config[:format_style])
102
+
103
+ return if config[:allowed].include?(config[:default])
104
+
105
+ Dim::ExitHelper.exit(
106
+ code: 1,
107
+ filename: @filepath,
108
+ msg: "default value for #{attribute} must be from allowed list of #{config[:allowed]}"
109
+ )
110
+ end
111
+
112
+ def symbolize_values(config)
113
+ config[:format_style] = config[:format_style].to_sym if config[:format_style]
114
+ config[:format_shift] = config[:format_shift].to_i
115
+ config[:default] = '' unless config[:default]
116
+ end
117
+
118
+ private
119
+
120
+ def exit_with_error(config:, config_value:, attribute:)
121
+ msg = "Invalid value \"#{config_value}\" for #{config} detected for attribute #{attribute}"
122
+ Dim::ExitHelper.exit(code: 1, filename: @filepath, msg: msg)
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../exit_helper'
4
+
5
+ module Dim::Helpers
6
+ module FileHelper
7
+ def open_yml_file(folder, filename, allow_empty_file: false)
8
+ file_path = Pathname.new(File.join(folder, filename)).cleanpath.to_s
9
+ binary_data = File.binread(file_path).chomp
10
+ begin
11
+ data = YAML.parse(
12
+ binary_data.encode('utf-8', invalid: :replace, undef: :replace, replace: '?'),
13
+ filename: file_path
14
+ )
15
+ rescue Psych::SyntaxError => e
16
+ Dim::ExitHelper.exit(code: 1, filename: filename, msg: e.message)
17
+ end
18
+
19
+ Dim::ExitHelper.exit(code: 1, filename: filename, msg: 'not a valid yaml file') unless data || allow_empty_file
20
+ return unless data
21
+
22
+ data.to_ruby
23
+ end
24
+ end
25
+ end