casegen 2.0.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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 }