dim-toolkit 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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