csv_decision 0.4.1 → 0.5.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/CHANGELOG.md +4 -0
- data/README.md +43 -19
- data/csv_decision.gemspec +1 -1
- data/doc/CSVDecision.html +6 -6
- data/doc/CSVDecision/CellValidationError.html +1 -1
- data/doc/CSVDecision/Columns.html +124 -42
- data/doc/CSVDecision/Columns/Dictionary.html +101 -7
- data/doc/CSVDecision/Data.html +1 -1
- data/doc/CSVDecision/Decision.html +444 -98
- data/doc/CSVDecision/Defaults.html +1 -1
- data/doc/CSVDecision/Dictionary.html +4 -4
- data/doc/CSVDecision/Dictionary/Entry.html +31 -31
- data/doc/CSVDecision/Error.html +1 -1
- data/doc/CSVDecision/FileError.html +1 -1
- data/doc/CSVDecision/Header.html +2 -2
- data/doc/CSVDecision/Index.html +1 -1
- data/doc/CSVDecision/Input.html +129 -3
- data/doc/CSVDecision/Load.html +1 -1
- data/doc/CSVDecision/Matchers.html +168 -41
- data/doc/CSVDecision/Matchers/Constant.html +7 -7
- data/doc/CSVDecision/Matchers/Function.html +1 -1
- data/doc/CSVDecision/Matchers/Guard.html +16 -16
- data/doc/CSVDecision/Matchers/Matcher.html +13 -13
- data/doc/CSVDecision/Matchers/Numeric.html +8 -14
- data/doc/CSVDecision/Matchers/Pattern.html +10 -10
- data/doc/CSVDecision/Matchers/Proc.html +1 -1
- data/doc/CSVDecision/Matchers/Range.html +1 -1
- data/doc/CSVDecision/Matchers/Symbol.html +19 -29
- data/doc/CSVDecision/Options.html +1 -1
- data/doc/CSVDecision/Parse.html +4 -4
- data/doc/CSVDecision/Paths.html +742 -0
- data/doc/CSVDecision/Result.html +139 -70
- data/doc/CSVDecision/Scan.html +313 -0
- data/doc/CSVDecision/Scan/InputHashes.html +369 -0
- data/doc/CSVDecision/ScanRow.html +1 -1
- data/doc/CSVDecision/Table.html +134 -52
- data/doc/CSVDecision/TableValidationError.html +1 -1
- data/doc/CSVDecision/Validate.html +1 -1
- data/doc/_index.html +26 -5
- data/doc/class_list.html +1 -1
- data/doc/file.README.html +50 -28
- data/doc/index.html +50 -28
- data/doc/method_list.html +234 -98
- data/doc/top-level-namespace.html +1 -1
- data/lib/csv_decision.rb +3 -0
- data/lib/csv_decision/columns.rb +11 -0
- data/lib/csv_decision/decision.rb +82 -56
- data/lib/csv_decision/dictionary.rb +5 -1
- data/lib/csv_decision/header.rb +1 -1
- data/lib/csv_decision/input.rb +14 -11
- data/lib/csv_decision/parse.rb +6 -2
- data/lib/csv_decision/paths.rb +78 -0
- data/lib/csv_decision/result.rb +42 -35
- data/lib/csv_decision/scan.rb +116 -0
- data/lib/csv_decision/table.rb +18 -7
- data/lib/csv_decision/validate.rb +1 -1
- data/spec/csv_decision/columns_spec.rb +14 -0
- data/spec/csv_decision/decision_spec.rb +1 -3
- data/spec/csv_decision/examples_spec.rb +25 -0
- data/spec/csv_decision/table_spec.rb +87 -0
- metadata +7 -2
@@ -0,0 +1,116 @@
|
|
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
|
+
# Scan the input hash for all the paths specified in the decision table
|
9
|
+
# @api private
|
10
|
+
class Scan
|
11
|
+
# Main method for making decisions with a table that has paths.
|
12
|
+
#
|
13
|
+
# @param table [CSVDecision::Table] Decision table.
|
14
|
+
# @param input [Hash] Input hash (keys may or may not be symbolized)
|
15
|
+
# @param symbolize_keys [Boolean] Set to false if keys are symbolized and it's
|
16
|
+
# OK to mutate the input hash. Otherwise a copy of the input hash is symbolized.
|
17
|
+
# @return [Hash{Symbol=>Object}] Decision result.
|
18
|
+
def self.table(table:, input:, symbolize_keys:)
|
19
|
+
input = symbolize_keys ? input.deep_symbolize_keys : input
|
20
|
+
decision = Decision.new(table: table)
|
21
|
+
input_hashes = InputHashes.new
|
22
|
+
|
23
|
+
if table.options[:first_match]
|
24
|
+
scan_first_match(input: input, decision: decision, input_hashes: input_hashes)
|
25
|
+
else
|
26
|
+
scan_accumulate(input: input, decision: decision, input_hashes: input_hashes)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.scan_first_match(input:, decision:, input_hashes:)
|
31
|
+
decision.table.paths.each do |path, rows|
|
32
|
+
data = input_hashes.data(decision: decision, path: path, input: input)
|
33
|
+
next if data == {}
|
34
|
+
|
35
|
+
result = decision.index_scan_first_match(
|
36
|
+
scan_cols: data[:scan_cols],
|
37
|
+
hash: data[:hash],
|
38
|
+
index_rows: Array(rows)
|
39
|
+
)
|
40
|
+
return result if result != {}
|
41
|
+
end
|
42
|
+
|
43
|
+
{}
|
44
|
+
end
|
45
|
+
private_class_method :scan_first_match
|
46
|
+
|
47
|
+
def self.scan_accumulate(input:, decision:, input_hashes:)
|
48
|
+
# Final result
|
49
|
+
result = {}
|
50
|
+
|
51
|
+
decision.table.paths.each do |path, rows|
|
52
|
+
data = input_hashes.data(decision: decision, path: path, input: input)
|
53
|
+
next if data == {}
|
54
|
+
|
55
|
+
result = scan(rows: rows, input: data, final: result, decision: decision)
|
56
|
+
end
|
57
|
+
|
58
|
+
result
|
59
|
+
end
|
60
|
+
private_class_method :scan_accumulate
|
61
|
+
|
62
|
+
def self.scan(rows:, input:, final:, decision:)
|
63
|
+
# Note that +rows+ must be enclosed in an array for this method to work.
|
64
|
+
result = decision.index_scan_accumulate(scan_cols: input[:scan_cols],
|
65
|
+
hash: input[:hash],
|
66
|
+
index_rows: [rows])
|
67
|
+
|
68
|
+
# Accumulate this potentially multi-row result into the final result.
|
69
|
+
final = accumulate(final: final, result: result) if result.present?
|
70
|
+
|
71
|
+
final
|
72
|
+
end
|
73
|
+
private_class_method :scan
|
74
|
+
|
75
|
+
def self.accumulate(final:, result:)
|
76
|
+
return result if final == {}
|
77
|
+
|
78
|
+
final.each_pair { |key, value| final[key] = Array(value) + Array(result[key]) }
|
79
|
+
final
|
80
|
+
end
|
81
|
+
private_class_method :accumulate
|
82
|
+
|
83
|
+
# Derive the parsed input hash, using a cache for speed.
|
84
|
+
class InputHashes
|
85
|
+
def initialize
|
86
|
+
@input_hashes = {}
|
87
|
+
end
|
88
|
+
|
89
|
+
# @param path [Array<Symbol] Path for the input hash.
|
90
|
+
# @param input [Hash{Symbol=>Object}] Input hash.
|
91
|
+
# @return [Hash{Symbol=>Object}] Parsed input hash.
|
92
|
+
def data(decision:, path:, input:)
|
93
|
+
result = input(decision: decision, path: path, input: input)
|
94
|
+
|
95
|
+
decision.input(result) unless result == {}
|
96
|
+
|
97
|
+
result
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def input(decision:, path:, input:)
|
103
|
+
return @input_hashes[path] if @input_hashes.key?(path)
|
104
|
+
|
105
|
+
# Use the path - an array of symbol keys, to dig out the input sub-hash
|
106
|
+
hash = path.empty? ? input : input.dig(*path)
|
107
|
+
|
108
|
+
# Parse and transform the hash supplied as input
|
109
|
+
data = hash.blank? ? {} : Input.parse_data(table: decision.table, input: hash)
|
110
|
+
|
111
|
+
# Cache the parsed input hash data for this path
|
112
|
+
@input_hashes[path] = data
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
data/lib/csv_decision/table.rb
CHANGED
@@ -13,7 +13,7 @@ module CSVDecision
|
|
13
13
|
# @param input [Hash] Input hash.
|
14
14
|
# @return [{Symbol => Object, Array<Object>}] Decision hash.
|
15
15
|
def decide(input)
|
16
|
-
|
16
|
+
decision(input: input, symbolize_keys: true)
|
17
17
|
end
|
18
18
|
|
19
19
|
# Unsafe version of decide - may mutate the input hash and assumes the input
|
@@ -24,18 +24,22 @@ module CSVDecision
|
|
24
24
|
# Input hash will be mutated by any functions that have side effects.
|
25
25
|
# @return (see #decide)
|
26
26
|
def decide!(input)
|
27
|
-
|
27
|
+
decision( input: input, symbolize_keys: false)
|
28
28
|
end
|
29
29
|
|
30
30
|
# @return [CSVDecision::Columns] Dictionary of all input and output columns.
|
31
31
|
attr_accessor :columns
|
32
32
|
|
33
|
-
# @return [File, Pathname, nil] File path name if decision table was loaded from a
|
33
|
+
# @return [File, Pathname, nil] File path name if decision table was loaded from a
|
34
|
+
# CSV file.
|
34
35
|
attr_accessor :file
|
35
36
|
|
36
37
|
# @return [CSVDecision::Index] The index built on one or more input columns.
|
37
38
|
attr_accessor :index
|
38
39
|
|
40
|
+
# @return [CSVDecision::Path] The array of paths built on one or more input columns.
|
41
|
+
attr_accessor :paths
|
42
|
+
|
39
43
|
# @return [Hash] All options, explicitly set or defaulted, used to parse the table.
|
40
44
|
attr_accessor :options
|
41
45
|
|
@@ -77,14 +81,21 @@ module CSVDecision
|
|
77
81
|
|
78
82
|
# @api private
|
79
83
|
def initialize
|
80
|
-
@
|
81
|
-
@index = nil
|
82
|
-
@options = nil
|
83
|
-
@outs_functions = nil
|
84
|
+
@paths = []
|
84
85
|
@outs_rows = []
|
85
86
|
@if_rows = []
|
86
87
|
@rows = []
|
87
88
|
@scan_rows = []
|
88
89
|
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def decision(input:, symbolize_keys:)
|
94
|
+
if columns.paths.empty?
|
95
|
+
Decision.make(table: self, input: input, symbolize_keys: symbolize_keys)
|
96
|
+
else
|
97
|
+
Scan.table(table: self, input: input, symbolize_keys: symbolize_keys)
|
98
|
+
end
|
99
|
+
end
|
89
100
|
end
|
90
101
|
end
|
@@ -10,7 +10,7 @@ module CSVDecision
|
|
10
10
|
# @api private
|
11
11
|
module Validate
|
12
12
|
# These column types do not need a name.
|
13
|
-
COLUMN_TYPE_ANONYMOUS = Set.new(%i[guard if]).freeze
|
13
|
+
COLUMN_TYPE_ANONYMOUS = Set.new(%i[guard if path]).freeze
|
14
14
|
private_constant :COLUMN_TYPE_ANONYMOUS
|
15
15
|
|
16
16
|
# Validate a column header cell and return its type and name.
|
@@ -234,4 +234,18 @@ describe CSVDecision::Columns do
|
|
234
234
|
|
235
235
|
expect(table.columns.input_keys).to eq %i[country class CUSIP ISIN]
|
236
236
|
end
|
237
|
+
|
238
|
+
it 'recognises the path: columns' do
|
239
|
+
data = <<~DATA
|
240
|
+
path:, path:, in :type_cd, out :value, if:
|
241
|
+
header, , !nil?, :type_cd, :value.present?
|
242
|
+
payload, , !nil?, :type_cd, :value.present?
|
243
|
+
payload, ref_data, , :type_id, :value.present?
|
244
|
+
DATA
|
245
|
+
table = CSVDecision.parse(data)
|
246
|
+
|
247
|
+
expect(table.columns.input_keys).to eq %i[type_cd type_id]
|
248
|
+
expect(table.columns.paths[0].to_h).to eq(name: nil, eval: false, type: :path, set_if: nil)
|
249
|
+
expect(table.columns.paths[1].to_h).to eq(name: nil, eval: false, type: :path, set_if: nil)
|
250
|
+
end
|
237
251
|
end
|
@@ -12,9 +12,7 @@ describe CSVDecision::Decision do
|
|
12
12
|
|
13
13
|
table = CSVDecision.parse(data)
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
decision = CSVDecision::Decision.new(table: table, input: input)
|
15
|
+
decision = CSVDecision::Decision.new(table: table)
|
18
16
|
|
19
17
|
expect(decision).to be_a(CSVDecision::Decision)
|
20
18
|
end
|
@@ -214,4 +214,29 @@ context 'simple examples' do
|
|
214
214
|
expect(table.decide(ISIN: '123456789', country: 'GB', class: 'public')).to eq({})
|
215
215
|
expect(table.decide(ISIN: '123456789012', country: 'GB', class: 'private')).to eq(PAID: '123456789012', len: 12)
|
216
216
|
end
|
217
|
+
|
218
|
+
it 'scans the input hash paths accumulating matches' do
|
219
|
+
data = <<~DATA
|
220
|
+
path:, path:, out :value
|
221
|
+
header, , :source_name
|
222
|
+
header, metrics, :service_name
|
223
|
+
payload, , :amount
|
224
|
+
payload, ref_data, :account_id
|
225
|
+
DATA
|
226
|
+
table = CSVDecision.parse(data, first_match: false)
|
227
|
+
|
228
|
+
input = {
|
229
|
+
header: {
|
230
|
+
id: 1, type_cd: 'BUY', source_name: 'Client', client_name: 'AAPL',
|
231
|
+
metrics: { service_name: 'Trading', receive_time: '12:00' }
|
232
|
+
},
|
233
|
+
payload: {
|
234
|
+
tran_id: 9, amount: '100.00',
|
235
|
+
ref_data: { account_id: '5010', type_id: 'BUYL' }
|
236
|
+
}
|
237
|
+
}
|
238
|
+
|
239
|
+
expect(table.decide(input)).to eq(value: %w[Client Trading 100.00 5010])
|
240
|
+
expect(table.decide!(input)).to eq(value: %w[Client Trading 100.00 5010])
|
241
|
+
end
|
217
242
|
end
|
@@ -592,5 +592,92 @@ describe CSVDecision::Table do
|
|
592
592
|
end
|
593
593
|
end
|
594
594
|
end
|
595
|
+
|
596
|
+
it 'scans the input hash paths for a first match' do
|
597
|
+
data = <<~DATA
|
598
|
+
path:, path:, in :type_cd, out :value, if:
|
599
|
+
header, , !nil?, :type_cd, :value.present?
|
600
|
+
payload, , !nil?, :type_cd, :value.present?
|
601
|
+
payload, ref_data, , :type_id, :value.present?
|
602
|
+
DATA
|
603
|
+
table = CSVDecision.parse(data)
|
604
|
+
|
605
|
+
input = {
|
606
|
+
header: { id: 1, type_cd: 'BUY' },
|
607
|
+
payload: { tran_id: 9,
|
608
|
+
ref_data: { account_id: 5, type_id: 'BUYL' }
|
609
|
+
}
|
610
|
+
}
|
611
|
+
expect(table.decide(input)).to eq(value: 'BUY')
|
612
|
+
expect(table.decide!(input)).to eq(value: 'BUY')
|
613
|
+
|
614
|
+
input = {
|
615
|
+
header: { id: 1 },
|
616
|
+
payload: { tran_id: 9,
|
617
|
+
ref_data: { account_id: 5, type_id: 'BUYL' }
|
618
|
+
}
|
619
|
+
}
|
620
|
+
expect(table.decide(input)).to eq(value: 'BUYL')
|
621
|
+
expect(table.decide!(input)).to eq(value: 'BUYL')
|
622
|
+
|
623
|
+
input = {
|
624
|
+
payload: { tran_id: 9,
|
625
|
+
ref_data: { account_id: 5, type_id: 'BUYL' }
|
626
|
+
}
|
627
|
+
}
|
628
|
+
expect(table.decide(input)).to eq(value: 'BUYL')
|
629
|
+
|
630
|
+
input = {
|
631
|
+
payload: { tran_id: nil,
|
632
|
+
ref_data: { type_id: '' }
|
633
|
+
}
|
634
|
+
}
|
635
|
+
expect(table.decide(input)).to eq({})
|
636
|
+
end
|
637
|
+
|
638
|
+
it 'scans the input hash paths accumulating matches' do
|
639
|
+
data = <<~DATA
|
640
|
+
path:, path:, out :value, out :key, if:
|
641
|
+
header, , :source_name, source_nm, :value.present?
|
642
|
+
header, , :client_name, client_nm, :value.present?
|
643
|
+
header, , :client_ref, client_ref_id, :value.present?
|
644
|
+
header, metrics, :service_name, service_nm, :value.present?
|
645
|
+
payload, , :amount, trade_am, :value.present?
|
646
|
+
payload, ref_data, :account_id, account_id, :value.present?
|
647
|
+
header, metrics, :receive_time, receive_tm, :value.present?
|
648
|
+
DATA
|
649
|
+
table = CSVDecision.parse(data, first_match: false)
|
650
|
+
|
651
|
+
input = {
|
652
|
+
header: {
|
653
|
+
id: 1, type_cd: 'BUY', source_name: 'Client', client_name: 'AAPL', client_ref: 'A1',
|
654
|
+
metrics: { service_name: 'Trading', receive_time: '12:00' }
|
655
|
+
},
|
656
|
+
payload: { tran_id: 9,
|
657
|
+
amount: '100.00',
|
658
|
+
ref_data: { account_id: '5010', type_id: 'BUYL' }
|
659
|
+
}
|
660
|
+
}
|
661
|
+
result = { value: %w[Client AAPL A1 Trading 100.00 5010 12:00],
|
662
|
+
key: %w[source_nm client_nm client_ref_id service_nm trade_am account_id receive_tm] }
|
663
|
+
expect(table.decide(input)).to eq result
|
664
|
+
expect(table.decide!(input)).to eq result
|
665
|
+
|
666
|
+
input = {
|
667
|
+
header: {
|
668
|
+
id: 1, type_cd: 'BUY', source_name: 'Client', client_ref: 'A1',
|
669
|
+
metrics: { service_name: 'Trading' }
|
670
|
+
},
|
671
|
+
payload: { tran_id: 9,
|
672
|
+
amount: '100.00',
|
673
|
+
ref_data: { type_id: 'BUYL' }
|
674
|
+
}
|
675
|
+
}
|
676
|
+
result = { value: %w[Client A1 Trading 100.00],
|
677
|
+
key: %w[source_nm client_ref_id service_nm trade_am] }
|
678
|
+
|
679
|
+
expect(table.decide(input)).to eq result
|
680
|
+
expect(table.decide!(input)).to eq result
|
681
|
+
end
|
595
682
|
end
|
596
683
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: csv_decision
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brett Vickers
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-02-
|
11
|
+
date: 2018-02-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -203,7 +203,10 @@ files:
|
|
203
203
|
- doc/CSVDecision/Numeric.html
|
204
204
|
- doc/CSVDecision/Options.html
|
205
205
|
- doc/CSVDecision/Parse.html
|
206
|
+
- doc/CSVDecision/Paths.html
|
206
207
|
- doc/CSVDecision/Result.html
|
208
|
+
- doc/CSVDecision/Scan.html
|
209
|
+
- doc/CSVDecision/Scan/InputHashes.html
|
207
210
|
- doc/CSVDecision/ScanRow.html
|
208
211
|
- doc/CSVDecision/Symbol.html
|
209
212
|
- doc/CSVDecision/Table.html
|
@@ -243,7 +246,9 @@ files:
|
|
243
246
|
- lib/csv_decision/matchers/symbol.rb
|
244
247
|
- lib/csv_decision/options.rb
|
245
248
|
- lib/csv_decision/parse.rb
|
249
|
+
- lib/csv_decision/paths.rb
|
246
250
|
- lib/csv_decision/result.rb
|
251
|
+
- lib/csv_decision/scan.rb
|
247
252
|
- lib/csv_decision/scan_row.rb
|
248
253
|
- lib/csv_decision/table.rb
|
249
254
|
- lib/csv_decision/validate.rb
|