rast 0.1.0.pre

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,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Represents a logical rule object.
4
+ class Rule
5
+ # /**
6
+ # * TODO: Validation of the output_clause_src parameter.
7
+ # *
8
+ # * Instantiate a rule with the given clause. <br/>
9
+ # * <br/>
10
+ # * <b>Parameter Example:</b><br/>
11
+ # * Visible:Proposed|Approved<br/>
12
+ # * <br/>
13
+ # * Peace:Friendly|Indifferent\<br/>
14
+ # * ~War:Angry\<br/>
15
+ # * ~Neutral:Play safe
16
+ # */
17
+ def initialize(rules: {})
18
+ raise 'Must not have empty rules' if rules.empty?
19
+
20
+ @outcome_clause_hash = {}
21
+ # should have array of the rule pairs
22
+ # rules = pActRuleSrc.split("~");
23
+ duplicates = []
24
+
25
+ rules.each do |outcome, clause|
26
+ if duplicates.include?(outcome)
27
+ raise "#{outcome} matched multiple clauses"
28
+ end
29
+
30
+ duplicates << outcome
31
+ clause = remove_spaces(string: clause, separator: '\(')
32
+ clause = remove_spaces(string: clause, separator: '\)')
33
+ clause = remove_spaces(string: clause, separator: '&')
34
+ clause = remove_spaces(string: clause, separator: '\|')
35
+ clause = remove_spaces(string: clause, separator: '!')
36
+ @outcome_clause_hash[outcome.to_s] = clause.strip
37
+ end
38
+ end
39
+
40
+ def size
41
+ @outcome_clause_hash.size
42
+ end
43
+
44
+ # /**
45
+ # * Removes the leading and trailing spaces of rule tokens.
46
+ # *
47
+ # * @param string rule clause.
48
+ # * @param separator rule clause token.
49
+ # */
50
+ def remove_spaces(string: '', separator: '')
51
+ string.gsub(Regexp.new("\\s*#{separator} \\s*"), separator)
52
+ end
53
+
54
+ # /**
55
+ # * @return the actionList
56
+ # */
57
+ def outcomes
58
+ @outcome_clause_hash.keys
59
+ end
60
+
61
+ # /**
62
+ # * @param action action which rule we want to retrieve.
63
+ # * @return the actionToRuleClauses
64
+ # */
65
+ def clause(outcome: '')
66
+ @outcome_clause_hash[outcome]
67
+ end
68
+
69
+ # /**
70
+ # * Get rule result give a fixed list of scenario tokens. Used for fixed
71
+ # * list.
72
+ # *
73
+ # * @param scenario of interest.
74
+ # * @return the actionToRuleClauses
75
+ # */
76
+ def rule_outcome(scenario: [])
77
+ scenario_string = scenario.to_s
78
+ anded_scenario = scenario_string[1..-2].gsub(/,\s/, '&')
79
+
80
+ @outcome_clause_hash.each do |key, clause|
81
+ or_list_clause = clause.split('\|').map(&:strip)
82
+ return key if or_list_clause.include?(anded_scenario)
83
+ end
84
+ end
85
+
86
+ # /**
87
+ # * @see {@link Object#toString()}
88
+ # * @return String representation of this instance.
89
+ # */
90
+ def to_s
91
+ @outcome_clause_hash.to_s
92
+ end
93
+ end
@@ -0,0 +1,273 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'operator'
4
+ require_relative 'logic_helper'
5
+ require 'rast/converters/int_converter'
6
+ require 'rast/converters/bool_converter'
7
+ require 'rast/converters/str_converter'
8
+
9
+ # Evaluates the rules.
10
+ class RuleEvaluator
11
+ include LogicHelper
12
+
13
+ NOT = Operator.new(name: 'not', symbol: '!', precedence: 100)
14
+ AND = Operator.new(name: 'and', symbol: '&', precedence: 2)
15
+ OR = Operator.new(name: 'or', symbol: '|', precedence: 1)
16
+
17
+ OPERATORS = [NOT, AND, OR].freeze
18
+ OPERATORS_CONCAT = OPERATORS.map(&:to_s).join
19
+
20
+ # the "false" part of the "false[1]"
21
+ RE_TOKEN_BODY = /^.+(?=\[)/.freeze
22
+ RE_TOKENS = /([!|)(&])|([a-zA-Z\s0-9]+\[\d\])/.freeze
23
+
24
+ def self.operator_from_symbol(symbol: nil)
25
+ OPERATORS.find { |operator| operator.symbol == symbol }
26
+ end
27
+
28
+ DEFAULT_CONVERT_HASH = {
29
+ Integer => IntConverter.new,
30
+ TrueClass => BoolConverter.new,
31
+ FalseClass => BoolConverter.new,
32
+ String => StrConverter.new
33
+ }.freeze
34
+
35
+ # /** @param pConverterList list of rule token converters. */
36
+ def initialize(converters: [])
37
+ @converters = converters
38
+
39
+ @stack_operations = []
40
+ @stack_rpn = []
41
+ @stack_answer = []
42
+ end
43
+
44
+ # /**
45
+ # * Parses the math expression (complicated formula) and stores the result.
46
+ # *
47
+ # * @param pExpression <code>String</code> input expression (logical
48
+ # * expression formula)
49
+ # * @since 0.3.0
50
+ # */
51
+ def parse(expression: '')
52
+ # /* cleaning stacks */
53
+ @stack_operations.clear
54
+ @stack_rpn.clear
55
+
56
+ # /* splitting input string into tokens */
57
+ tokens = expression.split(RE_TOKENS).reject(&:empty?)
58
+
59
+ # /* loop for handling each token - shunting-yard algorithm */
60
+ tokens.each { |token| shunt_internal(token: token.strip) }
61
+
62
+ @stack_rpn << @stack_operations.pop while @stack_operations.any?
63
+ @stack_rpn.reverse!
64
+ end
65
+
66
+ # /**
67
+ # * Evaluates once parsed math expression with "var" variable included.
68
+ # *
69
+ # * @param scenario List of values to evaluate against the rule expression.
70
+ # * @param rule_token_convert mapping of rule tokens to converter.
71
+ # * @return <code>String</code> representation of the result
72
+ # */
73
+ def evaluate(scenario: [], rule_token_convert: {})
74
+ # /* check if is there something to evaluate */
75
+ if @stack_rpn.empty?
76
+ true
77
+ elsif @stack_rpn.size == 1
78
+ evaluate_one_rpn(scenario: scenario)
79
+ else
80
+ evaluate_multi_rpn(
81
+ scenario: scenario,
82
+ rule_token_convert: rule_token_convert
83
+ )
84
+ end
85
+ end
86
+
87
+ # /**
88
+ # * @param rule_token_convert token to converter map.
89
+ # * @param default_converter default converter to use.
90
+ # */
91
+ def next_value(rule_token_convert: {}, default_converter: nil)
92
+ subscript = -1
93
+ retval = []
94
+ value = @stack_answer.pop&.strip
95
+ if TRUE != value && FALSE != value
96
+ subscript = extract_subscript(token: value.to_s)
97
+ value_str = value.to_s.strip
98
+ if subscript > -1
99
+ converter = @converters[subscript]
100
+ value = converter.convert(value_str[/^.+(?=\[)/])
101
+ else
102
+ value = if rule_token_convert.nil? ||
103
+ rule_token_convert[value_str].nil?
104
+ default_converter.convert(value_str)
105
+ else
106
+ rule_token_convert[value_str].convert(value_str)
107
+ end
108
+ end
109
+ end
110
+ retval << subscript
111
+ retval << value
112
+ retval
113
+ end
114
+
115
+ # /** @param token token. */
116
+ def shunt_internal(token: '')
117
+ if open_bracket?(token: token)
118
+ @stack_operations << token
119
+ elsif close_bracket?(token: token)
120
+ while @stack_operations.any? &&
121
+ !open_bracket?(token: @stack_operations.last.strip)
122
+ @stack_rpn << @stack_operations.pop
123
+ end
124
+ @stack_operations.pop
125
+ elsif operator?(token: token)
126
+ while !@stack_operations.empty? &&
127
+ operator?(token: @stack_operations.last.strip) &&
128
+ precedence(symbol_char: token[0]) <=
129
+ precedence(symbol_char: @stack_operations.last.strip[0])
130
+ @stack_rpn << @stack_operations.pop
131
+ end
132
+ @stack_operations << token
133
+ else
134
+ @stack_rpn << token
135
+ end
136
+ end
137
+
138
+ private
139
+
140
+ # /**
141
+ # * Returns value of 'n' if rule token ends with '[n]'. where 'n' is the
142
+ # * variable group index.
143
+ # *
144
+ # * @param string token to check for subscript.
145
+ # */
146
+ def extract_subscript(token: '')
147
+ subscript = token[/\[(\d+)\]$/, 1]
148
+ subscript.nil? ? -1 : subscript.to_i
149
+ end
150
+
151
+ # /**
152
+ # * @param scenario List of values to evaluate against the rule expression.
153
+ # * @param rule_token_convert token to converter map.
154
+ # */
155
+ def evaluate_multi_rpn(scenario: [], rule_token_convert: {})
156
+ # /* clean answer stack */
157
+ @stack_answer.clear
158
+
159
+ # /* get the clone of the RPN stack for further evaluating */
160
+ stack_rpn_clone = Marshal.load(Marshal.dump(@stack_rpn))
161
+
162
+ # /* evaluating the RPN expression */
163
+
164
+ # binding.pry
165
+
166
+ while stack_rpn_clone.any?
167
+ token = stack_rpn_clone.pop.strip
168
+ if operator?(token: token)
169
+ if NOT.symbol == token
170
+ evaluate_multi_not(scenario: scenario)
171
+ else
172
+ evaluate_multi(
173
+ scenario: scenario,
174
+ rule_token_convert: rule_token_convert,
175
+ operator: RuleEvaluator.operator_from_symbol(symbol: token[0])
176
+ )
177
+ end
178
+ else
179
+ @stack_answer << token
180
+ end
181
+ end
182
+
183
+ raise 'Some operator is missing' if @stack_answer.size > 1
184
+
185
+ @stack_answer.pop[1..]
186
+ end
187
+
188
+ # /**
189
+ # * @param scenario List of values to evaluate against the rule expression.
190
+ # * @param rule_token_convert token to converter map.
191
+ # * @param operator OR/AND.
192
+ # */
193
+ def evaluate_multi(scenario: [], rule_token_convert: {}, operator: nil)
194
+ default_converter = DEFAULT_CONVERT_HASH[scenario.first.class]
195
+
196
+ # binding.pry
197
+
198
+ left_arr = next_value(
199
+ rule_token_convert: rule_token_convert,
200
+ default_converter: default_converter
201
+ )
202
+
203
+ right_arr = next_value(
204
+ rule_token_convert: rule_token_convert,
205
+ default_converter: default_converter
206
+ )
207
+
208
+ answer = send(
209
+ "perform_logical_#{operator.name}",
210
+ scenario: scenario,
211
+ left_subscript: left_arr[0],
212
+ right_subscript: right_arr[0],
213
+ left: left_arr[1],
214
+ right: right_arr[1]
215
+ )
216
+
217
+ @stack_answer << if answer[0] == '|'
218
+ answer
219
+ else
220
+ "|#{answer}"
221
+ end
222
+ end
223
+
224
+ # /**
225
+ # * @param scenario List of values to evaluate against the rule expression.
226
+ # */
227
+ def evaluate_multi_not(scenario: [])
228
+ left = @stack_answer.pop.strip
229
+
230
+ # binding.pry
231
+
232
+ answer = if LogicHelper::TRUE == left
233
+ LogicHelper::FALSE
234
+ elsif LogicHelper::FALSE == left
235
+ LogicHelper::TRUE
236
+ else
237
+ subscript = extract_subscript(token: left)
238
+ if subscript.negative?
239
+ (!scenario.include?(left)).to_s
240
+ else
241
+ default_converter = DEFAULT_CONVERT_HASH[scenario.first.class]
242
+ converted = default_converter.convert(left[RE_TOKEN_BODY])
243
+ (scenario[subscript] == converted).to_s
244
+ end
245
+ end
246
+
247
+ @stack_answer << if answer[0] == '|'
248
+ answer
249
+ else
250
+ "|#{answer}"
251
+ end
252
+ end
253
+
254
+ # /** @param scenario to evaluate against the rule expression. */
255
+ def evaluate_one_rpn(scenario: [])
256
+ single = @stack_rpn.last
257
+ subscript = extract_subscript(token: single)
258
+ if subscript > -1
259
+ default_converter = DEFAULT_CONVERT_HASH[scenario.first.class]
260
+ scenario[subscript] == default_converter.convert(single[RE_TOKEN_BODY])
261
+ else
262
+ scenario.include? single
263
+ end
264
+ end
265
+
266
+ def operator?(token: '')
267
+ !token.nil? && OPERATORS.map(&:symbol).include?(token)
268
+ end
269
+
270
+ def precedence(symbol_char: '')
271
+ RuleEvaluator.operator_from_symbol(symbol: symbol_char).precedence
272
+ end
273
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'rule_evaluator'
4
+
5
+ # undoc
6
+ class RuleProcessor
7
+ # /**
8
+ # * @param scenario current scenario.
9
+ # * @param caseFixture current test case fixture.
10
+ # */
11
+ def evaluate(scenario: [], fixture: nil)
12
+ # if scenario.empty? || fixture.nil?
13
+ # raise 'Scenario or fixture cannot be nil.'
14
+ # end
15
+
16
+ fixture[:spec].rule.outcomes.inject([]) do |retval, outcome|
17
+ process_outcome(
18
+ scenario: scenario,
19
+ fixture: fixture,
20
+ list: retval,
21
+ outcome: outcome
22
+ )
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def process_outcome(scenario: [], fixture: nil, list: [], outcome: '')
29
+ spec = fixture[:spec]
30
+
31
+ clause = spec.rule.clause(outcome: outcome)
32
+ rule_evaluator = RuleEvaluator.new(converters: spec.converters)
33
+
34
+ rule_evaluator.parse(expression: clause)
35
+
36
+ list << rule_evaluator.evaluate(
37
+ scenario: scenario,
38
+ rule_token_convert: fixture[:converter_hash]
39
+ )
40
+ end
41
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rast/assert'
4
+ require_relative 'rule_processor'
5
+
6
+ # Validates rules
7
+ class RuleValidator
8
+ def validate(scenario: [], fixture: {})
9
+ rule_result = RuleProcessor.new.evaluate(
10
+ scenario: scenario,
11
+ fixture: fixture
12
+ )
13
+
14
+ spec = fixture[:spec]
15
+ rule = spec.rule
16
+
17
+ single_result = rule.size == 1
18
+ if single_result
19
+ next_result = rule_result.first
20
+ outcome = rule.outcomes.first
21
+ binary_outcome(outcome: outcome, spec: spec, expected: next_result)
22
+ else
23
+ validate_multi(scenario: scenario, spec: spec, rule_result: rule_result)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def validate_multi(scenario: [], spec: nil, rule_result: [])
30
+ # binding.pry
31
+
32
+ matched_outputs = []
33
+ match_count = 0
34
+
35
+ rule_result.map { |result| result == 'true' }.each_with_index do |result, i|
36
+ next unless result
37
+
38
+ match_count += 1
39
+ matched_outputs << spec.rule.outcomes[i]
40
+ end
41
+ assert("Scenario must fall into a unique rule output/clause:
42
+ #{scenario} , matched: #{matched_outputs}") { match_count == 1 }
43
+
44
+ matched_outputs.first
45
+ end
46
+
47
+ def binary_outcome(outcome: '', spec: nil, expected: false)
48
+ is_positive = spec.pair.keys.include?(outcome)
49
+ if is_positive
50
+ expected == 'true' ? outcome : opposite(outcome: outcome, spec: spec)
51
+ else
52
+ expected == 'true' ? opposite(outcome: outcome, spec: spec) : outcome
53
+ end
54
+ end
55
+
56
+ def opposite(outcome: '', spec: nil)
57
+ if spec.pair.keys.include? outcome
58
+ spec.pair[outcome]
59
+ else
60
+ spec.pair_reversed[outcome]
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'parameter_generator'
4
+
5
+ # Main DSL. This is the entry point of the test when running a spec.
6
+ class SpecDSL
7
+ attr_accessor :subject, :rspec_methods, :execute_block, :prepare_block,
8
+ :transients, :outcomes, :fixtures
9
+
10
+ def initialize(subject: nil, fixtures: [], &block)
11
+ @subject = subject
12
+ @fixtures = fixtures
13
+
14
+ @transients = []
15
+ @result = nil
16
+ @rspec_methods = []
17
+
18
+ instance_eval(&block)
19
+ end
20
+
21
+ def result(outcome)
22
+ @outcome = outcome.to_s
23
+ end
24
+
25
+ def method_missing(method_name_symbol, *args, &block)
26
+ return super if method_name_symbol == :to_ary
27
+
28
+ @rspec_methods << {
29
+ name: method_name_symbol,
30
+ args: args.first,
31
+ block: block
32
+ }
33
+
34
+ self
35
+ end
36
+
37
+ def prepare(&block)
38
+ @prepare_block = block
39
+ @transients
40
+ end
41
+
42
+ def execute(&block)
43
+ @execute_block = block
44
+
45
+ @fixtures.sort_by! { |fixture| fixture[:expected_outcome] }
46
+ generate_rspecs
47
+ end
48
+
49
+ private
50
+
51
+ def generate_rspecs
52
+ spec = @fixtures.first[:spec]
53
+ main_scope = self
54
+
55
+ RSpec.describe "#{@subject.class}: #{spec.description}" do
56
+ main_scope.fixtures.each do |fixture|
57
+ generate_rspec(
58
+ scope: main_scope,
59
+ scenario: fixture[:scenario],
60
+ expected: fixture[:expected_outcome]
61
+ )
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ def generate_rspec(scope: nil, scenario: {}, expected: '')
68
+ spec_params = scenario.keys.inject('') do |output, key|
69
+ output += ', ' unless output == ''
70
+ output + "#{key}: #{scenario[key]}"
71
+ end
72
+
73
+ it "[#{expected}]=[#{spec_params}]" do
74
+ block_params = scenario.values
75
+ scope.prepare_block&.call(*block_params) unless scope.rspec_methods.any?
76
+
77
+ while scope.rspec_methods.any?
78
+ first_meth = scope.rspec_methods.shift
79
+ second_meth = scope.rspec_methods.shift
80
+ if first_meth[:name] == :allow && second_meth[:name] == :receive
81
+ allow(scope.subject)
82
+ .to receive(second_meth[:args], &second_meth[:block])
83
+ end
84
+ scope.rspec_methods.shift
85
+ end
86
+
87
+ actual = scope.execute_block.call(*block_params).to_s
88
+
89
+ expect(actual).to eq(expected)
90
+ end
91
+ end
92
+
93
+ # DSL Entry Point
94
+ def spec(subject: nil, fixtures: [], &block)
95
+ SpecDSL.new(subject: subject, fixtures: fixtures, &block)
96
+ end
@@ -0,0 +1,3 @@
1
+ module Rast
2
+ VERSION = '0.1.0.pre'.freeze
3
+ end
data/lib/rast.rb ADDED
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec'
4
+ require_relative 'rast/parameter_generator'
5
+ require_relative 'rast/spec_dsl'
6
+
7
+ # Main DSL. This is the entry point of the test when running a spec.
8
+ class Rast
9
+ # RSpec All scenario test
10
+ #
11
+ # Example:
12
+ # >> Hola.hi("spanish")
13
+ # => hola mundo
14
+ #
15
+ # Arguments:
16
+ # language: (String)
17
+
18
+
19
+
20
+ alias global_spec spec
21
+
22
+ def initialize(rasted_class, &block)
23
+ @rasted_class = rasted_class
24
+ @subject = rasted_class.new
25
+
26
+ spec_path = caller[2][/spec.*?\.rb/]
27
+ yaml_path = spec_path.gsub(/(\w+).rb/, 'rast/\\1.yml')
28
+
29
+ @generator = ParameterGenerator.new(yaml_path: yaml_path)
30
+
31
+ instance_eval(&block)
32
+ end
33
+
34
+ def spec(id, &block)
35
+ global_spec(
36
+ subject: @subject,
37
+ fixtures: @generator.generate_fixtures(spec_id: id),
38
+ &block
39
+ )
40
+ end
41
+ end
42
+
43
+ # DSL Entry Point
44
+ def rast(rasted_class, &block)
45
+ Rast.new(rasted_class, &block)
46
+ end
@@ -0,0 +1,18 @@
1
+ ---
2
+ specs:
3
+ spec_key:
4
+ description: Spec Description
5
+
6
+ variables:
7
+ param1:
8
+ - false
9
+ - true
10
+
11
+ converters:
12
+ - BoolConverter
13
+
14
+ rules:
15
+ 'true': 'true[0]'
16
+
17
+ pair:
18
+ 'true': 'false'
data/rast.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/rast/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'rast'
7
+ spec.version = Rast::VERSION
8
+ spec.authors = ['Royce Remulla']
9
+ spec.email = ['royce.com@gmail.com']
10
+
11
+ spec.summary = 'RSpec AST - All Scenario Testing'
12
+ spec.description = 'Extends RSpec functionality by using the catch-all-scenario testing (CAST) principle.'
13
+ spec.homepage = 'https://github.com/roycetech/rast'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
16
+
17
+ # spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
18
+ # spec.metadata['homepage_uri'] = spec.homepage
19
+ # spec.metadata['source_code_uri'] = "TODO: Put your gem's public repo URL here."
20
+ # spec.metadata['changelog_uri'] = "TODO: Put your gem's CHANGELOG.md URL here."
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
+ end
27
+ spec.bindir = 'exe'
28
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
+ spec.require_paths = %w[lib]
30
+ end