decision_agent 0.2.0 → 1.0.1
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/README.md +313 -8
- data/bin/decision_agent +104 -0
- data/lib/decision_agent/agent.rb +72 -1
- data/lib/decision_agent/context.rb +1 -0
- data/lib/decision_agent/data_enrichment/cache/memory_adapter.rb +86 -0
- data/lib/decision_agent/data_enrichment/cache_adapter.rb +49 -0
- data/lib/decision_agent/data_enrichment/circuit_breaker.rb +135 -0
- data/lib/decision_agent/data_enrichment/client.rb +220 -0
- data/lib/decision_agent/data_enrichment/config.rb +78 -0
- data/lib/decision_agent/data_enrichment/errors.rb +36 -0
- data/lib/decision_agent/decision.rb +102 -2
- data/lib/decision_agent/dmn/adapter.rb +135 -0
- data/lib/decision_agent/dmn/cache.rb +306 -0
- data/lib/decision_agent/dmn/decision_graph.rb +327 -0
- data/lib/decision_agent/dmn/decision_tree.rb +192 -0
- data/lib/decision_agent/dmn/errors.rb +30 -0
- data/lib/decision_agent/dmn/exporter.rb +217 -0
- data/lib/decision_agent/dmn/feel/evaluator.rb +819 -0
- data/lib/decision_agent/dmn/feel/functions.rb +420 -0
- data/lib/decision_agent/dmn/feel/parser.rb +349 -0
- data/lib/decision_agent/dmn/feel/simple_parser.rb +276 -0
- data/lib/decision_agent/dmn/feel/transformer.rb +372 -0
- data/lib/decision_agent/dmn/feel/types.rb +276 -0
- data/lib/decision_agent/dmn/importer.rb +77 -0
- data/lib/decision_agent/dmn/model.rb +197 -0
- data/lib/decision_agent/dmn/parser.rb +191 -0
- data/lib/decision_agent/dmn/testing.rb +333 -0
- data/lib/decision_agent/dmn/validator.rb +315 -0
- data/lib/decision_agent/dmn/versioning.rb +229 -0
- data/lib/decision_agent/dmn/visualizer.rb +513 -0
- data/lib/decision_agent/dsl/condition_evaluator.rb +984 -838
- data/lib/decision_agent/dsl/schema_validator.rb +53 -14
- data/lib/decision_agent/evaluators/dmn_evaluator.rb +308 -0
- data/lib/decision_agent/evaluators/json_rule_evaluator.rb +69 -9
- data/lib/decision_agent/explainability/condition_trace.rb +83 -0
- data/lib/decision_agent/explainability/explainability_result.rb +52 -0
- data/lib/decision_agent/explainability/rule_trace.rb +39 -0
- data/lib/decision_agent/explainability/trace_collector.rb +24 -0
- data/lib/decision_agent/monitoring/alert_manager.rb +5 -1
- data/lib/decision_agent/simulation/errors.rb +18 -0
- data/lib/decision_agent/simulation/impact_analyzer.rb +498 -0
- data/lib/decision_agent/simulation/monte_carlo_simulator.rb +635 -0
- data/lib/decision_agent/simulation/replay_engine.rb +486 -0
- data/lib/decision_agent/simulation/scenario_engine.rb +318 -0
- data/lib/decision_agent/simulation/scenario_library.rb +163 -0
- data/lib/decision_agent/simulation/shadow_test_engine.rb +287 -0
- data/lib/decision_agent/simulation/what_if_analyzer.rb +1002 -0
- data/lib/decision_agent/simulation.rb +17 -0
- data/lib/decision_agent/version.rb +1 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +23 -8
- data/lib/decision_agent/web/dmn_editor.rb +426 -0
- data/lib/decision_agent/web/public/app.js +119 -0
- data/lib/decision_agent/web/public/dmn-editor.css +596 -0
- data/lib/decision_agent/web/public/dmn-editor.html +250 -0
- data/lib/decision_agent/web/public/dmn-editor.js +553 -0
- data/lib/decision_agent/web/public/index.html +52 -0
- data/lib/decision_agent/web/public/simulation.html +130 -0
- data/lib/decision_agent/web/public/simulation_impact.html +478 -0
- data/lib/decision_agent/web/public/simulation_replay.html +551 -0
- data/lib/decision_agent/web/public/simulation_shadow.html +546 -0
- data/lib/decision_agent/web/public/simulation_whatif.html +532 -0
- data/lib/decision_agent/web/public/styles.css +86 -0
- data/lib/decision_agent/web/server.rb +1059 -23
- data/lib/decision_agent.rb +60 -2
- metadata +105 -61
- data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
- data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
- data/spec/ab_testing/ab_test_spec.rb +0 -270
- data/spec/ab_testing/ab_testing_agent_spec.rb +0 -481
- data/spec/ab_testing/storage/adapter_spec.rb +0 -64
- data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
- data/spec/activerecord_thread_safety_spec.rb +0 -553
- data/spec/advanced_operators_spec.rb +0 -3150
- data/spec/agent_spec.rb +0 -289
- data/spec/api_contract_spec.rb +0 -430
- data/spec/audit_adapters_spec.rb +0 -92
- data/spec/auth/access_audit_logger_spec.rb +0 -394
- data/spec/auth/authenticator_spec.rb +0 -112
- data/spec/auth/password_reset_spec.rb +0 -294
- data/spec/auth/permission_checker_spec.rb +0 -207
- data/spec/auth/permission_spec.rb +0 -73
- data/spec/auth/rbac_adapter_spec.rb +0 -550
- data/spec/auth/rbac_config_spec.rb +0 -82
- data/spec/auth/role_spec.rb +0 -51
- data/spec/auth/session_manager_spec.rb +0 -172
- data/spec/auth/session_spec.rb +0 -112
- data/spec/auth/user_spec.rb +0 -130
- data/spec/comprehensive_edge_cases_spec.rb +0 -1777
- data/spec/context_spec.rb +0 -127
- data/spec/decision_agent_spec.rb +0 -96
- data/spec/decision_spec.rb +0 -423
- data/spec/dsl/condition_evaluator_spec.rb +0 -774
- data/spec/dsl_validation_spec.rb +0 -648
- data/spec/edge_cases_spec.rb +0 -353
- data/spec/evaluation_spec.rb +0 -364
- data/spec/evaluation_validator_spec.rb +0 -165
- data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
- data/spec/examples.txt +0 -1633
- data/spec/issue_verification_spec.rb +0 -759
- data/spec/json_rule_evaluator_spec.rb +0 -587
- data/spec/monitoring/alert_manager_spec.rb +0 -378
- data/spec/monitoring/metrics_collector_spec.rb +0 -499
- data/spec/monitoring/monitored_agent_spec.rb +0 -222
- data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
- data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
- data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
- data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
- data/spec/performance_optimizations_spec.rb +0 -486
- data/spec/replay_edge_cases_spec.rb +0 -699
- data/spec/replay_spec.rb +0 -210
- data/spec/rfc8785_canonicalization_spec.rb +0 -215
- data/spec/scoring_spec.rb +0 -225
- data/spec/spec_helper.rb +0 -60
- data/spec/testing/batch_test_importer_spec.rb +0 -693
- data/spec/testing/batch_test_runner_spec.rb +0 -307
- data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
- data/spec/testing/test_result_comparator_spec.rb +0 -392
- data/spec/testing/test_scenario_spec.rb +0 -113
- data/spec/thread_safety_spec.rb +0 -482
- data/spec/thread_safety_spec.rb.broken +0 -878
- data/spec/versioning/adapter_spec.rb +0 -156
- data/spec/versioning_spec.rb +0 -1030
- data/spec/web/middleware/auth_middleware_spec.rb +0 -133
- data/spec/web/middleware/permission_middleware_spec.rb +0 -247
- data/spec/web_ui_rack_spec.rb +0 -1840
|
@@ -0,0 +1,819 @@
|
|
|
1
|
+
require_relative "../errors"
|
|
2
|
+
require_relative "simple_parser"
|
|
3
|
+
require_relative "parser"
|
|
4
|
+
require_relative "transformer"
|
|
5
|
+
require_relative "functions"
|
|
6
|
+
require_relative "types"
|
|
7
|
+
require_relative "../../dsl/condition_evaluator"
|
|
8
|
+
|
|
9
|
+
module DecisionAgent
|
|
10
|
+
module Dmn
|
|
11
|
+
module Feel
|
|
12
|
+
# FEEL expression evaluator with hybrid parsing strategy
|
|
13
|
+
# Phase 2A: Basic comparisons, ranges, list membership (regex-based)
|
|
14
|
+
# Phase 2B: Arithmetic, logical operators, functions (enhanced parser)
|
|
15
|
+
# Maps FEEL expressions to DecisionAgent ConditionEvaluator
|
|
16
|
+
# rubocop:disable Metrics/ClassLength
|
|
17
|
+
class Evaluator
|
|
18
|
+
def initialize
|
|
19
|
+
@simple_parser = SimpleParser.new
|
|
20
|
+
@parslet_parser = Parser.new
|
|
21
|
+
@transformer = Transformer.new
|
|
22
|
+
@cache = {}
|
|
23
|
+
@cache_mutex = Mutex.new
|
|
24
|
+
@ast_cache = {} # Cache for Parslet AST nodes
|
|
25
|
+
@use_parslet = true # Enable full Parslet parser
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Evaluate a FEEL expression against a context
|
|
29
|
+
# @param expression [String] FEEL expression (e.g., ">= 18", "in [1,2,3]", "age + 5")
|
|
30
|
+
# @param field_name [String] The field name being evaluated
|
|
31
|
+
# @param context [Hash] Evaluation context
|
|
32
|
+
# @return [Boolean] Evaluation result
|
|
33
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
34
|
+
def evaluate(expression, field_name, context)
|
|
35
|
+
return true if expression == "-" # DMN "don't care" marker
|
|
36
|
+
|
|
37
|
+
# Try Parslet parser first (Phase 2B)
|
|
38
|
+
if @use_parslet
|
|
39
|
+
begin
|
|
40
|
+
expr_key = expression.to_s.strip
|
|
41
|
+
|
|
42
|
+
# Check AST cache first
|
|
43
|
+
ast = @cache_mutex.synchronize do
|
|
44
|
+
@ast_cache[expr_key]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
if ast.nil?
|
|
48
|
+
parse_tree = @parslet_parser.parse(expr_key)
|
|
49
|
+
ast = @transformer.apply(parse_tree)
|
|
50
|
+
@cache_mutex.synchronize do
|
|
51
|
+
@ast_cache[expr_key] = ast
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
result = evaluate_ast_node(ast, context)
|
|
56
|
+
# If result is nil and AST is a simple field reference that doesn't exist in context,
|
|
57
|
+
# fall back to Phase 2A approach to return condition structure
|
|
58
|
+
unless result.nil? && ast.is_a?(Hash) && ast[:type] == :field &&
|
|
59
|
+
!context.key?(ast[:name]) && !context.key?(ast[:name].to_s) &&
|
|
60
|
+
!context.key?(ast[:name].to_sym)
|
|
61
|
+
return result
|
|
62
|
+
end
|
|
63
|
+
# Fall through to Phase 2A
|
|
64
|
+
rescue Parslet::ParseFailed, FeelParseError, FeelTransformError => e
|
|
65
|
+
# Fall back to Phase 2A approach
|
|
66
|
+
warn "Parslet parse failed: #{e.message}, falling back" if ENV["DEBUG_FEEL"]
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Phase 2A approach: use condition structures
|
|
71
|
+
# Check cache first (thread-safe)
|
|
72
|
+
cache_key = "#{expression}::#{field_name}"
|
|
73
|
+
condition = @cache_mutex.synchronize do
|
|
74
|
+
@cache[cache_key]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
return Dsl::ConditionEvaluator.evaluate(condition, context) if condition
|
|
78
|
+
|
|
79
|
+
# Parse and translate expression to condition structure
|
|
80
|
+
expr_str = expression.to_s.strip
|
|
81
|
+
|
|
82
|
+
# Check if expression matches any known pattern that can be successfully parsed
|
|
83
|
+
is_supported = literal?(expr_str) ||
|
|
84
|
+
comparison_expression?(expr_str) ||
|
|
85
|
+
list_expression?(expr_str) ||
|
|
86
|
+
range_expression?(expr_str)
|
|
87
|
+
|
|
88
|
+
# For SimpleParser, check if it can actually parse successfully
|
|
89
|
+
if SimpleParser.can_parse?(expr_str)
|
|
90
|
+
begin
|
|
91
|
+
@simple_parser.parse(expr_str)
|
|
92
|
+
is_supported = true
|
|
93
|
+
rescue FeelParseError
|
|
94
|
+
# SimpleParser says it can parse, but actually can't - not supported
|
|
95
|
+
is_supported = false
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
99
|
+
|
|
100
|
+
condition = parse_expression_to_condition(expression, field_name, context)
|
|
101
|
+
|
|
102
|
+
# If parse_expression_to_condition returned nil, create default condition structure
|
|
103
|
+
unless condition.is_a?(Hash)
|
|
104
|
+
condition = {
|
|
105
|
+
"field" => field_name,
|
|
106
|
+
"op" => "eq",
|
|
107
|
+
"value" => parse_value(expr_str)
|
|
108
|
+
}
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Store in cache (thread-safe)
|
|
112
|
+
@cache_mutex.synchronize do
|
|
113
|
+
@cache[cache_key] = condition
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# For completely unsupported expressions (no patterns matched), return condition structure
|
|
117
|
+
# This allows fallback to literal equality for unknown syntax
|
|
118
|
+
return condition unless is_supported
|
|
119
|
+
|
|
120
|
+
# Delegate to existing ConditionEvaluator for supported expressions
|
|
121
|
+
evaluation_result = Dsl::ConditionEvaluator.evaluate(condition, context)
|
|
122
|
+
|
|
123
|
+
# If evaluation returns false for a simple equality check and the field doesn't exist in context,
|
|
124
|
+
# treat as unsupported expression and return condition structure (fallback behavior)
|
|
125
|
+
if evaluation_result == false && condition["op"] == "eq"
|
|
126
|
+
field_key = condition["field"]
|
|
127
|
+
field_exists = context.key?(field_key) || context.key?(field_key.to_s) || context.key?(field_key.to_sym)
|
|
128
|
+
return condition unless field_exists
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# If evaluation returns nil, return condition structure as fallback
|
|
132
|
+
return condition if evaluation_result.nil?
|
|
133
|
+
|
|
134
|
+
evaluation_result
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Parse FEEL expression into operator and value (for internal use by Adapter)
|
|
138
|
+
# This maintains backward compatibility with Phase 2A
|
|
139
|
+
def parse_expression(expr)
|
|
140
|
+
expr = expr.to_s.strip
|
|
141
|
+
|
|
142
|
+
# Handle literal values (quoted strings, numbers, booleans)
|
|
143
|
+
return parse_literal(expr) if literal?(expr)
|
|
144
|
+
|
|
145
|
+
# Handle comparison operators
|
|
146
|
+
return parse_comparison(expr) if comparison_expression?(expr)
|
|
147
|
+
|
|
148
|
+
# Handle list membership
|
|
149
|
+
return parse_list_membership(expr) if list_expression?(expr)
|
|
150
|
+
|
|
151
|
+
# Handle range expressions
|
|
152
|
+
return parse_range(expr) if range_expression?(expr)
|
|
153
|
+
|
|
154
|
+
# Check if it's a simple parsable expression (arithmetic/logical)
|
|
155
|
+
if SimpleParser.can_parse?(expr)
|
|
156
|
+
begin
|
|
157
|
+
ast = @simple_parser.parse(expr)
|
|
158
|
+
return translate_ast(ast, nil)
|
|
159
|
+
rescue FeelParseError
|
|
160
|
+
# Fall back to literal equality
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Default: equality
|
|
165
|
+
{ operator: "eq", value: parse_value(expr) }
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
private
|
|
169
|
+
|
|
170
|
+
def literal?(expr)
|
|
171
|
+
# Quoted string
|
|
172
|
+
return true if expr.start_with?('"') && expr.end_with?('"')
|
|
173
|
+
# Number
|
|
174
|
+
return true if expr.match?(/^-?\d+(\.\d+)?$/)
|
|
175
|
+
# Boolean
|
|
176
|
+
return true if %w[true false].include?(expr.downcase)
|
|
177
|
+
|
|
178
|
+
false
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def parse_literal(expr)
|
|
182
|
+
if expr.start_with?('"') && expr.end_with?('"')
|
|
183
|
+
# String literal
|
|
184
|
+
{ operator: "eq", value: expr[1..-2] }
|
|
185
|
+
elsif expr.match?(/^-?\d+\.\d+$/)
|
|
186
|
+
# Float
|
|
187
|
+
{ operator: "eq", value: expr.to_f }
|
|
188
|
+
elsif expr.match?(/^-?\d+$/)
|
|
189
|
+
# Integer
|
|
190
|
+
{ operator: "eq", value: expr.to_i }
|
|
191
|
+
elsif expr.downcase == "true"
|
|
192
|
+
{ operator: "eq", value: true }
|
|
193
|
+
elsif expr.downcase == "false"
|
|
194
|
+
{ operator: "eq", value: false }
|
|
195
|
+
else
|
|
196
|
+
{ operator: "eq", value: expr }
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def comparison_expression?(expr)
|
|
201
|
+
expr.match?(/^(>=|<=|>|<|!=|=)/)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def parse_comparison(expr)
|
|
205
|
+
# Extract operator
|
|
206
|
+
operator_match = expr.match(/^(>=|<=|>|<|!=|=)\s*(.+)/)
|
|
207
|
+
return { operator: "eq", value: expr } unless operator_match
|
|
208
|
+
|
|
209
|
+
feel_op = operator_match[1]
|
|
210
|
+
value_str = operator_match[2]
|
|
211
|
+
|
|
212
|
+
# Map FEEL operator to ConditionEvaluator operator
|
|
213
|
+
condition_op = case feel_op
|
|
214
|
+
when ">=" then "gte"
|
|
215
|
+
when "<=" then "lte"
|
|
216
|
+
when ">" then "gt"
|
|
217
|
+
when "<" then "lt"
|
|
218
|
+
when "!=" then "neq"
|
|
219
|
+
when "=" then "eq"
|
|
220
|
+
else "eq"
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
{ operator: condition_op, value: parse_value(value_str) }
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def list_expression?(expr)
|
|
227
|
+
expr.match?(/\[.+\]/)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def parse_list_membership(expr)
|
|
231
|
+
# Handle "in [1, 2, 3]" or just "[1, 2, 3]"
|
|
232
|
+
list_match = expr.match(/(?:in\s+)?\[(.+)\]/)
|
|
233
|
+
return { operator: "eq", value: expr } unless list_match
|
|
234
|
+
|
|
235
|
+
items_str = list_match[1]
|
|
236
|
+
items = items_str.split(",").map { |item| parse_value(item.strip) }
|
|
237
|
+
|
|
238
|
+
{ operator: "in", value: items }
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def range_expression?(expr)
|
|
242
|
+
# FEEL ranges like "[10..20]", "(10..20)", etc.
|
|
243
|
+
expr.match?(/[\[(]\d+(\.\d+)?\.\.\d+(\.\d+)?[\])]/)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def parse_range(expr)
|
|
247
|
+
# Parse FEEL range syntax: [min..max], (min..max), [min..max), (min..max]
|
|
248
|
+
range_match = expr.match(/([\[(])(\d+(?:\.\d+)?)\.\.(\d+(?:\.\d+)?)([\])])/)
|
|
249
|
+
return { operator: "eq", value: expr } unless range_match
|
|
250
|
+
|
|
251
|
+
inclusive_start = range_match[1] == "["
|
|
252
|
+
min_val = parse_value(range_match[2])
|
|
253
|
+
max_val = parse_value(range_match[3])
|
|
254
|
+
inclusive_end = range_match[4] == "]"
|
|
255
|
+
|
|
256
|
+
# For Phase 2A, we support fully inclusive ranges with 'between' operator
|
|
257
|
+
if inclusive_start && inclusive_end
|
|
258
|
+
{ operator: "between", value: [min_val, max_val] }
|
|
259
|
+
else
|
|
260
|
+
# For half-open ranges, convert to inclusive by adjusting bounds
|
|
261
|
+
# [min..max) becomes [min..max-1] (if max is integer) or use compound conditions
|
|
262
|
+
# For simplicity, we'll convert to inclusive ranges with adjusted bounds
|
|
263
|
+
# This is a pragmatic approach for Phase 2A
|
|
264
|
+
adjusted_min = if inclusive_start
|
|
265
|
+
min_val
|
|
266
|
+
elsif min_val.is_a?(Integer)
|
|
267
|
+
min_val + 1
|
|
268
|
+
else
|
|
269
|
+
min_val + 0.0001
|
|
270
|
+
end
|
|
271
|
+
adjusted_max = if inclusive_end
|
|
272
|
+
max_val
|
|
273
|
+
elsif max_val.is_a?(Integer)
|
|
274
|
+
max_val - 1
|
|
275
|
+
else
|
|
276
|
+
max_val - 0.0001
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Ensure adjusted range is valid
|
|
280
|
+
if adjusted_min <= adjusted_max
|
|
281
|
+
{ operator: "between", value: [adjusted_min, adjusted_max] }
|
|
282
|
+
else
|
|
283
|
+
# Invalid range, fall back to error
|
|
284
|
+
raise FeelParseError,
|
|
285
|
+
"Invalid half-open range: #{expr}. Range would be empty after adjustment."
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def parse_value(str)
|
|
291
|
+
str = str.to_s.strip
|
|
292
|
+
|
|
293
|
+
# Remove quotes
|
|
294
|
+
return str[1..-2] if str.start_with?('"') && str.end_with?('"')
|
|
295
|
+
|
|
296
|
+
# Try to parse as number
|
|
297
|
+
if str.match?(/^-?\d+\.\d+$/)
|
|
298
|
+
return str.to_f
|
|
299
|
+
elsif str.match?(/^-?\d+$/)
|
|
300
|
+
return str.to_i
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Boolean
|
|
304
|
+
return true if str.downcase == "true"
|
|
305
|
+
return false if str.downcase == "false"
|
|
306
|
+
|
|
307
|
+
# Return as string
|
|
308
|
+
str
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Parse expression to condition structure (Phase 2A backward compatibility)
|
|
312
|
+
def parse_expression_to_condition(expression, field_name, context)
|
|
313
|
+
expr = expression.to_s.strip
|
|
314
|
+
|
|
315
|
+
# Try Phase 2A patterns
|
|
316
|
+
if literal?(expr) || comparison_expression?(expr) || list_expression?(expr) || range_expression?(expr)
|
|
317
|
+
parsed = parse_expression(expr)
|
|
318
|
+
return {
|
|
319
|
+
"field" => field_name,
|
|
320
|
+
"op" => parsed[:operator],
|
|
321
|
+
"value" => parsed[:value]
|
|
322
|
+
}
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Try simple parser for arithmetic/logical expressions
|
|
326
|
+
if SimpleParser.can_parse?(expr)
|
|
327
|
+
begin
|
|
328
|
+
ast = @simple_parser.parse(expr)
|
|
329
|
+
translated = translate_ast(ast, field_name, context)
|
|
330
|
+
# If translate_ast returns a valid Hash, return it
|
|
331
|
+
# Otherwise, fall through to default literal equality
|
|
332
|
+
return translated if translated.is_a?(Hash)
|
|
333
|
+
rescue FeelParseError, StandardError => e
|
|
334
|
+
warn "FEEL parse warning: #{e.message}, falling back to literal match" if ENV["DEBUG_FEEL"]
|
|
335
|
+
# Fall through to default
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Default: literal equality - always return a Hash
|
|
340
|
+
{
|
|
341
|
+
"field" => field_name,
|
|
342
|
+
"op" => "eq",
|
|
343
|
+
"value" => parse_value(expr)
|
|
344
|
+
}
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Translate AST to ConditionEvaluator format
|
|
348
|
+
def translate_ast(node, field_name, context = {})
|
|
349
|
+
case node[:type]
|
|
350
|
+
when :literal
|
|
351
|
+
# Just a value
|
|
352
|
+
return node[:value] if field_name.nil?
|
|
353
|
+
|
|
354
|
+
{ "field" => field_name, "op" => "eq", "value" => node[:value] }
|
|
355
|
+
|
|
356
|
+
when :field
|
|
357
|
+
# Field reference - get value from context
|
|
358
|
+
context.to_h[node[:name].to_sym] || context.to_h[node[:name]]
|
|
359
|
+
|
|
360
|
+
when :arithmetic
|
|
361
|
+
translate_arithmetic(node, field_name, context)
|
|
362
|
+
|
|
363
|
+
when :logical
|
|
364
|
+
translate_logical(node, field_name, context)
|
|
365
|
+
|
|
366
|
+
when :comparison
|
|
367
|
+
translate_comparison(node, field_name, context)
|
|
368
|
+
|
|
369
|
+
else
|
|
370
|
+
raise FeelEvaluationError.new("Unknown AST node type: #{node[:type]}", expression: node.inspect)
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Translate arithmetic operations
|
|
375
|
+
def translate_arithmetic(node, _field_name, context)
|
|
376
|
+
op = node[:operator]
|
|
377
|
+
|
|
378
|
+
if op == "negate"
|
|
379
|
+
# Unary negation
|
|
380
|
+
operand_val = evaluate_ast_value(node[:operand], context)
|
|
381
|
+
return -operand_val
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# Binary arithmetic
|
|
385
|
+
left_val = evaluate_ast_value(node[:left], context)
|
|
386
|
+
right_val = evaluate_ast_value(node[:right], context)
|
|
387
|
+
|
|
388
|
+
case op
|
|
389
|
+
when "+" then left_val + right_val
|
|
390
|
+
when "-" then left_val - right_val
|
|
391
|
+
when "*" then left_val * right_val
|
|
392
|
+
when "/" then left_val / right_val.to_f
|
|
393
|
+
when "**" then left_val**right_val
|
|
394
|
+
when "%" then left_val % right_val
|
|
395
|
+
else
|
|
396
|
+
raise FeelEvaluationError, "Unknown arithmetic operator: #{op}"
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Translate logical operations
|
|
401
|
+
def translate_logical(node, field_name, context)
|
|
402
|
+
op = node[:operator]
|
|
403
|
+
|
|
404
|
+
if op == "not"
|
|
405
|
+
# Unary NOT
|
|
406
|
+
operand = translate_ast(node[:operand], nil, context)
|
|
407
|
+
return { "all" => [{ "field" => field_name, "op" => "eq", "value" => false }] } if operand == true
|
|
408
|
+
return { "all" => [{ "field" => field_name, "op" => "eq", "value" => true }] } if operand == false
|
|
409
|
+
|
|
410
|
+
return !operand
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
# Binary logical (and, or)
|
|
414
|
+
left_condition = ast_to_condition(node[:left], field_name, context)
|
|
415
|
+
right_condition = ast_to_condition(node[:right], field_name, context)
|
|
416
|
+
|
|
417
|
+
case op
|
|
418
|
+
when "and"
|
|
419
|
+
{ "all" => [left_condition, right_condition] }
|
|
420
|
+
when "or"
|
|
421
|
+
{ "any" => [left_condition, right_condition] }
|
|
422
|
+
else
|
|
423
|
+
raise FeelEvaluationError, "Unknown logical operator: #{op}"
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# Translate comparison operations
|
|
428
|
+
def translate_comparison(node, _field_name, context)
|
|
429
|
+
left_val = evaluate_ast_value(node[:left], context)
|
|
430
|
+
right_val = evaluate_ast_value(node[:right], context)
|
|
431
|
+
op = node[:operator]
|
|
432
|
+
|
|
433
|
+
# Map FEEL comparison to ConditionEvaluator operator
|
|
434
|
+
condition_op = case op
|
|
435
|
+
when ">=" then "gte"
|
|
436
|
+
when "<=" then "lte"
|
|
437
|
+
when ">" then "gt"
|
|
438
|
+
when "<" then "lt"
|
|
439
|
+
when "!=" then "neq"
|
|
440
|
+
when "=" then "eq"
|
|
441
|
+
else "eq"
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# If left side is a field reference, use it as the field
|
|
445
|
+
if node[:left][:type] == :field
|
|
446
|
+
return {
|
|
447
|
+
"field" => node[:left][:name],
|
|
448
|
+
"op" => condition_op,
|
|
449
|
+
"value" => right_val
|
|
450
|
+
}
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# Otherwise, evaluate both sides and return boolean result
|
|
454
|
+
case op
|
|
455
|
+
when ">=" then left_val >= right_val
|
|
456
|
+
when "<=" then left_val <= right_val
|
|
457
|
+
when ">" then left_val > right_val
|
|
458
|
+
when "<" then left_val < right_val
|
|
459
|
+
when "!=" then left_val != right_val
|
|
460
|
+
when "=" then left_val == right_val
|
|
461
|
+
else left_val == right_val
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# Convert AST node to condition structure
|
|
466
|
+
def ast_to_condition(node, field_name, context)
|
|
467
|
+
case node[:type]
|
|
468
|
+
when :comparison
|
|
469
|
+
translate_comparison(node, field_name, context)
|
|
470
|
+
when :logical
|
|
471
|
+
translate_logical(node, field_name, context)
|
|
472
|
+
when :field
|
|
473
|
+
# Field reference in boolean context
|
|
474
|
+
{ "field" => node[:name], "op" => "eq", "value" => true }
|
|
475
|
+
when :literal
|
|
476
|
+
# Literal in boolean context
|
|
477
|
+
{ "field" => field_name, "op" => "eq", "value" => node[:value] }
|
|
478
|
+
else
|
|
479
|
+
# Evaluate and create condition
|
|
480
|
+
val = translate_ast(node, nil, context)
|
|
481
|
+
{ "field" => field_name, "op" => "eq", "value" => val }
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
# Evaluate AST node to get a value (not a condition)
|
|
486
|
+
def evaluate_ast_value(node, context)
|
|
487
|
+
case node[:type]
|
|
488
|
+
when :literal
|
|
489
|
+
node[:value]
|
|
490
|
+
when :field
|
|
491
|
+
context.to_h[node[:name].to_sym] || context.to_h[node[:name]] || 0
|
|
492
|
+
when :arithmetic
|
|
493
|
+
translate_arithmetic(node, nil, context)
|
|
494
|
+
else
|
|
495
|
+
translate_ast(node, nil, context)
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# Evaluate Parslet AST node (Phase 2B - full FEEL support)
|
|
500
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
501
|
+
def evaluate_ast_node(node, context)
|
|
502
|
+
return node unless node.is_a?(Hash)
|
|
503
|
+
|
|
504
|
+
# Handle nodes without type - might be raw Parslet output
|
|
505
|
+
if node[:type].nil? || node[:type].to_s.empty?
|
|
506
|
+
# Try to extract value from common Parslet structures
|
|
507
|
+
return node[:value] if node.key?(:value)
|
|
508
|
+
return node[:number] if node.key?(:number)
|
|
509
|
+
return node[:string] if node.key?(:string)
|
|
510
|
+
return node[:boolean] if node.key?(:boolean)
|
|
511
|
+
|
|
512
|
+
return node
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
case node[:type]
|
|
516
|
+
when :number, :string, :boolean
|
|
517
|
+
node[:value]
|
|
518
|
+
when :null
|
|
519
|
+
nil
|
|
520
|
+
when :field
|
|
521
|
+
get_field_value(node[:name], context)
|
|
522
|
+
when :list, :list_literal
|
|
523
|
+
evaluate_list(node, context)
|
|
524
|
+
when :context, :context_literal
|
|
525
|
+
evaluate_context(node, context)
|
|
526
|
+
when :range
|
|
527
|
+
evaluate_range(node, context)
|
|
528
|
+
when :function_call
|
|
529
|
+
evaluate_function_call(node, context)
|
|
530
|
+
when :property_access
|
|
531
|
+
evaluate_property_access(node, context)
|
|
532
|
+
when :comparison
|
|
533
|
+
evaluate_comparison_node?(node, context)
|
|
534
|
+
when :arithmetic
|
|
535
|
+
evaluate_arithmetic_node(node, context)
|
|
536
|
+
when :logical
|
|
537
|
+
evaluate_logical_node(node, context)
|
|
538
|
+
when :conditional
|
|
539
|
+
evaluate_conditional(node, context)
|
|
540
|
+
when :quantified
|
|
541
|
+
evaluate_quantified(node, context)
|
|
542
|
+
when :for
|
|
543
|
+
evaluate_for(node, context)
|
|
544
|
+
when :filter
|
|
545
|
+
evaluate_filter(node, context)
|
|
546
|
+
when :between
|
|
547
|
+
evaluate_between?(node, context)
|
|
548
|
+
when :in
|
|
549
|
+
evaluate_in_node?(node, context)
|
|
550
|
+
when :instance_of
|
|
551
|
+
evaluate_instance_of?(node, context)
|
|
552
|
+
else
|
|
553
|
+
raise FeelEvaluationError, "Unknown AST node type: #{node[:type]}"
|
|
554
|
+
end
|
|
555
|
+
end
|
|
556
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
557
|
+
|
|
558
|
+
# Get field value from context
|
|
559
|
+
def get_field_value(field_name, context)
|
|
560
|
+
ctx = context.to_h
|
|
561
|
+
ctx[field_name.to_sym] || ctx[field_name] || ctx[field_name.to_s]
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
# Evaluate list literal
|
|
565
|
+
def evaluate_list(node, context)
|
|
566
|
+
return [] if node[:elements].nil? || node[:elements].empty?
|
|
567
|
+
|
|
568
|
+
Array(node[:elements]).map { |elem| evaluate_ast_node(elem, context) }
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
# Evaluate context literal
|
|
572
|
+
def evaluate_context(node, context)
|
|
573
|
+
result = {}
|
|
574
|
+
return result if node[:pairs].nil? || node[:pairs].empty?
|
|
575
|
+
|
|
576
|
+
node[:pairs].each do |key, value|
|
|
577
|
+
result[key.to_sym] = evaluate_ast_node(value, context)
|
|
578
|
+
end
|
|
579
|
+
result
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
# Evaluate range
|
|
583
|
+
def evaluate_range(node, context)
|
|
584
|
+
start_val = evaluate_ast_node(node[:start], context)
|
|
585
|
+
end_val = evaluate_ast_node(node[:end], context)
|
|
586
|
+
|
|
587
|
+
{
|
|
588
|
+
type: :range,
|
|
589
|
+
start: start_val,
|
|
590
|
+
end: end_val,
|
|
591
|
+
start_inclusive: node[:start_inclusive],
|
|
592
|
+
end_inclusive: node[:end_inclusive]
|
|
593
|
+
}
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
# Evaluate function call
|
|
597
|
+
def evaluate_function_call(node, context)
|
|
598
|
+
# Extract function name - could be a string or a field node
|
|
599
|
+
function_name = if node[:name].is_a?(Hash)
|
|
600
|
+
if node[:name][:type] == :field
|
|
601
|
+
node[:name][:name]
|
|
602
|
+
else
|
|
603
|
+
node[:name][:name] || node[:name][:identifier] || node[:name].to_s
|
|
604
|
+
end
|
|
605
|
+
else
|
|
606
|
+
node[:name]
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
args = Array(node[:arguments]).map { |arg| evaluate_ast_node(arg, context) }
|
|
610
|
+
|
|
611
|
+
Functions.execute(function_name.to_s, args, context)
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
# Evaluate property access
|
|
615
|
+
def evaluate_property_access(node, context)
|
|
616
|
+
object = evaluate_ast_node(node[:object], context)
|
|
617
|
+
property = node[:property]
|
|
618
|
+
|
|
619
|
+
case object
|
|
620
|
+
when Hash
|
|
621
|
+
object[property.to_sym] || object[property.to_s] || object[property]
|
|
622
|
+
when Types::Context
|
|
623
|
+
object[property.to_sym]
|
|
624
|
+
else
|
|
625
|
+
object.respond_to?(property) ? object.send(property) : nil
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
# Evaluate comparison node
|
|
630
|
+
def evaluate_comparison_node?(node, context)
|
|
631
|
+
left_val = evaluate_ast_node(node[:left], context)
|
|
632
|
+
right_val = evaluate_ast_node(node[:right], context)
|
|
633
|
+
|
|
634
|
+
case node[:operator]
|
|
635
|
+
when "=" then left_val == right_val
|
|
636
|
+
when "!=" then left_val != right_val
|
|
637
|
+
when "<" then left_val < right_val
|
|
638
|
+
when ">" then left_val > right_val
|
|
639
|
+
when "<=" then left_val <= right_val
|
|
640
|
+
when ">=" then left_val >= right_val
|
|
641
|
+
else false
|
|
642
|
+
end
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
# Evaluate arithmetic node
|
|
646
|
+
def evaluate_arithmetic_node(node, context)
|
|
647
|
+
if node[:operand]
|
|
648
|
+
# Unary operation
|
|
649
|
+
operand_val = evaluate_ast_node(node[:operand], context)
|
|
650
|
+
return node[:operator] == "negate" ? -operand_val : operand_val
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
# Binary operation
|
|
654
|
+
left_val = evaluate_ast_node(node[:left], context)
|
|
655
|
+
right_val = evaluate_ast_node(node[:right], context)
|
|
656
|
+
|
|
657
|
+
case node[:operator]
|
|
658
|
+
when "+" then left_val + right_val
|
|
659
|
+
when "-" then left_val - right_val
|
|
660
|
+
when "*" then left_val * right_val
|
|
661
|
+
when "/" then left_val / right_val.to_f
|
|
662
|
+
when "**" then left_val**right_val
|
|
663
|
+
when "%" then left_val % right_val
|
|
664
|
+
else 0
|
|
665
|
+
end
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
# Evaluate logical node
|
|
669
|
+
def evaluate_logical_node(node, context)
|
|
670
|
+
if node[:operand]
|
|
671
|
+
# Unary NOT
|
|
672
|
+
operand_val = evaluate_ast_node(node[:operand], context)
|
|
673
|
+
return !operand_val
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
# Binary operation
|
|
677
|
+
left_val = evaluate_ast_node(node[:left], context)
|
|
678
|
+
|
|
679
|
+
case node[:operator]
|
|
680
|
+
when "and"
|
|
681
|
+
return false unless left_val
|
|
682
|
+
|
|
683
|
+
right_val = evaluate_ast_node(node[:right], context)
|
|
684
|
+
left_val && right_val
|
|
685
|
+
when "or"
|
|
686
|
+
return true if left_val
|
|
687
|
+
|
|
688
|
+
right_val = evaluate_ast_node(node[:right], context)
|
|
689
|
+
left_val || right_val
|
|
690
|
+
else
|
|
691
|
+
false
|
|
692
|
+
end
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
# Evaluate if/then/else conditional
|
|
696
|
+
def evaluate_conditional(node, context)
|
|
697
|
+
condition_val = evaluate_ast_node(node[:condition], context)
|
|
698
|
+
|
|
699
|
+
if condition_val
|
|
700
|
+
evaluate_ast_node(node[:then_expr], context)
|
|
701
|
+
else
|
|
702
|
+
evaluate_ast_node(node[:else_expr], context)
|
|
703
|
+
end
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
# Evaluate quantified expression (some/every)
|
|
707
|
+
def evaluate_quantified(node, context)
|
|
708
|
+
list_val = evaluate_ast_node(node[:list], context)
|
|
709
|
+
return false unless list_val.is_a?(Array) || list_val.is_a?(Types::List)
|
|
710
|
+
|
|
711
|
+
variable = node[:variable]
|
|
712
|
+
|
|
713
|
+
case node[:quantifier]
|
|
714
|
+
when "some"
|
|
715
|
+
Array(list_val).any? do |item|
|
|
716
|
+
item_context = context.to_h.merge(variable.to_sym => item)
|
|
717
|
+
evaluate_ast_node(node[:condition], item_context)
|
|
718
|
+
end
|
|
719
|
+
when "every"
|
|
720
|
+
Array(list_val).all? do |item|
|
|
721
|
+
item_context = context.to_h.merge(variable.to_sym => item)
|
|
722
|
+
evaluate_ast_node(node[:condition], item_context)
|
|
723
|
+
end
|
|
724
|
+
else
|
|
725
|
+
false
|
|
726
|
+
end
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
# Evaluate for expression
|
|
730
|
+
def evaluate_for(node, context)
|
|
731
|
+
list_val = evaluate_ast_node(node[:list], context)
|
|
732
|
+
return [] unless list_val.is_a?(Array) || list_val.is_a?(Types::List)
|
|
733
|
+
|
|
734
|
+
variable = node[:variable]
|
|
735
|
+
|
|
736
|
+
Array(list_val).map do |item|
|
|
737
|
+
item_context = context.to_h.merge(variable.to_sym => item)
|
|
738
|
+
evaluate_ast_node(node[:return_expr], item_context)
|
|
739
|
+
end
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
# Evaluate filter expression
|
|
743
|
+
def evaluate_filter(node, context)
|
|
744
|
+
list_val = evaluate_ast_node(node[:list], context)
|
|
745
|
+
return [] unless list_val.is_a?(Array) || list_val.is_a?(Types::List)
|
|
746
|
+
|
|
747
|
+
Array(list_val).select do |item|
|
|
748
|
+
# For filter, use 'item' as the implicit variable
|
|
749
|
+
item_context = if item.is_a?(Hash)
|
|
750
|
+
context.to_h.merge(item)
|
|
751
|
+
else
|
|
752
|
+
context.to_h.merge(item: item)
|
|
753
|
+
end
|
|
754
|
+
evaluate_ast_node(node[:condition], item_context)
|
|
755
|
+
end
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
# Evaluate between expression
|
|
759
|
+
def evaluate_between?(node, context)
|
|
760
|
+
value = evaluate_ast_node(node[:value], context)
|
|
761
|
+
min_val = evaluate_ast_node(node[:min], context)
|
|
762
|
+
max_val = evaluate_ast_node(node[:max], context)
|
|
763
|
+
|
|
764
|
+
value.between?(min_val, max_val)
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
# Evaluate in expression
|
|
768
|
+
def evaluate_in_node?(node, context)
|
|
769
|
+
value = evaluate_ast_node(node[:value], context)
|
|
770
|
+
list_val = evaluate_ast_node(node[:list], context)
|
|
771
|
+
|
|
772
|
+
if list_val.is_a?(Array) || list_val.is_a?(Types::List)
|
|
773
|
+
Array(list_val).include?(value)
|
|
774
|
+
elsif list_val.is_a?(Hash) && list_val[:type] == :range
|
|
775
|
+
# Check if value is in range
|
|
776
|
+
in_range?(value, list_val)
|
|
777
|
+
else
|
|
778
|
+
false
|
|
779
|
+
end
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
# Evaluate instance of expression
|
|
783
|
+
def evaluate_instance_of?(node, context)
|
|
784
|
+
value = evaluate_ast_node(node[:value], context)
|
|
785
|
+
type_name = node[:type_name]
|
|
786
|
+
|
|
787
|
+
case type_name
|
|
788
|
+
when "number"
|
|
789
|
+
value.is_a?(Numeric)
|
|
790
|
+
when "string"
|
|
791
|
+
value.is_a?(String)
|
|
792
|
+
when "boolean"
|
|
793
|
+
value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
794
|
+
when "date"
|
|
795
|
+
value.is_a?(Types::Date) || value.is_a?(Date) || value.is_a?(Time)
|
|
796
|
+
when "time"
|
|
797
|
+
value.is_a?(Types::Time) || value.is_a?(Time)
|
|
798
|
+
when "duration"
|
|
799
|
+
value.is_a?(Types::Duration)
|
|
800
|
+
when "list"
|
|
801
|
+
value.is_a?(Array) || value.is_a?(Types::List)
|
|
802
|
+
when "context"
|
|
803
|
+
value.is_a?(Hash) || value.is_a?(Types::Context)
|
|
804
|
+
else
|
|
805
|
+
false
|
|
806
|
+
end
|
|
807
|
+
end
|
|
808
|
+
|
|
809
|
+
# Check if value is in range
|
|
810
|
+
def in_range?(value, range)
|
|
811
|
+
start_check = range[:start_inclusive] ? value >= range[:start] : value > range[:start]
|
|
812
|
+
end_check = range[:end_inclusive] ? value <= range[:end] : value < range[:end]
|
|
813
|
+
start_check && end_check
|
|
814
|
+
end
|
|
815
|
+
end
|
|
816
|
+
# rubocop:enable Metrics/ClassLength
|
|
817
|
+
end
|
|
818
|
+
end
|
|
819
|
+
end
|