csv_decision 0.0.8 → 0.0.9
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/.rubocop.yml +3 -0
- data/CHANGELOG.md +4 -0
- data/README.md +62 -28
- data/csv_decision.gemspec +1 -1
- data/doc/CSVDecision/CellValidationError.html +2 -2
- data/doc/CSVDecision/Columns/Dictionary.html +114 -20
- data/doc/CSVDecision/Columns/Entry.html +2 -2
- data/doc/CSVDecision/Columns.html +109 -27
- data/doc/CSVDecision/Data.html +2 -2
- data/doc/CSVDecision/Decide.html +2 -2
- data/doc/CSVDecision/Decision.html +21 -21
- data/doc/CSVDecision/Dictionary/Entry.html +508 -0
- data/doc/CSVDecision/Dictionary.html +265 -0
- data/doc/CSVDecision/Error.html +2 -2
- data/doc/CSVDecision/FileError.html +3 -3
- data/doc/CSVDecision/Header.html +37 -136
- data/doc/CSVDecision/Input.html +2 -2
- data/doc/CSVDecision/Load.html +2 -2
- data/doc/CSVDecision/Matchers/Constant.html +2 -2
- data/doc/CSVDecision/Matchers/Function.html +2 -2
- data/doc/CSVDecision/Matchers/Guard.html +92 -25
- data/doc/CSVDecision/Matchers/Matcher.html +14 -18
- data/doc/CSVDecision/Matchers/Numeric.html +2 -2
- data/doc/CSVDecision/Matchers/Pattern.html +2 -2
- data/doc/CSVDecision/Matchers/Range.html +2 -2
- data/doc/CSVDecision/Matchers/Symbol.html +2 -2
- data/doc/CSVDecision/Matchers.html +5 -5
- data/doc/CSVDecision/Options.html +2 -2
- data/doc/CSVDecision/Parse.html +6 -4
- data/doc/CSVDecision/Result.html +944 -0
- data/doc/CSVDecision/ScanRow.html +70 -80
- data/doc/CSVDecision/Table.html +134 -54
- data/doc/CSVDecision.html +5 -5
- data/doc/_index.html +18 -4
- data/doc/class_list.html +1 -1
- data/doc/file.README.html +132 -62
- data/doc/index.html +132 -62
- data/doc/method_list.html +156 -60
- data/doc/top-level-namespace.html +2 -2
- data/lib/csv_decision/columns.rb +1 -8
- data/lib/csv_decision/decision.rb +45 -96
- data/lib/csv_decision/dictionary.rb +149 -0
- data/lib/csv_decision/header.rb +6 -133
- data/lib/csv_decision/matchers.rb +1 -2
- data/lib/csv_decision/parse.rb +18 -7
- data/lib/csv_decision/result.rb +180 -0
- data/lib/csv_decision/scan_row.rb +13 -7
- data/lib/csv_decision/table.rb +6 -5
- data/lib/csv_decision.rb +3 -1
- data/spec/csv_decision/columns_spec.rb +25 -4
- data/spec/csv_decision/examples_spec.rb +25 -0
- data/spec/csv_decision/matchers/guard_spec.rb +26 -9
- data/spec/csv_decision/table_spec.rb +48 -2
- metadata +7 -2
@@ -11,26 +11,25 @@ module CSVDecision
|
|
11
11
|
# @param table [CSVDecision::Table] Decision table being processed.
|
12
12
|
# @param input [Hash{Symbol=>Object}] Input hash data structure.
|
13
13
|
def initialize(table:, input:)
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
@first_match = table.options[:first_match]
|
18
|
-
@outs = table.columns.outs
|
19
|
-
@outs_functions = table.outs_functions
|
20
|
-
|
21
|
-
# Partial result always includes the input hash for calculating output functions
|
22
|
-
@partial_result = input[:hash].dup if @outs_functions
|
14
|
+
# The result object is a hash of values, and each value will be an array if this is
|
15
|
+
# a multi-row result for the +first_match: false+ option.
|
16
|
+
@result = Result.new(table: table, input: input)
|
23
17
|
|
18
|
+
# All rows picked by the matching process. An array if +first_match: false+, otherwise
|
19
|
+
# a single row.
|
24
20
|
@rows_picked = []
|
21
|
+
|
22
|
+
# Relevant table attributes
|
23
|
+
table_attributes(table)
|
25
24
|
end
|
26
25
|
|
27
26
|
# Scan the decision table up against the input hash.
|
28
27
|
#
|
29
|
-
# @param
|
30
|
-
# @
|
31
|
-
# @return [self] Decision object built so far.
|
28
|
+
# @param (see #initialize)
|
29
|
+
# @return [{Symbol=>Object}] Decision result.
|
32
30
|
def scan(table:, input:)
|
33
31
|
table.each do |row, index|
|
32
|
+
# +row_scan+ returns false if more rows need to be scanned, truthy otherwise.
|
34
33
|
return result if row_scan(input: input, row: row, scan_row: table.scan_rows[index])
|
35
34
|
end
|
36
35
|
|
@@ -39,133 +38,83 @@ module CSVDecision
|
|
39
38
|
|
40
39
|
private
|
41
40
|
|
42
|
-
#
|
43
|
-
|
41
|
+
# Record the relevant table attributes.
|
42
|
+
def table_attributes(table)
|
43
|
+
@first_match = table.options[:first_match]
|
44
|
+
@outs = table.columns.outs
|
45
|
+
@outs_functions = table.outs_functions
|
46
|
+
end
|
47
|
+
|
48
|
+
# Derive the final result.
|
49
|
+
#
|
50
|
+
# @return [nil, Hash{Symbol=>Object}] Final result hash if matches found,
|
51
|
+
# otherwise the empty hash for no result.
|
44
52
|
def result
|
45
53
|
return {} if @rows_picked.blank?
|
46
|
-
@first_match ?
|
54
|
+
@first_match ? @result.attributes : accumulated_result
|
47
55
|
end
|
48
56
|
|
57
|
+
# Scan the row for matches against the input conditions.
|
49
58
|
def row_scan(input:, row:, scan_row:)
|
59
|
+
# +add+ returns false if more rows need to be scanned, truthy otherwise.
|
50
60
|
add(row) if Decide.matches?(row: row, input: input, scan_row: scan_row)
|
51
61
|
end
|
52
62
|
|
53
63
|
# Add a matched row to the decision object being built.
|
54
64
|
#
|
55
|
-
# @param row [Array]
|
65
|
+
# @param row [Array] Data row.
|
66
|
+
# @return [false, Hash]
|
56
67
|
def add(row)
|
57
68
|
return add_first_match(row) if @first_match
|
58
69
|
|
59
70
|
# Accumulate output rows
|
60
71
|
@rows_picked << row
|
61
|
-
@
|
72
|
+
@result.accumulate_outs(row)
|
62
73
|
|
63
74
|
# Not done
|
64
75
|
false
|
65
76
|
end
|
66
77
|
|
67
78
|
def accumulated_result
|
68
|
-
return
|
69
|
-
return eval_outs(@rows_picked.first) unless @multi_result
|
79
|
+
return @result.final unless @outs_functions
|
80
|
+
return @result.eval_outs(@rows_picked.first) unless @result.multi_result
|
70
81
|
|
71
82
|
multi_row_result
|
72
83
|
end
|
73
84
|
|
74
85
|
def multi_row_result
|
75
86
|
# Scan each output column that contains functions
|
76
|
-
@outs.each_pair
|
77
|
-
# Does this column have any functions defined?
|
78
|
-
next unless column.eval
|
79
|
-
|
80
|
-
eval_column_procs(col, column)
|
81
|
-
end
|
82
|
-
|
83
|
-
final_result
|
84
|
-
end
|
85
|
-
|
86
|
-
def accumulate_outs(column_name:, cell:)
|
87
|
-
case (current = @result[column_name])
|
88
|
-
when nil
|
89
|
-
@result[column_name] = cell
|
90
|
-
|
91
|
-
when Array
|
92
|
-
@result[column_name] << cell
|
87
|
+
@outs.each_pair { |col, column| eval_column_procs(col: col, column: column) if column.eval }
|
93
88
|
|
94
|
-
|
95
|
-
@result[column_name] = [current, cell]
|
96
|
-
@multi_result ||= true
|
97
|
-
end
|
89
|
+
@result.final
|
98
90
|
end
|
99
91
|
|
100
|
-
def eval_column_procs(col
|
92
|
+
def eval_column_procs(col:, column:)
|
101
93
|
@rows_picked.each_with_index do |row, index|
|
102
94
|
proc = row[col]
|
103
95
|
next unless proc.is_a?(Matchers::Proc)
|
104
96
|
|
105
97
|
# Evaluate the proc and update the result
|
106
|
-
eval_cell_proc(proc: proc, column_name: column.name, index: index)
|
98
|
+
@result.eval_cell_proc(proc: proc, column_name: column.name, index: index)
|
107
99
|
end
|
108
100
|
end
|
109
101
|
|
110
|
-
# Update the partial result calculated so far and call the function
|
111
|
-
def eval_cell_proc(proc:, column_name:, index:)
|
112
|
-
value = proc.function[partial_result(index)]
|
113
|
-
@multi_result ? @result[column_name][index] = value : @result[column_name] = value
|
114
|
-
end
|
115
|
-
|
116
|
-
def partial_result(index)
|
117
|
-
@result.each_pair do |column_name, value|
|
118
|
-
# Delete this column from the partial result in case there is data from a prior result row
|
119
|
-
next @partial_result.delete(column_name) if value[index].is_a?(Matchers::Proc)
|
120
|
-
@partial_result[column_name] = value[index]
|
121
|
-
end
|
122
|
-
|
123
|
-
@partial_result
|
124
|
-
end
|
125
|
-
|
126
|
-
def final_result
|
127
|
-
@result
|
128
|
-
end
|
129
|
-
|
130
102
|
def add_first_match(row)
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
# Common case is just copying output column values to the final result
|
136
|
-
@outs.each_pair { |col, column| @result[column.name] = row[col] }
|
137
|
-
end
|
138
|
-
|
139
|
-
def eval_outs(row)
|
140
|
-
# Set the constants first, in case the functions refer to them
|
141
|
-
eval_outs_constants(row)
|
142
|
-
|
143
|
-
# Then evaluate the functions, left to right
|
144
|
-
eval_outs_procs(row)
|
103
|
+
# This decision row may contain procs, which if present will need to be evaluated.
|
104
|
+
# If this row contains if: columns then this row may be filtered out, in which case
|
105
|
+
# this method call will return false.
|
106
|
+
return eval_single_row(row) if @outs_functions
|
145
107
|
|
146
|
-
|
108
|
+
# Common case is just copying output column values to the final result.
|
109
|
+
@rows_picked = row
|
110
|
+
@result.add_outs(row)
|
147
111
|
end
|
148
112
|
|
149
|
-
def
|
150
|
-
|
151
|
-
value = row[col]
|
152
|
-
next if value.is_a?(Matchers::Proc)
|
153
|
-
|
154
|
-
@partial_result[column.name] = value
|
155
|
-
@result[column.name] = value
|
156
|
-
end
|
157
|
-
end
|
113
|
+
def eval_single_row(row)
|
114
|
+
return false unless (result = @result.eval_outs(row))
|
158
115
|
|
159
|
-
|
160
|
-
|
161
|
-
proc = row[col]
|
162
|
-
next unless proc.is_a?(Matchers::Proc)
|
163
|
-
|
164
|
-
value = proc.function[@partial_result]
|
165
|
-
|
166
|
-
@partial_result[column.name] = value
|
167
|
-
@result[column.name] = value
|
168
|
-
end
|
116
|
+
@rows_picked = row
|
117
|
+
result
|
169
118
|
end
|
170
119
|
end
|
171
120
|
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# CSV Decision: CSV based Ruby decision tables.
|
4
|
+
# Created December 2017.
|
5
|
+
# @author Brett Vickers <brett@phillips-vickers.com>
|
6
|
+
# See LICENSE and README.md for details.
|
7
|
+
module CSVDecision
|
8
|
+
# Parse the CSV file's header row. These methods are only required at table load time.
|
9
|
+
# @api private
|
10
|
+
module Dictionary
|
11
|
+
# Table used to build a column dictionary entry.
|
12
|
+
ENTRY = {
|
13
|
+
in: { type: :in, eval: nil },
|
14
|
+
'in/text': { type: :in, eval: false },
|
15
|
+
out: { type: :out, eval: nil },
|
16
|
+
'out/text': { type: :out, eval: false },
|
17
|
+
guard: { type: :guard, eval: true },
|
18
|
+
if: { type: :if, eval: true }
|
19
|
+
}.freeze
|
20
|
+
private_constant :ENTRY
|
21
|
+
|
22
|
+
# Value object to hold column dictionary entries.
|
23
|
+
Entry = Struct.new(:name, :eval, :type) do
|
24
|
+
def ins?
|
25
|
+
%i[in guard].member?(type) ? true : false
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# TODO: implement all anonymous column types
|
30
|
+
# COLUMN_TYPE_ANONYMOUS = Set.new(%i[path if guard]).freeze
|
31
|
+
# These column types do not need a name
|
32
|
+
COLUMN_TYPE_ANONYMOUS = Set.new(%i[guard if]).freeze
|
33
|
+
private_constant :COLUMN_TYPE_ANONYMOUS
|
34
|
+
|
35
|
+
# Classify and build a dictionary of all input and output columns by
|
36
|
+
# parsing the header row.
|
37
|
+
#
|
38
|
+
# @param header [Array<String>] The header row after removing any empty columns.
|
39
|
+
# @return [Hash<Hash>] Column dictionary is a hash of hashes.
|
40
|
+
def self.build(header:, dictionary:)
|
41
|
+
header.each_with_index do |cell, index|
|
42
|
+
dictionary = parse_cell(cell: cell, index: index, dictionary: dictionary)
|
43
|
+
end
|
44
|
+
|
45
|
+
validate(dictionary)
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.validate(dictionary)
|
49
|
+
dictionary.outs.each_pair do |col, column|
|
50
|
+
validate_out(dictionary: dictionary, column_name: column.name, col: col)
|
51
|
+
end
|
52
|
+
|
53
|
+
dictionary
|
54
|
+
end
|
55
|
+
private_class_method :validate
|
56
|
+
|
57
|
+
def self.validate_out(dictionary:, column_name:, col:)
|
58
|
+
if dictionary.ins.any? { |_, column| column_name == column.name }
|
59
|
+
raise CellValidationError, "output column name '#{column_name}' is also an input column"
|
60
|
+
end
|
61
|
+
|
62
|
+
return unless dictionary.outs.any? { |key, column| column_name == column.name && col != key }
|
63
|
+
raise CellValidationError, "output column name '#{column_name}' is duplicated"
|
64
|
+
end
|
65
|
+
private_class_method :validate_out
|
66
|
+
|
67
|
+
def self.validate_column(cell:, index:)
|
68
|
+
match = Header::COLUMN_TYPE.match(cell)
|
69
|
+
raise CellValidationError, 'column name is not well formed' unless match
|
70
|
+
|
71
|
+
column_type = match['type']&.downcase&.to_sym
|
72
|
+
column_name = column_name(type: column_type, name: match['name'], index: index)
|
73
|
+
|
74
|
+
[column_type, column_name]
|
75
|
+
rescue CellValidationError => exp
|
76
|
+
raise CellValidationError, "header column '#{cell}' is not valid as the #{exp.message}"
|
77
|
+
end
|
78
|
+
private_class_method :validate_column
|
79
|
+
|
80
|
+
def self.column_name(type:, name:, index:)
|
81
|
+
# if: columns are named after their index, which is an integer and so cannot
|
82
|
+
# clash with other column name types, which are symbols.
|
83
|
+
return index if type == :if
|
84
|
+
|
85
|
+
return format_column_name(name) if name.present?
|
86
|
+
|
87
|
+
return if COLUMN_TYPE_ANONYMOUS.member?(type)
|
88
|
+
raise CellValidationError, 'column name is missing'
|
89
|
+
end
|
90
|
+
private_class_method :column_name
|
91
|
+
|
92
|
+
def self.format_column_name(name)
|
93
|
+
column_name = name.strip.tr("\s", '_')
|
94
|
+
|
95
|
+
return column_name.to_sym if Header::COLUMN_NAME_RE.match(column_name)
|
96
|
+
raise CellValidationError, "column name '#{name}' contains invalid characters"
|
97
|
+
end
|
98
|
+
private_class_method :format_column_name
|
99
|
+
|
100
|
+
# Returns the normalized column type, along with an indication if
|
101
|
+
# the column requires evaluation
|
102
|
+
def self.column_type(column_name, entry)
|
103
|
+
Entry.new(column_name, entry[:eval], entry[:type])
|
104
|
+
end
|
105
|
+
private_class_method :column_type
|
106
|
+
|
107
|
+
def self.parse_cell(cell:, index:, dictionary:)
|
108
|
+
column_type, column_name = validate_column(cell: cell, index: index)
|
109
|
+
|
110
|
+
entry = column_type(column_name, ENTRY[column_type])
|
111
|
+
|
112
|
+
dictionary_entry(dictionary: dictionary, entry: entry, index: index)
|
113
|
+
end
|
114
|
+
private_class_method :parse_cell
|
115
|
+
|
116
|
+
def self.dictionary_entry(dictionary:, entry:, index:)
|
117
|
+
case entry.type
|
118
|
+
# Header column that has a function for setting the value (planned feature)
|
119
|
+
# when :set, :'set/nil?', :'set/blank?'
|
120
|
+
# # Default function will set the input value unconditionally or conditionally
|
121
|
+
# dictionary.defaults[index] =
|
122
|
+
# Columns::Default.new(entry.name, nil, default_if(type))
|
123
|
+
#
|
124
|
+
# # Treat set: as an in: column
|
125
|
+
# dictionary.ins[index] = entry
|
126
|
+
|
127
|
+
when :in, :guard
|
128
|
+
dictionary.ins[index] = entry
|
129
|
+
|
130
|
+
when :out
|
131
|
+
dictionary.outs[index] = entry
|
132
|
+
|
133
|
+
when :if
|
134
|
+
dictionary.outs[index] = entry
|
135
|
+
dictionary.ifs[index] = entry
|
136
|
+
end
|
137
|
+
|
138
|
+
dictionary
|
139
|
+
end
|
140
|
+
private_class_method :dictionary_entry
|
141
|
+
|
142
|
+
# def self.default_if(type)
|
143
|
+
# return nil if type == :set
|
144
|
+
# return :nil? if type == :'set/nil'
|
145
|
+
# :blank?
|
146
|
+
# end
|
147
|
+
# private_class_method :default_if
|
148
|
+
end
|
149
|
+
end
|
data/lib/csv_decision/header.rb
CHANGED
@@ -20,37 +20,20 @@ module CSVDecision
|
|
20
20
|
\s*:\s*(?<name>\S?.*)\z
|
21
21
|
}xi
|
22
22
|
|
23
|
-
COLUMN_ENTRY = {
|
24
|
-
in: { type: :in, eval: nil },
|
25
|
-
'in/text': { type: :in, eval: false },
|
26
|
-
out: { type: :out, eval: nil },
|
27
|
-
'out/text': { type: :out, eval: false },
|
28
|
-
guard: { type: :guard, eval: true },
|
29
|
-
if: { type: :if, eval: true }
|
30
|
-
}.freeze
|
31
|
-
private_constant :COLUMN_ENTRY
|
32
|
-
|
33
|
-
# TODO: implement all anonymous column types
|
34
|
-
# COLUMN_TYPE_ANONYMOUS = Set.new(%i[path if guard]).freeze
|
35
|
-
# These column types do not need a name
|
36
|
-
COLUMN_TYPE_ANONYMOUS = Set.new(%i[guard]).freeze
|
37
|
-
private_constant :COLUMN_TYPE_ANONYMOUS
|
38
|
-
|
39
23
|
# Regular expression string for a column name.
|
40
24
|
# More lenient than a Ruby method name - note any spaces will have been replaced with
|
41
25
|
# underscores.
|
42
26
|
COLUMN_NAME = "\\w[\\w:/!?]*"
|
43
27
|
|
44
|
-
#
|
45
|
-
COLUMN_NAME_RE = Matchers.regexp(COLUMN_NAME)
|
46
|
-
private_constant :COLUMN_NAME_RE
|
28
|
+
# Regular expression for matching a column name.
|
29
|
+
COLUMN_NAME_RE = Matchers.regexp(Header::COLUMN_NAME)
|
47
30
|
|
48
31
|
# Check if the given row contains a recognisable header cell.
|
49
32
|
#
|
50
33
|
# @param row [Array<String>] Header row.
|
51
34
|
# @return [Boolean] Return true if the row looks like a header.
|
52
35
|
def self.row?(row)
|
53
|
-
row.
|
36
|
+
row.any? { |cell| cell.match(COLUMN_TYPE) }
|
54
37
|
end
|
55
38
|
|
56
39
|
# Strip empty columns from all data rows.
|
@@ -66,52 +49,9 @@ module CSVDecision
|
|
66
49
|
rows.shift
|
67
50
|
end
|
68
51
|
|
69
|
-
#
|
70
|
-
#
|
71
|
-
# @
|
72
|
-
# @return [Hash<Hash>] Column dictionary is a hash of hashes.
|
73
|
-
def self.dictionary(row:)
|
74
|
-
dictionary = Columns::Dictionary.new
|
75
|
-
|
76
|
-
row.each_with_index do |cell, index|
|
77
|
-
dictionary = parse_cell(cell: cell, index: index, dictionary: dictionary)
|
78
|
-
end
|
79
|
-
|
80
|
-
validate(dictionary: dictionary)
|
81
|
-
end
|
82
|
-
|
83
|
-
def self.validate(dictionary:)
|
84
|
-
dictionary.outs.each_value do |column|
|
85
|
-
next unless input_column?(dictionary: dictionary, column_name: column.name)
|
86
|
-
|
87
|
-
raise CellValidationError, "output column name '#{column.name}' is also an input column"
|
88
|
-
end
|
89
|
-
|
90
|
-
dictionary
|
91
|
-
end
|
92
|
-
private_class_method :validate
|
93
|
-
|
94
|
-
def self.input_column?(dictionary:, column_name:)
|
95
|
-
dictionary.ins.each_value { |column| return true if column_name == column.name }
|
96
|
-
|
97
|
-
false
|
98
|
-
end
|
99
|
-
private_class_method :input_column?
|
100
|
-
|
101
|
-
def self.validate_column(cell:)
|
102
|
-
match = COLUMN_TYPE.match(cell)
|
103
|
-
raise CellValidationError, 'column name is not well formed' unless match
|
104
|
-
|
105
|
-
column_type = match['type']&.downcase&.to_sym
|
106
|
-
column_name = column_name(type: column_type, name: match['name'])
|
107
|
-
|
108
|
-
[column_type, column_name]
|
109
|
-
rescue CellValidationError => exp
|
110
|
-
raise CellValidationError, "header column '#{cell}' is not valid as the #{exp.message}"
|
111
|
-
end
|
112
|
-
private_class_method :validate_column
|
113
|
-
|
114
|
-
# Array of all empty column indices.
|
52
|
+
# Build an array of all empty column indices.
|
53
|
+
# @param row [Array]
|
54
|
+
# @return [false, Array<Integer>]
|
115
55
|
def self.empty_columns?(row:)
|
116
56
|
result = []
|
117
57
|
row&.each_with_index { |cell, index| result << index if cell == '' }
|
@@ -119,72 +59,5 @@ module CSVDecision
|
|
119
59
|
result.empty? ? false : result
|
120
60
|
end
|
121
61
|
private_class_method :empty_columns?
|
122
|
-
|
123
|
-
def self.column_name(type:, name:)
|
124
|
-
return format_column_name(name) if name.present?
|
125
|
-
|
126
|
-
return if COLUMN_TYPE_ANONYMOUS.member?(type)
|
127
|
-
|
128
|
-
raise CellValidationError, 'column name is missing'
|
129
|
-
end
|
130
|
-
private_class_method :column_name
|
131
|
-
|
132
|
-
def self.format_column_name(name)
|
133
|
-
column_name = name.strip.tr("\s", '_')
|
134
|
-
|
135
|
-
return column_name.to_sym if COLUMN_NAME_RE.match(column_name)
|
136
|
-
|
137
|
-
raise CellValidationError, "column name '#{name}' contains invalid characters"
|
138
|
-
end
|
139
|
-
private_class_method :format_column_name
|
140
|
-
|
141
|
-
# Returns the normalized column type, along with an indication if
|
142
|
-
# the column requires evaluation
|
143
|
-
def self.column_type(column_name, type)
|
144
|
-
entry = COLUMN_ENTRY[type]
|
145
|
-
Columns::Entry.new(column_name, entry[:eval], entry[:type])
|
146
|
-
end
|
147
|
-
private_class_method :column_type
|
148
|
-
|
149
|
-
def self.parse_cell(cell:, index:, dictionary:)
|
150
|
-
column_type, column_name = validate_column(cell: cell)
|
151
|
-
|
152
|
-
entry = column_type(column_name, column_type)
|
153
|
-
|
154
|
-
dictionary_entry(dictionary: dictionary,
|
155
|
-
type: entry.type,
|
156
|
-
entry: entry,
|
157
|
-
index: index)
|
158
|
-
end
|
159
|
-
private_class_method :parse_cell
|
160
|
-
|
161
|
-
def self.dictionary_entry(dictionary:, type:, entry:, index:)
|
162
|
-
case type
|
163
|
-
# Header column that has a function for setting the value (planned feature)
|
164
|
-
# when :set, :'set/nil', :'set/blank'
|
165
|
-
# # Default function will set the input value unconditionally or conditionally
|
166
|
-
# dictionary.defaults[index] =
|
167
|
-
# Columns::Default.new(entry.name, nil, default_if(type))
|
168
|
-
#
|
169
|
-
# # Treat set: as an in: column
|
170
|
-
# dictionary.ins[index] = entry
|
171
|
-
|
172
|
-
when :in, :guard
|
173
|
-
dictionary.ins[index] = entry
|
174
|
-
|
175
|
-
when :out
|
176
|
-
dictionary.outs[index] = entry
|
177
|
-
end
|
178
|
-
|
179
|
-
dictionary
|
180
|
-
end
|
181
|
-
private_class_method :dictionary_entry
|
182
|
-
|
183
|
-
# def self.default_if(type)
|
184
|
-
# return nil if type == :set
|
185
|
-
# return :nil? if type == :'set/nil'
|
186
|
-
# :blank?
|
187
|
-
# end
|
188
|
-
# private_class_method :default_if
|
189
62
|
end
|
190
63
|
end
|
@@ -124,8 +124,7 @@ module CSVDecision
|
|
124
124
|
Matchers.parse(columns: columns, matchers: @outs, row: row)
|
125
125
|
end
|
126
126
|
|
127
|
-
#
|
128
|
-
# a custom Matcher class.
|
127
|
+
# Subclass and override {#matches?} to implement a custom Matcher class.
|
129
128
|
class Matcher
|
130
129
|
def initialize(_options = nil); end
|
131
130
|
|
data/lib/csv_decision/parse.rb
CHANGED
@@ -14,7 +14,7 @@ module CSVDecision
|
|
14
14
|
# Error validating a cell when parsing input table data.
|
15
15
|
class CellValidationError < Error; end
|
16
16
|
|
17
|
-
# Table parsing error message enhanced to include the file being processed
|
17
|
+
# Table parsing error message enhanced to include the file being processed.
|
18
18
|
class FileError < Error; end
|
19
19
|
|
20
20
|
# Builds a decision table from the input data - which may either be a file, CSV string
|
@@ -62,6 +62,7 @@ module CSVDecision
|
|
62
62
|
|
63
63
|
parse_table(table: table, input: data, options: options)
|
64
64
|
|
65
|
+
# The table object is now immutable.
|
65
66
|
table.columns.deep_freeze
|
66
67
|
table.freeze
|
67
68
|
rescue CSVDecision::Error => exp
|
@@ -93,25 +94,35 @@ module CSVDecision
|
|
93
94
|
|
94
95
|
def self.parse_data(table:, matchers:)
|
95
96
|
table.rows.each_with_index do |row, index|
|
96
|
-
row
|
97
|
-
|
97
|
+
# Mutate the row if we find anything other than a simple string constant in its
|
98
|
+
# data cells.
|
99
|
+
row = parse_row(table: table, matchers: matchers, row: row, index: index)
|
98
100
|
|
99
|
-
# Does the
|
101
|
+
# Does the row have any output functions?
|
100
102
|
outs_functions(table: table, index: index)
|
101
103
|
|
104
|
+
# No more mutations required for this row.
|
102
105
|
row.freeze
|
103
106
|
end
|
104
107
|
end
|
105
108
|
private_class_method :parse_data
|
106
109
|
|
110
|
+
def self.parse_row(table:, matchers:, row:, index:)
|
111
|
+
row, table.scan_rows[index] = matchers.parse_ins(columns: table.columns.ins, row: row)
|
112
|
+
row, table.outs_rows[index] = matchers.parse_outs(columns: table.columns.outs, row: row)
|
113
|
+
|
114
|
+
row
|
115
|
+
end
|
116
|
+
private_class_method :parse_row
|
117
|
+
|
107
118
|
def self.outs_functions(table:, index:)
|
108
119
|
return if table.outs_rows[index].procs.empty?
|
109
120
|
|
110
121
|
# Set this flag as the table has output functions
|
111
|
-
table.outs_functions
|
122
|
+
table.outs_functions = true
|
112
123
|
|
113
|
-
|
114
|
-
table.outs_rows[index].procs.each { |col| outs[col].eval = true }
|
124
|
+
# Update the output columns that contain functions needing evaluation.
|
125
|
+
table.outs_rows[index].procs.each { |col| table.columns.outs[col].eval = true }
|
115
126
|
end
|
116
127
|
private_class_method :outs_functions
|
117
128
|
end
|