rulezilla 0.1.4

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.
@@ -0,0 +1,63 @@
1
+ module Rulezilla
2
+ class RuleBuilder
3
+ class DefaultCondition; end
4
+
5
+ class GherkinToConditionRule
6
+ include Rulezilla::DSL
7
+
8
+ define :'this is a "field"' do
9
+ condition { name =~ /^this is\s?a?n? \"(.*)\"$/i }
10
+
11
+ result do
12
+ field = name.scan(/^this is\s?a?n? \"(.*)\"$/i).flatten.first
13
+ "#{field}?"
14
+ end
15
+ end
16
+
17
+ define :'the "field" is "value"' do
18
+ condition { name =~ /^the \"(.*)\" is \"(.*)\"$/i }
19
+
20
+ result do
21
+ field, value = name.scan(/^the \"(.*)\" is \"(.*)\"$/i).flatten
22
+ field = field.gsub(/\s/, '_').downcase
23
+
24
+ "#{field} == #{ConditionValueEvaluateRule.apply(value: value)}"
25
+ end
26
+ end
27
+
28
+ define :'the "field" is in: {table}' do
29
+ condition { name =~ /^the \"(.*)\" is in:$/i }
30
+
31
+ result do
32
+ field = name.scan(/^the \"(.*)\" is in:$/i).flatten.first
33
+ field = field.gsub(/\s/, '_').downcase
34
+
35
+ values = rows.map{ |row| "#{ConditionValueEvaluateRule.apply(value: row['cells'].first)}" }.join(', ')
36
+
37
+ "[#{values}].include?(#{field})"
38
+ end
39
+ end
40
+
41
+ define :'none of the above' do
42
+ condition { name == 'none of the above' }
43
+ result(DefaultCondition)
44
+ end
45
+
46
+ default { raise "Condition steps is not recognised: #{name}" }
47
+ end
48
+
49
+
50
+
51
+ class ConditionValueEvaluateRule
52
+ include Rulezilla::DSL
53
+
54
+ define :blank do
55
+ condition { value == 'blank'}
56
+ result("''")
57
+ end
58
+
59
+ default { "\"#{value}\"" }
60
+ end
61
+ end
62
+
63
+ end
@@ -0,0 +1,49 @@
1
+ module Rulezilla
2
+ class RuleBuilder
3
+ class GherkinToResultRule
4
+ include Rulezilla::DSL
5
+
6
+ group :keyword_is do
7
+ condition { name =~ /^the #{step_keyword} is/i }
8
+
9
+ define :value_is do
10
+ condition { name =~ /^the #{step_keyword} is \"(.*)\"$/i }
11
+ result { "\"#{name.scan(/^the #{step_keyword} is \"(.*)\"$/i).flatten.first}\"" }
12
+ end
13
+
14
+ define :duration_is do
15
+ condition { name =~ /^the #{step_keyword} is \"(\d+)\" (days?|hours?|minutes?|seconds?)$/i }
16
+ result do
17
+ quantity, unit = name.scan(/^the #{step_keyword} is \"(\d+)\" (days?|hours?|minutes?|seconds?)$/i).flatten
18
+
19
+ multiplier = case unit
20
+ when /day/
21
+ 86400
22
+ when /hour/
23
+ 3600
24
+ when /minute/
25
+ 60
26
+ when /second/
27
+ 1
28
+ end
29
+
30
+ quantity.to_i * multiplier
31
+ end
32
+ end
33
+ end
34
+
35
+ define :start_with_this_is_a do
36
+ condition { name =~ /^this is an? #{step_keyword}$/i }
37
+ result("true")
38
+ end
39
+
40
+ define :start_with_this_is_not_a do
41
+ condition { name =~ /^this is not an? #{step_keyword}$/i }
42
+ result("false")
43
+ end
44
+
45
+ default { raise "Unrecognisable step: #{name}" }
46
+
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,76 @@
1
+ module Rulezilla
2
+ class Tree
3
+ attr_reader :current_node, :root_node
4
+
5
+ def initialize(node)
6
+ @root_node = node
7
+ @root_node.name = :root
8
+ @current_node = node
9
+ end
10
+
11
+ def go_up
12
+ @current_node = is_root? ? @root_node : @current_node.parent
13
+ end
14
+
15
+ def find_all(record, node=@root_node)
16
+ array = []
17
+ if node.applies?(record)
18
+ node.children.each do |child_node|
19
+ array += find_all(record, child_node)
20
+ end
21
+
22
+ return node.has_result? ? array + [node] : array
23
+ end
24
+ return array
25
+ end
26
+
27
+ def trace(record, node=@root_node)
28
+ if node.applies?(record)
29
+ node.children.each do |child_node|
30
+ array = trace(record, child_node)
31
+ return [node] + array unless array.empty?
32
+ end
33
+ return node.has_result? ? [node] : []
34
+ end
35
+ return []
36
+ end
37
+
38
+ def all_results(record, node=@root_node, results=[])
39
+ if node.has_result?
40
+ results << node.result(record) rescue NoMethodError
41
+ end
42
+
43
+ node.children.each do |child_node|
44
+ all_results(record, child_node, results)
45
+ end
46
+
47
+ return results
48
+ end
49
+
50
+ def create_and_move_to_child(name=nil)
51
+ node = Node.new
52
+ node.name = name
53
+ @current_node.add_child(node)
54
+ @current_node = node
55
+ node
56
+ end
57
+
58
+ def clone_and_append_children(children, node=@current_node)
59
+ children.each do |child_node|
60
+ child_node = child_node.dup
61
+ node.add_child(child_node)
62
+
63
+ if child_node.has_children?
64
+ children_nodes = child_node.children
65
+ child_node.children = []
66
+ clone_and_append_children(children_nodes, child_node)
67
+ end
68
+ end
69
+ end
70
+
71
+ private
72
+ def is_root?
73
+ @current_node == @root_node
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,3 @@
1
+ module Rulezilla
2
+ VERSION = '0.1.4'
3
+ end
data/rulezilla.gemspec ADDED
@@ -0,0 +1,18 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+ require 'rulezilla/version'
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ['Peter Wu']
6
+ gem.email = ['peter.wu@simplybusiness.com']
7
+ gem.description = %q{Rules DSL}
8
+ gem.summary = %q{Rules DSL}
9
+ gem.homepage = %q{https://github.com/simplybusiness/rulezilla}
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.name = 'rulezilla'
13
+ gem.require_paths = ['lib']
14
+ gem.version = Rulezilla::VERSION
15
+ gem.license = 'MIT'
16
+
17
+ gem.add_runtime_dependency('gherkin')
18
+ end
@@ -0,0 +1,21 @@
1
+ Feature: Default Support methods
2
+
3
+ Scenario Outline: does_not?
4
+ Given the rule is:
5
+ """
6
+ define :not_sleep do
7
+ condition { does_not?(go_to_bed) }
8
+ result('Tired')
9
+ end
10
+
11
+ default('Refreshing')
12
+ """
13
+ When the record has attribute "go_to_bed" and returns "<value>"
14
+ Then the result is "<result>"
15
+
16
+ Examples:
17
+ | value | result |
18
+ | true | Refreshing |
19
+ | false | Tired |
20
+ | | Refreshing |
21
+ | nil | Refreshing |
@@ -0,0 +1,90 @@
1
+ Feature: Rulezilla Gherkin DSL
2
+
3
+ Scenario: Condition: something is something
4
+ Given the gherkin is:
5
+ """
6
+ Feature: Awesomeness Rule
7
+
8
+ Scenario: Robocop
9
+ When the "target" is "Robocop"
10
+ Then the awesomeness is "very awesome"
11
+ """
12
+ When the record has attribute "target" and returns "Robocop"
13
+ Then the result is "very awesome"
14
+
15
+
16
+ Scenario: Multiple Condition: something is something
17
+ Given the gherkin is:
18
+ """
19
+ Feature: Winner Rule
20
+
21
+ Scenario: Robocop vs Ironman
22
+ When the "target" is "Robocop"
23
+ And the "opponent" is "Ironman"
24
+ Then the winner is "Ironman"
25
+ """
26
+ When the record has attribute "target" and returns "Robocop"
27
+ And the record has attribute "opponent" and returns "Ironman"
28
+ Then the result is "Ironman"
29
+
30
+
31
+ Scenario: Default
32
+ Given the gherkin is:
33
+ """
34
+ Feature: Winner Rule
35
+
36
+ Scenario: Default
37
+ When none of the above
38
+ Then the winner is "Ironman"
39
+ """
40
+ Then the result is "Ironman"
41
+
42
+
43
+ Scenario: 'The :keyword is :value', Keyword mismatch
44
+ Given the incorrect gherkin is:
45
+ """
46
+ Feature: Winner Rule
47
+
48
+ Scenario: Default
49
+ When none of the above
50
+ Then the loser is "Hello kitty"
51
+ """
52
+ Then it raises exception "Unrecognisable step: the loser is 'Hello kitty'"
53
+
54
+
55
+ Scenario: True value
56
+ Given the gherkin is:
57
+ """
58
+ Feature: Dummy Rule
59
+
60
+ Scenario: Dummy
61
+ When the "name" is "007"
62
+ Then this is a dummy
63
+ """
64
+ When the record has attribute "name" and returns "007"
65
+ Then the result is "true"
66
+
67
+
68
+ Scenario: False value
69
+ Given the gherkin is:
70
+ """
71
+ Feature: Dummy Rule
72
+
73
+ Scenario: Not Dummy
74
+ When the "name" is "Tom"
75
+ Then this is not a dummy
76
+ """
77
+ When the record has attribute "name" and returns "Tom"
78
+ Then the result is "false"
79
+
80
+
81
+ Scenario: 'This is( not) a :keyword', Keyword mismatch
82
+ Given the incorrect gherkin is:
83
+ """
84
+ Feature: Winner Rule
85
+
86
+ Scenario: Not Dummy
87
+ When the "name" is "Tom"
88
+ Then this is not a cat
89
+ """
90
+ Then it raises exception "Unrecognisable step: this is not a 'cat'"
@@ -0,0 +1,16 @@
1
+ @rule_steps
2
+ Feature: Animal Rule
3
+
4
+ Scenario: entity is a cat
5
+ When this is a "cat"
6
+ Then this is an animal
7
+
8
+ Scenario: telephone number is dog or bird
9
+ When the "entity" is in:
10
+ | dog |
11
+ | bird |
12
+ Then this is an animal
13
+
14
+ Scenario: default
15
+ When none of the above
16
+ Then this is not an animal
@@ -0,0 +1,14 @@
1
+ @rule_steps
2
+ Feature: Duration Rule
3
+
4
+ Scenario: Maths class
5
+ When the "name of the class" is "Maths"
6
+ Then the duration is "1" minute
7
+
8
+ Scenario: Science class
9
+ When the "name of the class" is "Science"
10
+ Then the duration is "10" hours
11
+
12
+ Scenario: PE
13
+ When the "name of the class" is "PE"
14
+ Then the duration is "2" days
@@ -0,0 +1,299 @@
1
+ Feature: Rulezilla DSL
2
+
3
+ Scenario: To get all outcome from a rule
4
+ Given the rule is:
5
+ """
6
+ group :group_1 do
7
+ condition { false }
8
+
9
+ group :group_1_1 do
10
+ condition { true }
11
+
12
+ define :rule_1_1_1 do
13
+ condition { false }
14
+ result('A')
15
+ end
16
+
17
+ default('B')
18
+ end
19
+
20
+ define :rule_1_1 do
21
+ condition { true }
22
+ result('C')
23
+ end
24
+
25
+ define :rule_1_2 do
26
+ condition { true }
27
+ result('D')
28
+ end
29
+ end
30
+
31
+ define :rule_2 do
32
+ condition { false }
33
+ result('E')
34
+ end
35
+
36
+ default('F')
37
+ """
38
+ Then all the outcomes are "A, B, C, D, E, F"
39
+
40
+ Scenario: Rule is evaluated from top to bottom order
41
+ Given the rule is:
42
+ """
43
+ define :rule_1 do
44
+ condition { true }
45
+ result('Yes')
46
+ end
47
+
48
+ define :rule_2 do
49
+ condition { true }
50
+ result('May be')
51
+ end
52
+ """
53
+ Then the result is "Yes"
54
+
55
+
56
+ Scenario: Nesting in Rule DSL
57
+ Given the rule is:
58
+ """
59
+ group :group_1 do
60
+ condition { true }
61
+
62
+ define :good do
63
+ condition { true }
64
+ result('Good')
65
+ end
66
+ end
67
+ """
68
+ Then the result is "Good"
69
+
70
+
71
+ Scenario: If nothing is matched in a group, it will fall to default value of the group
72
+ Given the rule is:
73
+ """
74
+ group :group_1 do
75
+ condition { true }
76
+
77
+ define :good do
78
+ condition { false }
79
+ result('Good')
80
+ end
81
+
82
+ default('It is alright')
83
+ end
84
+ """
85
+ Then the result is "It is alright"
86
+
87
+
88
+ Scenario: If nothing is matched, and no default is define in the group, it will fall to the next default
89
+ Given the rule is:
90
+ """
91
+ group :group_1 do
92
+ condition { true }
93
+
94
+ define :good do
95
+ condition { false }
96
+ result('Good')
97
+ end
98
+ end
99
+
100
+ default('Everything is awesome')
101
+ """
102
+ Then the result is "Everything is awesome"
103
+
104
+
105
+ Scenario: If nothing is matched, it will continue to evaluate the next group
106
+ Given the rule is:
107
+ """
108
+ group :group_1 do
109
+ condition { true }
110
+
111
+ define :good do
112
+ condition { false }
113
+ result('Good')
114
+ end
115
+ end
116
+
117
+ group :group_2 do
118
+ condition { true }
119
+
120
+ define :bad do
121
+ condition { true }
122
+ result('Bad')
123
+ end
124
+ end
125
+ """
126
+ Then the result is "Bad"
127
+
128
+
129
+ Scenario Outline: It evaluate the rule against a record
130
+ Given the rule is:
131
+ """
132
+ define :fruit do
133
+ condition { fruit }
134
+ result('This is good!')
135
+ end
136
+
137
+ define :fruit do
138
+ condition { !fruit }
139
+ result('Oh, too bad')
140
+ end
141
+ """
142
+ When the record has attribute "fruit" and returns "<value>"
143
+ Then the result is "<result>"
144
+
145
+ Examples:
146
+ | value | result |
147
+ | true | This is good! |
148
+ | false | Oh, too bad |
149
+
150
+
151
+ Scenario: To get all matching outcomes from a rule
152
+ Given the rule is:
153
+ """
154
+ group :group_1 do
155
+ condition { true }
156
+
157
+ group :group_1_1 do
158
+ condition { true }
159
+
160
+ define :rule_1_1_1 do
161
+ condition { false }
162
+ result('A')
163
+ end
164
+
165
+ default('B')
166
+ end
167
+
168
+ define :rule_1_1 do
169
+ condition { true }
170
+ result('C')
171
+ end
172
+
173
+ define :rule_1_2 do
174
+ condition { true }
175
+ result('D')
176
+ end
177
+ end
178
+
179
+ define :rule_2 do
180
+ condition { false }
181
+ result('E')
182
+ end
183
+
184
+ default('F')
185
+ """
186
+ Then all the matching outcomes are "B, C, D, F"
187
+
188
+
189
+ Scenario: Support Module
190
+ Given the rule class name is "FruitRule"
191
+ And the support module called "FruitRuleSupport" has definition:
192
+ """
193
+ def is_fruit?
194
+ fruit == true
195
+ end
196
+ """
197
+ And the rule is:
198
+ """
199
+ define :fruit do
200
+ condition { is_fruit? }
201
+ result('This is good!')
202
+ end
203
+
204
+ define :fruit do
205
+ condition { !is_fruit? }
206
+ result('Oh, too bad')
207
+ end
208
+ """
209
+ When the record has attribute "fruit" and returns "true"
210
+ Then the result is "This is good!"
211
+
212
+
213
+ Scenario Outline: Validate the presence of attributes
214
+ Given the rule is:
215
+ """
216
+ validate_attributes_presence :apple, :orange
217
+
218
+ default(true)
219
+ """
220
+ When the record has attribute "<attributes>"
221
+ Then "<does or does not>" raise the exception "<exception>"
222
+
223
+ Examples:
224
+ | attributes | does or does not | exception |
225
+ | apple | does | Missing orange attributes |
226
+ | orange | does | Missing apple attributes |
227
+ | apple, orange | does not | |
228
+
229
+
230
+ Scenario: Rule return nil if no rule is defined Given the rule is:
231
+ Given the rule is:
232
+ """
233
+ """
234
+ Then the result is "nil"
235
+
236
+ Scenario Outline: Trace the path to the result
237
+ Given the rule is:
238
+ """
239
+ group :group_1 do
240
+ condition { group_1_condition }
241
+
242
+ group :group_2 do
243
+ condition { group_2_condition }
244
+
245
+ define :rule_1 do
246
+ condition { false }
247
+ result('A')
248
+ end
249
+
250
+ define :rule_2 do
251
+ condition { rule_2_condition }
252
+ result('B')
253
+ end
254
+
255
+ default('C')
256
+ end
257
+
258
+ default('D')
259
+ end
260
+
261
+ define :rule_3 do
262
+ condition { rule_3_condition }
263
+ result('E')
264
+ end
265
+
266
+ default('F')
267
+ """
268
+ When the record has attribute "group_1_condition" and returns "<group_1_condition>"
269
+ And the record has attribute "group_2_condition" and returns "<group_2_condition>"
270
+ And the record has attribute "rule_2_condition" and returns "<rule_2_condition>"
271
+ And the record has attribute "rule_3_condition" and returns "<rule_3_condition>"
272
+ Then the trace is "<trace>"
273
+
274
+ Examples:
275
+ | group_1_condition | group_2_condition | rule_2_condition | rule_3_condition | trace |
276
+ | true | true | true | true | root -> group_1 -> group_2 -> rule_2 |
277
+ | true | false | true | true | root -> group_1 |
278
+ | false | true | true | true | root -> rule_3 |
279
+ | false | true | true | false | root |
280
+
281
+ Scenario: Include rule
282
+ Given there is a rule called "CommonRule":
283
+ """
284
+ define :a do
285
+ condition { true }
286
+ result { 'A' }
287
+ end
288
+
289
+ define :b do
290
+ condition { false }
291
+ result { 'B' }
292
+ end
293
+ """
294
+ And our rule is:
295
+ """
296
+ include_rule CommonRule
297
+ """
298
+ Then all the outcomes are "A, B"
299
+ And the result is "A"