casegen 2.0.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/.rubocop.yml +109 -0
- data/.ruby-version +1 -1
- data/Gemfile +3 -1
- data/Gemfile.lock +51 -6
- data/README.md +10 -119
- data/Rakefile +9 -7
- data/bin/casegen +2 -1
- data/casegen.gemspec +13 -9
- data/doc/bounding_box.rb +37 -0
- data/doc/cart.rb +43 -0
- data/doc/expect_only.rb +28 -0
- data/doc/pricing.rb +50 -0
- data/doc/ruby_array.rb +41 -0
- data/lib/case_gen/combination.rb +38 -0
- data/lib/case_gen/combo_matcher.rb +15 -0
- data/lib/case_gen/exclude_rule.rb +50 -0
- data/lib/case_gen/expect_rule.rb +24 -0
- data/lib/case_gen/generator.rb +40 -0
- data/lib/case_gen/output/exclude.rb +6 -0
- data/lib/case_gen/output/exclude_as_table.rb +13 -0
- data/lib/case_gen/output/exclude_as_text.rb +12 -0
- data/lib/case_gen/output/exclude_inline.rb +13 -0
- data/lib/case_gen/output/exclude_inline_footnotes.rb +20 -0
- data/lib/case_gen/output.rb +66 -0
- data/lib/case_gen/rule_description.rb +11 -0
- data/lib/case_gen/set.rb +16 -0
- data/lib/casegen.rb +15 -183
- data/spec/cart_sample_spec.rb +46 -0
- data/spec/case_gen/combination_spec.rb +11 -0
- data/spec/case_gen/exclude_rule_spec.rb +17 -0
- data/spec/exclude_as_table_spec.rb +39 -0
- data/spec/exclude_as_text_spec.rb +58 -0
- data/spec/exclude_inline_footnotes_spec.rb +58 -0
- data/spec/exclude_inline_spec.rb +50 -0
- data/spec/expect_only_spec.rb +30 -0
- data/spec/spec_helper.rb +113 -0
- metadata +101 -35
- data/.idea/encodings.xml +0 -5
- data/.idea/misc.xml +0 -5
- data/.idea/modules.xml +0 -9
- data/.idea/vcs.xml +0 -7
- data/doc/calc.sample.txt +0 -13
- data/doc/cart.sample.rb +0 -3
- data/doc/cart.sample.txt +0 -33
- data/doc/ruby_array.sample.rb +0 -26
- data/lib/agents/sets/enum/by.rb +0 -244
- data/lib/agents/sets/enum/cluster.rb +0 -164
- data/lib/agents/sets/enum/inject.rb +0 -50
- data/lib/agents/sets/enum/nest.rb +0 -117
- data/lib/agents/sets/enum/op.rb +0 -283
- data/lib/agents/sets/enum/pipe.rb +0 -160
- data/lib/agents/sets/enum/tree.rb +0 -442
- data/lib/agents/sets.rb +0 -313
- data/test/agents/console_output_test.rb +0 -27
- data/test/agents/sets.test.rb +0 -227
- data/test/agents_test.rb +0 -41
- data/test/casegen.tests.rb +0 -0
- data/test/parser_test.rb +0 -163
- data/test/test_helper.rb +0 -2
data/doc/cart.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../lib/casegen'
|
4
|
+
|
5
|
+
sets = {
|
6
|
+
payment: ['Credit', 'Check', 'Online Bank'],
|
7
|
+
amount: [100, 1_000, 10_000],
|
8
|
+
shipping: ['Ground', 'Air'],
|
9
|
+
ship_to_country: ['US', 'Outside US'],
|
10
|
+
bill_to_country: ['US', 'Outside US']
|
11
|
+
}
|
12
|
+
|
13
|
+
rules = {
|
14
|
+
exclude: [
|
15
|
+
{
|
16
|
+
criteria: %(shipping == "Ground" && ship_to_country == "Outside US" ),
|
17
|
+
description: 'Our ground shipper will only ship things within the US',
|
18
|
+
},
|
19
|
+
{
|
20
|
+
criteria: -> { payment == 'Check' && bill_to_country == 'Outside US' },
|
21
|
+
description: 'Our bank will not accept checks written from banks outside the US.',
|
22
|
+
},
|
23
|
+
{
|
24
|
+
criteria: -> { payment == 'Online Bank' && amount >= 1_000 },
|
25
|
+
description: <<~_,
|
26
|
+
While the online bank will process amounts > $1,000, we've experienced
|
27
|
+
occasional problems with their services and have had to write off some
|
28
|
+
transactions, so we no longer allow this payment option for amounts
|
29
|
+
greater than $1,000.
|
30
|
+
_
|
31
|
+
},
|
32
|
+
{
|
33
|
+
criteria: -> { ship_to_country == 'US' && bill_to_country == 'Outside US' },
|
34
|
+
description: "If we're shipping to the US, billing party cannot be outside US"
|
35
|
+
},
|
36
|
+
]
|
37
|
+
}
|
38
|
+
|
39
|
+
if __FILE__ == $PROGRAM_NAME
|
40
|
+
puts CaseGen.generate(sets, rules, :exclude_as_text).to_s
|
41
|
+
else
|
42
|
+
Fixtures.add(:cart, sets, rules)
|
43
|
+
end
|
data/doc/expect_only.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../lib/casegen'
|
4
|
+
|
5
|
+
sets = {
|
6
|
+
duration: [12, 24, 36, 60],
|
7
|
+
unit: [60, 3600],
|
8
|
+
result: [:expect]
|
9
|
+
}
|
10
|
+
|
11
|
+
rules = {
|
12
|
+
expect: [
|
13
|
+
{duration: 12, unit: 60, result: '12m'},
|
14
|
+
{duration: 12, unit: 3600, result: '12h'},
|
15
|
+
{duration: 24, unit: 60, result: '24m'},
|
16
|
+
{duration: 24, unit: 3600, result: '1d'},
|
17
|
+
{duration: 36, unit: 60, result: '36m'},
|
18
|
+
{duration: 36, unit: 3600, result: '1d 12h'},
|
19
|
+
{duration: 60, unit: 60, result: '1h'},
|
20
|
+
{duration: 60, unit: 3600, result: '2d 12h'},
|
21
|
+
]
|
22
|
+
}
|
23
|
+
|
24
|
+
if __FILE__ == $PROGRAM_NAME
|
25
|
+
puts CaseGen.generate(sets, rules, :exclude_inline).to_s
|
26
|
+
else
|
27
|
+
Fixtures.add(:duration, sets, rules)
|
28
|
+
end
|
data/doc/pricing.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../lib/casegen'
|
4
|
+
|
5
|
+
sets = {
|
6
|
+
subtotal: [25, 75, 200],
|
7
|
+
discount: %w[0% 10% 20%],
|
8
|
+
promo: %w[none apr fall],
|
9
|
+
total: [:expect]
|
10
|
+
}
|
11
|
+
|
12
|
+
rules = {
|
13
|
+
exclude: [
|
14
|
+
{
|
15
|
+
criteria: %(subtotal < 100 && discount == '20%'),
|
16
|
+
description: 'Total must be above $100 to apply the 20% discount',
|
17
|
+
},
|
18
|
+
{
|
19
|
+
criteria: %((subtotal < 50) && discount == '10%'),
|
20
|
+
note: 'Total must be above $50 to apply the 10% discount',
|
21
|
+
},
|
22
|
+
{
|
23
|
+
criteria: %(discount != '20%' && subtotal == 200),
|
24
|
+
reason: 'Orders over 100 automatically get 20% discount',
|
25
|
+
},
|
26
|
+
{
|
27
|
+
criteria: %(discount != '10%' && subtotal == 75),
|
28
|
+
reason: 'Orders between 50 and 100 automatically get 10% discount',
|
29
|
+
},
|
30
|
+
{
|
31
|
+
criteria: %(discount == '20%' && promo != 'none'),
|
32
|
+
reason: '20% discount cannot be combined with promo',
|
33
|
+
},
|
34
|
+
],
|
35
|
+
expect: [
|
36
|
+
{subtotal: 25, promo: 'none', total: '25.00'},
|
37
|
+
{subtotal: 25, promo: 'apr', total: '17.50', reason: 'apr promo is 30%'},
|
38
|
+
{subtotal: 25, promo: 'fall', total: '16.25', note: 'fall promo is 35%'},
|
39
|
+
{subtotal: 75, promo: 'none', total: '67.50', note: '10% discount'},
|
40
|
+
{subtotal: 75, promo: 'apr', total: '47.25', reason: '10% + apr promo is 30%'},
|
41
|
+
{subtotal: 75, promo: 'fall', total: '43.88', note: '10% + fall promo is 35%'},
|
42
|
+
{subtotal: 200, promo: 'none', total: '160.00', note: '20% discount'},
|
43
|
+
]
|
44
|
+
}
|
45
|
+
|
46
|
+
if __FILE__ == $PROGRAM_NAME
|
47
|
+
puts CaseGen.generate(sets, rules, :exclude_as_text)
|
48
|
+
else
|
49
|
+
Fixtures.add(:pricing, sets, rules)
|
50
|
+
end
|
data/doc/ruby_array.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../lib/case_gen'
|
4
|
+
|
5
|
+
raise "This doesn't currently work. Do I even want to keep it?"
|
6
|
+
|
7
|
+
# CLabs::CaseGen::CaseGen.new(DATA.read)
|
8
|
+
|
9
|
+
# Outputs
|
10
|
+
#
|
11
|
+
# DataSubmitCase = Struct.new(:role, :authorization_code, :submit_enabled)
|
12
|
+
#
|
13
|
+
# cases = [DataSubmitCase.new("admin", "none", "true"),
|
14
|
+
# DataSubmitCase.new("admin", "invalid", "true"),
|
15
|
+
# DataSubmitCase.new("admin", "valid", "true"),
|
16
|
+
# DataSubmitCase.new("standard", "none", "false"),
|
17
|
+
# DataSubmitCase.new("standard", "invalid", "false"),
|
18
|
+
# DataSubmitCase.new("standard", "valid", "true")]
|
19
|
+
|
20
|
+
__END__
|
21
|
+
|
22
|
+
sets
|
23
|
+
----
|
24
|
+
role: admin, standard
|
25
|
+
authorization code: none, invalid, valid
|
26
|
+
submit enabled: true, false
|
27
|
+
|
28
|
+
rules(sets)
|
29
|
+
-----------
|
30
|
+
exclude role = admin AND submit enabled = false
|
31
|
+
Admin role can always submit
|
32
|
+
|
33
|
+
exclude role = standard AND authorization code = none AND submit enabled = true
|
34
|
+
exclude role = standard AND authorization code = invalid AND submit enabled = true
|
35
|
+
exclude role = standard AND authorization code = valid AND submit enabled = false
|
36
|
+
Standard role can only submit when authorization code is valid
|
37
|
+
|
38
|
+
|
39
|
+
ruby_array(rules)
|
40
|
+
-------------
|
41
|
+
DataSubmitCase
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CaseGen
|
4
|
+
class Combination
|
5
|
+
attr_reader :names, :excluded_by_rule
|
6
|
+
|
7
|
+
def initialize(hash_pairs)
|
8
|
+
@names = hash_pairs.map do |h|
|
9
|
+
k = h.first.first
|
10
|
+
v = h.first.last
|
11
|
+
append(k, v)
|
12
|
+
k
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def hash_row
|
17
|
+
{}.tap do |h|
|
18
|
+
@names.each do |ivar|
|
19
|
+
h[ivar] = instance_variable_get("@#{ivar}")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def append(key, value)
|
25
|
+
@names << key if defined?(@names)
|
26
|
+
instance_variable_set("@#{key}", value)
|
27
|
+
self.class.attr_accessor key
|
28
|
+
end
|
29
|
+
|
30
|
+
def exclude_with(rule)
|
31
|
+
@excluded_by_rule = rule
|
32
|
+
end
|
33
|
+
|
34
|
+
def excluded?
|
35
|
+
!@excluded_by_rule.nil?
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CaseGen
|
4
|
+
module ComboMatcher
|
5
|
+
def matches_criteria(combo, additional_ignore_keys = [])
|
6
|
+
criteria_keys = (@rule_data.keys - additional_ignore_keys) - ignore_keys
|
7
|
+
criteria = @rule_data.slice(*criteria_keys)
|
8
|
+
criteria == combo.hash_row.slice(*criteria_keys)
|
9
|
+
end
|
10
|
+
|
11
|
+
def ignore_keys
|
12
|
+
%i[description reason note index]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CaseGen
|
4
|
+
class ExcludeRule
|
5
|
+
include ComboMatcher
|
6
|
+
include RuleDescription
|
7
|
+
|
8
|
+
attr_reader :rule_data, :description
|
9
|
+
|
10
|
+
def initialize(rule_data)
|
11
|
+
@rule_data = rule_data
|
12
|
+
@description = rule_description(rule_data)
|
13
|
+
@criteria = rule_data[:criteria]
|
14
|
+
end
|
15
|
+
|
16
|
+
def apply(combos)
|
17
|
+
matches = combos.select do |combo|
|
18
|
+
case @criteria
|
19
|
+
when String
|
20
|
+
combo.instance_eval(@criteria)
|
21
|
+
when Proc
|
22
|
+
combo.instance_exec(&@criteria)
|
23
|
+
when nil
|
24
|
+
# if the rule data has keys matching the combo, then compare the
|
25
|
+
# values of provided keys.
|
26
|
+
matches_criteria(combo)
|
27
|
+
else
|
28
|
+
raise "Unknown rule criteria class: #{@criteria.class}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
process_matches(combos, matches)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def process_matches(combos, matches)
|
38
|
+
combos.each do |combo|
|
39
|
+
next unless matches.include?(combo)
|
40
|
+
next if combo.excluded?
|
41
|
+
|
42
|
+
combo.exclude_with(self)
|
43
|
+
expect_keys = combo.names.select { |name| combo.send(name) == :expect }
|
44
|
+
expect_keys.each do |expect_key|
|
45
|
+
combo.send("#{expect_key}=", '')
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CaseGen
|
4
|
+
class ExpectRule
|
5
|
+
include ComboMatcher
|
6
|
+
|
7
|
+
def initialize(rule_data)
|
8
|
+
@rule_data = rule_data
|
9
|
+
end
|
10
|
+
|
11
|
+
def apply(combos)
|
12
|
+
combos.each do |combo|
|
13
|
+
expect_keys = combo.names.select { |name| combo.send(name) == :expect }
|
14
|
+
next if expect_keys.none?
|
15
|
+
|
16
|
+
next unless matches_criteria(combo, expect_keys)
|
17
|
+
|
18
|
+
expect_keys.each do |expect_key|
|
19
|
+
combo.send("#{expect_key}=", @rule_data[expect_key])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'tablesmith'
|
4
|
+
|
5
|
+
module CaseGen
|
6
|
+
class Generator
|
7
|
+
attr_reader :sets, :rules, :combos, :exclusions
|
8
|
+
|
9
|
+
def initialize(sets, rules)
|
10
|
+
@sets = sets.map do |title, values|
|
11
|
+
CaseGen::Set.new(title, values)
|
12
|
+
end
|
13
|
+
@rules = rules
|
14
|
+
@combos = generate_combinations
|
15
|
+
apply_rules
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def generate_combinations
|
21
|
+
hash_pairs = @sets.map(&:hash_pairs)
|
22
|
+
product_of(hash_pairs).map { |c| Combination.new(c) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def product_of(sets)
|
26
|
+
head, *rest = sets
|
27
|
+
head.product(*rest)
|
28
|
+
end
|
29
|
+
|
30
|
+
def apply_rules
|
31
|
+
@rules.each do |type, rules|
|
32
|
+
klass = CaseGen.const_get("#{type.to_s.capitalize}Rule")
|
33
|
+
rules.each_with_index do |rule_data, idx|
|
34
|
+
rule_data[:index] = idx + 1
|
35
|
+
klass.new(rule_data).apply(@combos)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CaseGen
|
4
|
+
class ExcludeAsText < CaseGen::Output
|
5
|
+
def exclude_output(_)
|
6
|
+
body = @generator.rules[:exclude].map do |rule|
|
7
|
+
[rule[:criteria], " #{rule_description(rule)}", '']
|
8
|
+
end
|
9
|
+
(header + body).join("\n")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CaseGen
|
4
|
+
class ExcludeInlineFootnotes < CaseGen::Output
|
5
|
+
def partition_exclusions?
|
6
|
+
false
|
7
|
+
end
|
8
|
+
|
9
|
+
def exclude_description(rule)
|
10
|
+
"[#{rule.rule_data[:index]}]"
|
11
|
+
end
|
12
|
+
|
13
|
+
def exclude_output(_)
|
14
|
+
body = @generator.rules[:exclude].map do |rule|
|
15
|
+
["[#{rule[:index]}] #{rule_description(rule)}"]
|
16
|
+
end
|
17
|
+
(header + body).join("\n")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CaseGen
|
4
|
+
class Output
|
5
|
+
def self.create(generator, output_type = :exclude)
|
6
|
+
klass_name = output_type.to_s.split('_').map(&:capitalize).join.to_s
|
7
|
+
Object.const_get("CaseGen::#{klass_name}").new(generator)
|
8
|
+
end
|
9
|
+
|
10
|
+
include RuleDescription
|
11
|
+
|
12
|
+
def initialize(generator)
|
13
|
+
@generator = generator
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
update_excluded_values
|
18
|
+
include, exclude = partition_exclusions
|
19
|
+
as_table(include).tap { |o| o << exclude_output(exclude) }
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def exclude_output(_exclude)
|
25
|
+
''
|
26
|
+
end
|
27
|
+
|
28
|
+
def as_table(combos)
|
29
|
+
combos.map(&:hash_row).to_table.to_s
|
30
|
+
end
|
31
|
+
|
32
|
+
def update_excluded_values
|
33
|
+
@generator.combos.each do |combo|
|
34
|
+
if combo.excluded?
|
35
|
+
value = exclude_description(combo.excluded_by_rule)
|
36
|
+
combo.append(:exclude, value)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def partition_exclusions
|
42
|
+
combos = @generator.combos
|
43
|
+
|
44
|
+
if partition_exclusions?
|
45
|
+
exclude, include = combos.partition(&:excluded?)
|
46
|
+
[include, exclude]
|
47
|
+
else
|
48
|
+
[combos, []]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def partition_exclusions?
|
53
|
+
true
|
54
|
+
end
|
55
|
+
|
56
|
+
def exclude_description(_rule)
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
|
60
|
+
def header
|
61
|
+
['', 'exclude', '-------']
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
Dir[File.join(__dir__, 'output', '*.rb')].sort.each { |fn| require fn }
|