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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.rubocop.yml +109 -0
  4. data/.ruby-version +1 -1
  5. data/Gemfile +3 -1
  6. data/Gemfile.lock +51 -6
  7. data/README.md +10 -119
  8. data/Rakefile +9 -7
  9. data/bin/casegen +2 -1
  10. data/casegen.gemspec +13 -9
  11. data/doc/bounding_box.rb +37 -0
  12. data/doc/cart.rb +43 -0
  13. data/doc/expect_only.rb +28 -0
  14. data/doc/pricing.rb +50 -0
  15. data/doc/ruby_array.rb +41 -0
  16. data/lib/case_gen/combination.rb +38 -0
  17. data/lib/case_gen/combo_matcher.rb +15 -0
  18. data/lib/case_gen/exclude_rule.rb +50 -0
  19. data/lib/case_gen/expect_rule.rb +24 -0
  20. data/lib/case_gen/generator.rb +40 -0
  21. data/lib/case_gen/output/exclude.rb +6 -0
  22. data/lib/case_gen/output/exclude_as_table.rb +13 -0
  23. data/lib/case_gen/output/exclude_as_text.rb +12 -0
  24. data/lib/case_gen/output/exclude_inline.rb +13 -0
  25. data/lib/case_gen/output/exclude_inline_footnotes.rb +20 -0
  26. data/lib/case_gen/output.rb +66 -0
  27. data/lib/case_gen/rule_description.rb +11 -0
  28. data/lib/case_gen/set.rb +16 -0
  29. data/lib/casegen.rb +15 -183
  30. data/spec/cart_sample_spec.rb +46 -0
  31. data/spec/case_gen/combination_spec.rb +11 -0
  32. data/spec/case_gen/exclude_rule_spec.rb +17 -0
  33. data/spec/exclude_as_table_spec.rb +39 -0
  34. data/spec/exclude_as_text_spec.rb +58 -0
  35. data/spec/exclude_inline_footnotes_spec.rb +58 -0
  36. data/spec/exclude_inline_spec.rb +50 -0
  37. data/spec/expect_only_spec.rb +30 -0
  38. data/spec/spec_helper.rb +113 -0
  39. metadata +101 -35
  40. data/.idea/encodings.xml +0 -5
  41. data/.idea/misc.xml +0 -5
  42. data/.idea/modules.xml +0 -9
  43. data/.idea/vcs.xml +0 -7
  44. data/doc/calc.sample.txt +0 -13
  45. data/doc/cart.sample.rb +0 -3
  46. data/doc/cart.sample.txt +0 -33
  47. data/doc/ruby_array.sample.rb +0 -26
  48. data/lib/agents/sets/enum/by.rb +0 -244
  49. data/lib/agents/sets/enum/cluster.rb +0 -164
  50. data/lib/agents/sets/enum/inject.rb +0 -50
  51. data/lib/agents/sets/enum/nest.rb +0 -117
  52. data/lib/agents/sets/enum/op.rb +0 -283
  53. data/lib/agents/sets/enum/pipe.rb +0 -160
  54. data/lib/agents/sets/enum/tree.rb +0 -442
  55. data/lib/agents/sets.rb +0 -313
  56. data/test/agents/console_output_test.rb +0 -27
  57. data/test/agents/sets.test.rb +0 -227
  58. data/test/agents_test.rb +0 -41
  59. data/test/casegen.tests.rb +0 -0
  60. data/test/parser_test.rb +0 -163
  61. 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
@@ -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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CaseGen
4
+ class Exclude < CaseGen::Output
5
+ end
6
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CaseGen
4
+ class ExcludeAsTable < CaseGen::Output
5
+ def exclude_output(exclude)
6
+ "\n#{as_table(exclude)}"
7
+ end
8
+
9
+ def exclude_description(rule)
10
+ rule.description
11
+ end
12
+ end
13
+ 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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CaseGen
4
+ class ExcludeInline < CaseGen::Output
5
+ def partition_exclusions?
6
+ false
7
+ end
8
+
9
+ def exclude_description(rule)
10
+ rule.description
11
+ end
12
+ end
13
+ 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 }