rast 0.18.0 → 0.19.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/.travis.yml +15 -0
- data/CHANGELOG.md +14 -0
- data/Documentation.md +297 -0
- data/Gemfile +0 -2
- data/Getting-Started-Detailed.md +122 -0
- data/Getting-Started.md +19 -102
- data/README.md +80 -16
- data/examples/enum_module.rb +20 -6
- data/examples/factory_example.rb +4 -4
- data/examples/hotel_finder.rb +14 -0
- data/examples/person.rb +6 -0
- data/examples/prime_number.rb +13 -8
- data/lib/rast/parameter_generator.rb +107 -53
- data/lib/rast/rast_spec.rb +8 -2
- data/lib/rast/rules/logic_helper.rb +76 -95
- data/lib/rast/rules/rule_evaluator.rb +100 -98
- data/lib/rast/rules/rule_validator.rb +14 -7
- data/lib/rast/rules/token_util.rb +17 -0
- data/lib/rast/spec_dsl.rb +57 -35
- data/lib/rast.rb +5 -1
- data/lib/template_spec.yml +5 -7
- data/rast.gemspec +1 -1
- metadata +8 -9
- data/examples/arithmetic_module.rb +0 -8
- data/examples/double_example.rb +0 -14
- data/examples/logic_four.rb +0 -15
- data/examples/lohika.rb +0 -27
- data/examples/phone.rb +0 -6
- data/examples/quiz_module.rb +0 -34
- data/examples/triple.rb +0 -15
@@ -11,113 +11,38 @@ module LogicHelper
|
|
11
11
|
TRUE = '*true'
|
12
12
|
FALSE = '*false'
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
# * @param left left token, no subscript.
|
19
|
-
# * @param right right token, no subscript.
|
20
|
-
# */
|
21
|
-
def perform_logical_and(scenario: [], left_subscript: -1, right_subscript: -1,
|
22
|
-
left: nil, right: nil)
|
23
|
-
if FALSE == left && left_subscript == -1 || FALSE == right && right_subscript == -1
|
24
|
-
FALSE
|
25
|
-
elsif TRUE == left && left_subscript == -1 && TRUE == right && right_subscript == -1
|
26
|
-
TRUE
|
27
|
-
elsif TRUE == left && left_subscript == -1
|
28
|
-
if right_subscript < 0
|
29
|
-
scenario.include?(right).to_s
|
30
|
-
else
|
31
|
-
(scenario[right_subscript] == right).to_s
|
32
|
-
end
|
33
|
-
elsif TRUE == right && right_subscript == -1
|
34
|
-
if left_subscript < 0
|
35
|
-
scenario.include?(left).to_s
|
36
|
-
else
|
37
|
-
(scenario[left_subscript] == left).to_s
|
38
|
-
end
|
39
|
-
else
|
40
|
-
left_eval = pevaluate(
|
41
|
-
scenario: scenario,
|
42
|
-
subscript: left_subscript,
|
43
|
-
object: left
|
44
|
-
)
|
14
|
+
OPPOSITE = {
|
15
|
+
TRUE => FALSE,
|
16
|
+
FALSE => TRUE
|
17
|
+
}.freeze
|
45
18
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
subscript: right_subscript,
|
51
|
-
object: right
|
52
|
-
)
|
53
|
-
|
54
|
-
(left_eval && right_eval).to_s
|
55
|
-
end
|
56
|
-
end
|
19
|
+
LOGIC_PRIMARY_RESULT = {
|
20
|
+
and: FALSE,
|
21
|
+
or: TRUE
|
22
|
+
}.freeze
|
57
23
|
|
58
24
|
# /**
|
59
|
-
# * @
|
60
|
-
# * @
|
61
|
-
# * @
|
62
|
-
# * @
|
63
|
-
# * @param right right token.
|
25
|
+
# * @scenario list of scenario tokens.
|
26
|
+
# * @left left left token object.
|
27
|
+
# * @right right right token object.
|
28
|
+
# * @operation :and or :or.
|
64
29
|
# */
|
65
|
-
def
|
66
|
-
|
67
|
-
|
68
|
-
TRUE
|
69
|
-
elsif FALSE == left && left_subscript == -1 && FALSE == right && right_subscript == -1
|
70
|
-
FALSE
|
71
|
-
elsif FALSE == left && left_subscript == -1
|
72
|
-
if right_subscript < 0
|
73
|
-
scenario.include?(right).to_s
|
74
|
-
else
|
75
|
-
(scenario[right_subscript] == right).to_s
|
76
|
-
end
|
77
|
-
elsif FALSE == right && right_subscript == -1
|
78
|
-
if left_subscript < 0
|
79
|
-
scenario.include?(left).to_s
|
80
|
-
else
|
81
|
-
(scenario[left_subscript] == left).to_s
|
82
|
-
end
|
83
|
-
else
|
84
|
-
left_eval = pevaluate(
|
85
|
-
scenario: scenario,
|
86
|
-
subscript: left_subscript,
|
87
|
-
object: left
|
88
|
-
)
|
30
|
+
def perform_logical(scenario: [], left: {}, right: {}, operation: :nil)
|
31
|
+
evaluated = send(:both_internal?, left, right, operation)
|
32
|
+
return evaluated if evaluated
|
89
33
|
|
90
|
-
|
34
|
+
default = operation == :and ? TRUE : FALSE
|
35
|
+
return present?(scenario, right).to_s if internal_match?(default, left)
|
91
36
|
|
92
|
-
|
93
|
-
scenario: scenario,
|
94
|
-
subscript: right_subscript,
|
95
|
-
object: right
|
96
|
-
)
|
97
|
-
|
98
|
-
(left_eval || right_eval).to_s
|
99
|
-
end
|
100
|
-
end
|
37
|
+
return present?(scenario, left).to_s if internal_match?(default, right)
|
101
38
|
|
102
|
-
|
103
|
-
# * Helper method to evaluate left or right token.
|
104
|
-
# *
|
105
|
-
# * @param scenario list of scenario tokens.
|
106
|
-
# * @param subscript scenario token subscript.
|
107
|
-
# * @param object left or right token.
|
108
|
-
# */
|
109
|
-
def pevaluate(scenario: [], subscript: -1, object: nil)
|
110
|
-
if subscript < 0
|
111
|
-
scenario.include?(object)
|
112
|
-
else
|
113
|
-
scenario[subscript] == object
|
114
|
-
end
|
39
|
+
send("evaluate_#{operation}", scenario, left, right).to_s
|
115
40
|
end
|
116
41
|
|
117
42
|
# /**
|
118
43
|
# * Check if the token is opening bracket.
|
119
44
|
# *
|
120
|
-
# * @
|
45
|
+
# * @token Input <code>String</code> token
|
121
46
|
# * @return <code>boolean</code> output
|
122
47
|
# */
|
123
48
|
def open_bracket?(token: '')
|
@@ -133,4 +58,60 @@ module LogicHelper
|
|
133
58
|
def close_bracket?(token: '')
|
134
59
|
token == ')'
|
135
60
|
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
# @left hash containing token and subscript
|
65
|
+
# @right hash containing token and subscript
|
66
|
+
# @operation symbol either :and or :or
|
67
|
+
def both_internal?(left, right, operation)
|
68
|
+
default = LOGIC_PRIMARY_RESULT[operation]
|
69
|
+
if internal_match?(default, left) || internal_match?(default, right)
|
70
|
+
return default
|
71
|
+
end
|
72
|
+
|
73
|
+
opposite = OPPOSITE[default]
|
74
|
+
if internal_match?(opposite, left) && internal_match?(opposite, right)
|
75
|
+
return opposite
|
76
|
+
end
|
77
|
+
|
78
|
+
false
|
79
|
+
end
|
80
|
+
|
81
|
+
def evaluate_and(scenario, left, right)
|
82
|
+
left_eval = present?(scenario, left)
|
83
|
+
|
84
|
+
return false unless left_eval
|
85
|
+
|
86
|
+
right_eval = present?(scenario, right)
|
87
|
+
left_eval && right_eval
|
88
|
+
end
|
89
|
+
|
90
|
+
def evaluate_or(scenario, left, right)
|
91
|
+
left_eval = present?(scenario, left)
|
92
|
+
|
93
|
+
return true if left_eval
|
94
|
+
|
95
|
+
right_eval = present?(scenario, right)
|
96
|
+
left_eval || right_eval
|
97
|
+
end
|
98
|
+
|
99
|
+
# /**
|
100
|
+
# * Helper method to evaluate left or right token.
|
101
|
+
# *
|
102
|
+
# * @param scenario list of scenario tokens.
|
103
|
+
# * @param subscript scenario token subscript.
|
104
|
+
# * @param object left or right token.
|
105
|
+
# */
|
106
|
+
def present?(scenario, token)
|
107
|
+
if token[:subscript] < 0
|
108
|
+
scenario.include?(token[:value])
|
109
|
+
else
|
110
|
+
scenario[token[:subscript]] == token[:value]
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def internal_match?(internal, token)
|
115
|
+
token[:value] == internal && token[:subscript] == -1
|
116
|
+
end
|
136
117
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'rast/rules/operator'
|
4
|
+
require 'rast/rules/token_util'
|
4
5
|
require 'rast/rules/logic_helper'
|
5
6
|
require 'rast/converters/int_converter'
|
6
7
|
require 'rast/converters/float_converter'
|
@@ -8,7 +9,7 @@ require 'rast/converters/default_converter'
|
|
8
9
|
require 'rast/converters/bool_converter'
|
9
10
|
require 'rast/converters/str_converter'
|
10
11
|
|
11
|
-
# Evaluates the rules.
|
12
|
+
# Evaluates the rules. "Internal refers to the `*true` or `*false` results."
|
12
13
|
class RuleEvaluator
|
13
14
|
include LogicHelper
|
14
15
|
|
@@ -99,27 +100,30 @@ class RuleEvaluator
|
|
99
100
|
# * @param rule_token_convert token to converter map.
|
100
101
|
# * @param default_converter default converter to use.
|
101
102
|
# */
|
102
|
-
def next_value(rule_token_convert: {}
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
if
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
103
|
+
def next_value(rule_token_convert: {})
|
104
|
+
token = @stack_answer.pop
|
105
|
+
default = {
|
106
|
+
subscript: -1,
|
107
|
+
value: token
|
108
|
+
}
|
109
|
+
|
110
|
+
return default if token.is_a?(Array) || [TRUE, FALSE].include?(token)
|
111
|
+
|
112
|
+
next_value_default(rule_token_convert, token)
|
113
|
+
end
|
114
|
+
|
115
|
+
# private
|
116
|
+
def next_value_default(rule_token_convert, token)
|
117
|
+
token_cleaned = token.to_s.strip
|
118
|
+
subscript = TokenUtil.extract_subscript(token: token_cleaned)
|
119
|
+
token_body = subscript > -1 ? token_cleaned[/^.+(?=\[)/] : token_cleaned
|
120
|
+
|
121
|
+
raise "Config Error: Outcome clause token: '#{token}' not found in variables" if rule_token_convert[token_body].nil?
|
119
122
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
+
{
|
124
|
+
value: rule_token_convert[token_body].convert(token_body),
|
125
|
+
subscript: subscript
|
126
|
+
}
|
123
127
|
end
|
124
128
|
|
125
129
|
# /** @param token token. */
|
@@ -127,39 +131,34 @@ class RuleEvaluator
|
|
127
131
|
if open_bracket?(token: token)
|
128
132
|
@stack_operations << token
|
129
133
|
elsif close_bracket?(token: token)
|
130
|
-
|
131
|
-
!open_bracket?(token: @stack_operations.last.strip)
|
132
|
-
@stack_rpn << @stack_operations.pop
|
133
|
-
end
|
134
|
-
@stack_operations.pop
|
134
|
+
shunt_close
|
135
135
|
elsif operator?(token: token)
|
136
|
-
|
137
|
-
operator?(token: @stack_operations.last.strip) &&
|
138
|
-
precedence(symbol_char: token[0]) <=
|
139
|
-
precedence(symbol_char: @stack_operations.last.strip[0])
|
140
|
-
@stack_rpn << @stack_operations.pop
|
141
|
-
end
|
142
|
-
@stack_operations << token
|
136
|
+
shunt_operator(token)
|
143
137
|
else
|
144
138
|
@stack_rpn << token
|
145
139
|
end
|
146
140
|
end
|
147
141
|
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
return -1 if token.is_a? Array
|
142
|
+
def shunt_operator(token)
|
143
|
+
while !@stack_operations.empty? &&
|
144
|
+
operator?(token: @stack_operations.last.strip) &&
|
145
|
+
precedence(symbol_char: token[0]) <=
|
146
|
+
precedence(symbol_char: @stack_operations.last.strip[0])
|
147
|
+
@stack_rpn << @stack_operations.pop
|
148
|
+
end
|
149
|
+
@stack_operations << token
|
150
|
+
end
|
158
151
|
|
159
|
-
|
160
|
-
|
152
|
+
def shunt_close
|
153
|
+
while @stack_operations.any? &&
|
154
|
+
!open_bracket?(token: @stack_operations.last.strip)
|
155
|
+
@stack_rpn << @stack_operations.pop
|
156
|
+
end
|
157
|
+
@stack_operations.pop
|
161
158
|
end
|
162
159
|
|
160
|
+
private
|
161
|
+
|
163
162
|
# /**
|
164
163
|
# * @param scenario List of values to evaluate against the rule expression.
|
165
164
|
# * @param rule_token_convert token to converter map.
|
@@ -170,29 +169,36 @@ class RuleEvaluator
|
|
170
169
|
|
171
170
|
# /* get the clone of the RPN stack for further evaluating */
|
172
171
|
stack_rpn_clone = Marshal.load(Marshal.dump(@stack_rpn))
|
172
|
+
evaluate_stack_rpn(stack_rpn_clone, scenario, rule_token_convert)
|
173
|
+
|
174
|
+
raise 'Some operator is missing' if @stack_answer.size > 1
|
173
175
|
|
174
|
-
|
175
|
-
|
176
|
-
|
176
|
+
last = @stack_answer.pop
|
177
|
+
last[1..last.size]
|
178
|
+
end
|
179
|
+
|
180
|
+
# evaluating the RPN expression
|
181
|
+
def evaluate_stack_rpn(stack_rpn, scenario, rule_token_convert)
|
182
|
+
while stack_rpn.any?
|
183
|
+
token = stack_rpn.pop
|
177
184
|
if operator?(token: token)
|
178
|
-
|
179
|
-
evaluate_multi_not(scenario: scenario)
|
180
|
-
else
|
181
|
-
evaluate_multi(
|
182
|
-
scenario: scenario,
|
183
|
-
rule_token_convert: rule_token_convert,
|
184
|
-
operator: RuleEvaluator.operator_from_symbol(symbol: token[0])
|
185
|
-
)
|
186
|
-
end
|
185
|
+
evaluate_operator(scenario, rule_token_convert, token)
|
187
186
|
else
|
188
187
|
@stack_answer << token
|
189
188
|
end
|
190
189
|
end
|
190
|
+
end
|
191
191
|
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
192
|
+
def evaluate_operator(scenario, rule_token_convert, token)
|
193
|
+
if NOT.symbol == token
|
194
|
+
evaluate_multi_not(scenario: scenario)
|
195
|
+
else
|
196
|
+
evaluate_multi(
|
197
|
+
scenario: scenario,
|
198
|
+
rule_token_convert: rule_token_convert,
|
199
|
+
operator: RuleEvaluator.operator_from_symbol(symbol: token[0])
|
200
|
+
)
|
201
|
+
end
|
196
202
|
end
|
197
203
|
|
198
204
|
# /**
|
@@ -201,35 +207,21 @@ class RuleEvaluator
|
|
201
207
|
# * @param operator OR/AND.
|
202
208
|
# */
|
203
209
|
def evaluate_multi(scenario: [], rule_token_convert: {}, operator: nil)
|
204
|
-
default_converter = DEFAULT_CONVERT_HASH[scenario.first.class]
|
205
|
-
|
206
210
|
# Convert 'nil' to nil.
|
207
|
-
formatted_scenario = scenario.map { |token| token == 'nil' ? nil: token }
|
211
|
+
formatted_scenario = scenario.map { |token| token == 'nil' ? nil : token }
|
208
212
|
|
209
|
-
|
210
|
-
|
211
|
-
default_converter: default_converter
|
212
|
-
)
|
213
|
-
|
214
|
-
right_arr = next_value(
|
215
|
-
rule_token_convert: rule_token_convert,
|
216
|
-
default_converter: default_converter
|
217
|
-
)
|
213
|
+
left = next_value(rule_token_convert: rule_token_convert)
|
214
|
+
right = next_value(rule_token_convert: rule_token_convert)
|
218
215
|
|
219
216
|
answer = send(
|
220
|
-
|
217
|
+
:perform_logical,
|
221
218
|
scenario: formatted_scenario,
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
right: right_arr[1]
|
219
|
+
left: left,
|
220
|
+
right: right,
|
221
|
+
operation: operator.name.to_sym
|
226
222
|
)
|
227
223
|
|
228
|
-
@stack_answer <<
|
229
|
-
answer
|
230
|
-
else
|
231
|
-
"*#{answer}"
|
232
|
-
end
|
224
|
+
@stack_answer << format_internal_result(answer)
|
233
225
|
end
|
234
226
|
|
235
227
|
# /**
|
@@ -243,28 +235,38 @@ class RuleEvaluator
|
|
243
235
|
elsif LogicHelper::FALSE == latest
|
244
236
|
LogicHelper::TRUE
|
245
237
|
else
|
246
|
-
|
247
|
-
converter = DEFAULT_CONVERT_HASH[scenario.first.class]
|
248
|
-
if subscript < 0
|
249
|
-
converted = converter.convert(latest)
|
250
|
-
(!scenario.include?(converted)).to_s
|
251
|
-
else
|
252
|
-
converted = converter.convert(latest[RE_TOKEN_BODY])
|
253
|
-
(scenario[subscript] != converted).to_s
|
254
|
-
end
|
238
|
+
evaluate_non_internal(scenario, latest)
|
255
239
|
end
|
256
240
|
|
257
|
-
@stack_answer <<
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
241
|
+
@stack_answer << format_internal_result(answer)
|
242
|
+
end
|
243
|
+
|
244
|
+
def evaluate_non_internal(scenario, latest)
|
245
|
+
subscript = TokenUtil.extract_subscript(token: latest)
|
246
|
+
converter = DEFAULT_CONVERT_HASH[scenario.first.class]
|
247
|
+
if subscript < 0
|
248
|
+
converted = converter.convert(latest)
|
249
|
+
(!scenario.include?(converted)).to_s
|
250
|
+
else
|
251
|
+
converted = converter.convert(latest[RE_TOKEN_BODY])
|
252
|
+
(scenario[subscript] != converted).to_s
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
# returns true if answer starts with *, *true if answer is true, same goes for
|
257
|
+
# false.
|
258
|
+
def format_internal_result(answer)
|
259
|
+
if answer[0] == '*'
|
260
|
+
answer
|
261
|
+
else
|
262
|
+
"*#{answer}"
|
263
|
+
end
|
262
264
|
end
|
263
265
|
|
264
266
|
# /** @param scenario to evaluate against the rule expression. */
|
265
267
|
def evaluate_one_rpn(scenario: [])
|
266
268
|
single = @stack_rpn.last
|
267
|
-
subscript = extract_subscript(token: single)
|
269
|
+
subscript = TokenUtil.extract_subscript(token: single)
|
268
270
|
default_converter = DEFAULT_CONVERT_HASH[scenario.first.class]
|
269
271
|
if subscript > -1
|
270
272
|
scenario[subscript] == default_converter.convert(single[RE_TOKEN_BODY])
|
@@ -11,8 +11,13 @@ class RuleValidator
|
|
11
11
|
)
|
12
12
|
|
13
13
|
spec = fixture[:spec]
|
14
|
-
|
14
|
+
validate_results(scenario, rule_result, spec)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
15
18
|
|
19
|
+
def validate_results(scenario, rule_result, spec)
|
20
|
+
rule = spec.rule
|
16
21
|
single_result = rule.size == 1
|
17
22
|
if single_result
|
18
23
|
next_result = rule_result.first
|
@@ -23,8 +28,6 @@ class RuleValidator
|
|
23
28
|
end
|
24
29
|
end
|
25
30
|
|
26
|
-
private
|
27
|
-
|
28
31
|
def validate_multi(scenario: [], spec: nil, rule_result: [])
|
29
32
|
matched_outputs = []
|
30
33
|
match_count = 0
|
@@ -36,14 +39,18 @@ class RuleValidator
|
|
36
39
|
matched_outputs << spec.rule.outcomes[i]
|
37
40
|
end
|
38
41
|
|
39
|
-
|
40
|
-
match_count == 1 || match_count == 0 && !spec.default_outcome.nil?
|
41
|
-
end
|
42
|
+
verify_results(spec, scenario, matched_outputs, match_count)
|
42
43
|
|
43
44
|
matched_outputs.first || spec.default_outcome
|
44
45
|
end
|
45
46
|
|
46
|
-
|
47
|
+
def verify_results(spec, scenario, matched_outputs, match_count)
|
48
|
+
Rast.assert("#{spec.description} #{scenario} must fall into a unique rule" \
|
49
|
+
" outcome/clause, matched: #{matched_outputs}") do
|
50
|
+
match_count == 1 || match_count.zero? && !spec.default_outcome.nil?
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
47
54
|
def binary_outcome(outcome: '', spec: nil, expected: false)
|
48
55
|
if expected == 'true'
|
49
56
|
outcome
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
module TokenUtil
|
5
|
+
# /**
|
6
|
+
# * Returns value of 'n' if rule token ends with '[n]'. where 'n' is the
|
7
|
+
# * variable group index.
|
8
|
+
# *
|
9
|
+
# * @param string token to check for subscript.
|
10
|
+
# */
|
11
|
+
def self.extract_subscript(token: '')
|
12
|
+
return -1 if token.is_a? Array
|
13
|
+
|
14
|
+
subscript = token[/\[(\d+)\]$/, 1]
|
15
|
+
subscript.nil? ? -1 : subscript.to_i
|
16
|
+
end
|
17
|
+
end
|