casegen 2.0.0 → 3.0.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/.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 }
|