decision_agent 0.3.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.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +272 -7
  3. data/lib/decision_agent/agent.rb +72 -1
  4. data/lib/decision_agent/context.rb +1 -0
  5. data/lib/decision_agent/data_enrichment/cache/memory_adapter.rb +86 -0
  6. data/lib/decision_agent/data_enrichment/cache_adapter.rb +49 -0
  7. data/lib/decision_agent/data_enrichment/circuit_breaker.rb +135 -0
  8. data/lib/decision_agent/data_enrichment/client.rb +220 -0
  9. data/lib/decision_agent/data_enrichment/config.rb +78 -0
  10. data/lib/decision_agent/data_enrichment/errors.rb +36 -0
  11. data/lib/decision_agent/decision.rb +102 -2
  12. data/lib/decision_agent/dmn/feel/evaluator.rb +28 -6
  13. data/lib/decision_agent/dsl/condition_evaluator.rb +982 -839
  14. data/lib/decision_agent/dsl/schema_validator.rb +51 -13
  15. data/lib/decision_agent/evaluators/dmn_evaluator.rb +106 -19
  16. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +69 -9
  17. data/lib/decision_agent/explainability/condition_trace.rb +83 -0
  18. data/lib/decision_agent/explainability/explainability_result.rb +52 -0
  19. data/lib/decision_agent/explainability/rule_trace.rb +39 -0
  20. data/lib/decision_agent/explainability/trace_collector.rb +24 -0
  21. data/lib/decision_agent/monitoring/alert_manager.rb +5 -1
  22. data/lib/decision_agent/simulation/errors.rb +18 -0
  23. data/lib/decision_agent/simulation/impact_analyzer.rb +498 -0
  24. data/lib/decision_agent/simulation/monte_carlo_simulator.rb +635 -0
  25. data/lib/decision_agent/simulation/replay_engine.rb +486 -0
  26. data/lib/decision_agent/simulation/scenario_engine.rb +318 -0
  27. data/lib/decision_agent/simulation/scenario_library.rb +163 -0
  28. data/lib/decision_agent/simulation/shadow_test_engine.rb +287 -0
  29. data/lib/decision_agent/simulation/what_if_analyzer.rb +1002 -0
  30. data/lib/decision_agent/simulation.rb +17 -0
  31. data/lib/decision_agent/version.rb +1 -1
  32. data/lib/decision_agent/versioning/activerecord_adapter.rb +23 -8
  33. data/lib/decision_agent/web/public/app.js +119 -0
  34. data/lib/decision_agent/web/public/index.html +49 -0
  35. data/lib/decision_agent/web/public/simulation.html +130 -0
  36. data/lib/decision_agent/web/public/simulation_impact.html +478 -0
  37. data/lib/decision_agent/web/public/simulation_replay.html +551 -0
  38. data/lib/decision_agent/web/public/simulation_shadow.html +546 -0
  39. data/lib/decision_agent/web/public/simulation_whatif.html +532 -0
  40. data/lib/decision_agent/web/public/styles.css +65 -0
  41. data/lib/decision_agent/web/server.rb +594 -23
  42. data/lib/decision_agent.rb +60 -2
  43. metadata +53 -73
  44. data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
  45. data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
  46. data/spec/ab_testing/ab_test_spec.rb +0 -270
  47. data/spec/ab_testing/ab_testing_agent_spec.rb +0 -655
  48. data/spec/ab_testing/storage/adapter_spec.rb +0 -64
  49. data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
  50. data/spec/activerecord_thread_safety_spec.rb +0 -553
  51. data/spec/advanced_operators_spec.rb +0 -3150
  52. data/spec/agent_spec.rb +0 -289
  53. data/spec/api_contract_spec.rb +0 -430
  54. data/spec/audit_adapters_spec.rb +0 -92
  55. data/spec/auth/access_audit_logger_spec.rb +0 -394
  56. data/spec/auth/authenticator_spec.rb +0 -112
  57. data/spec/auth/password_reset_spec.rb +0 -294
  58. data/spec/auth/permission_checker_spec.rb +0 -207
  59. data/spec/auth/permission_spec.rb +0 -73
  60. data/spec/auth/rbac_adapter_spec.rb +0 -778
  61. data/spec/auth/rbac_config_spec.rb +0 -82
  62. data/spec/auth/role_spec.rb +0 -51
  63. data/spec/auth/session_manager_spec.rb +0 -172
  64. data/spec/auth/session_spec.rb +0 -112
  65. data/spec/auth/user_spec.rb +0 -130
  66. data/spec/comprehensive_edge_cases_spec.rb +0 -1777
  67. data/spec/context_spec.rb +0 -127
  68. data/spec/decision_agent_spec.rb +0 -96
  69. data/spec/decision_spec.rb +0 -423
  70. data/spec/dmn/decision_graph_spec.rb +0 -282
  71. data/spec/dmn/decision_tree_spec.rb +0 -203
  72. data/spec/dmn/feel/errors_spec.rb +0 -18
  73. data/spec/dmn/feel/functions_spec.rb +0 -400
  74. data/spec/dmn/feel/simple_parser_spec.rb +0 -274
  75. data/spec/dmn/feel/types_spec.rb +0 -176
  76. data/spec/dmn/feel_parser_spec.rb +0 -489
  77. data/spec/dmn/hit_policy_spec.rb +0 -202
  78. data/spec/dmn/integration_spec.rb +0 -226
  79. data/spec/dsl/condition_evaluator_spec.rb +0 -774
  80. data/spec/dsl_validation_spec.rb +0 -648
  81. data/spec/edge_cases_spec.rb +0 -353
  82. data/spec/evaluation_spec.rb +0 -364
  83. data/spec/evaluation_validator_spec.rb +0 -165
  84. data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
  85. data/spec/examples.txt +0 -1909
  86. data/spec/fixtures/dmn/complex_decision.dmn +0 -81
  87. data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
  88. data/spec/fixtures/dmn/simple_decision.dmn +0 -40
  89. data/spec/issue_verification_spec.rb +0 -759
  90. data/spec/json_rule_evaluator_spec.rb +0 -587
  91. data/spec/monitoring/alert_manager_spec.rb +0 -378
  92. data/spec/monitoring/metrics_collector_spec.rb +0 -501
  93. data/spec/monitoring/monitored_agent_spec.rb +0 -225
  94. data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
  95. data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
  96. data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
  97. data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
  98. data/spec/performance_optimizations_spec.rb +0 -493
  99. data/spec/replay_edge_cases_spec.rb +0 -699
  100. data/spec/replay_spec.rb +0 -210
  101. data/spec/rfc8785_canonicalization_spec.rb +0 -215
  102. data/spec/scoring_spec.rb +0 -225
  103. data/spec/spec_helper.rb +0 -60
  104. data/spec/testing/batch_test_importer_spec.rb +0 -693
  105. data/spec/testing/batch_test_runner_spec.rb +0 -307
  106. data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
  107. data/spec/testing/test_result_comparator_spec.rb +0 -392
  108. data/spec/testing/test_scenario_spec.rb +0 -113
  109. data/spec/thread_safety_spec.rb +0 -490
  110. data/spec/thread_safety_spec.rb.broken +0 -878
  111. data/spec/versioning/adapter_spec.rb +0 -156
  112. data/spec/versioning_spec.rb +0 -1030
  113. data/spec/web/middleware/auth_middleware_spec.rb +0 -133
  114. data/spec/web/middleware/permission_middleware_spec.rb +0 -247
  115. data/spec/web_ui_rack_spec.rb +0 -2134
@@ -1,400 +0,0 @@
1
- require "spec_helper"
2
- require "decision_agent/dmn/feel/functions"
3
-
4
- RSpec.describe DecisionAgent::Dmn::Feel::Functions do
5
- describe "String Functions" do
6
- describe "substring" do
7
- it "extracts substring with start and length" do
8
- result = described_class.execute("substring", ["hello world", 1, 5])
9
- expect(result).to eq("hello")
10
- end
11
-
12
- it "extracts substring from start to end" do
13
- result = described_class.execute("substring", ["hello world", 7])
14
- expect(result).to eq("world")
15
- end
16
-
17
- it "handles 1-based indexing" do
18
- result = described_class.execute("substring", ["abc", 2, 1])
19
- expect(result).to eq("b")
20
- end
21
- end
22
-
23
- describe "string length" do
24
- it "returns length of string" do
25
- result = described_class.execute("string length", ["hello"])
26
- expect(result).to eq(5)
27
- end
28
-
29
- it "returns 0 for empty string" do
30
- result = described_class.execute("string length", [""])
31
- expect(result).to eq(0)
32
- end
33
- end
34
-
35
- describe "upper case" do
36
- it "converts to uppercase" do
37
- result = described_class.execute("upper case", ["hello"])
38
- expect(result).to eq("HELLO")
39
- end
40
- end
41
-
42
- describe "lower case" do
43
- it "converts to lowercase" do
44
- result = described_class.execute("lower case", ["HELLO"])
45
- expect(result).to eq("hello")
46
- end
47
- end
48
-
49
- describe "contains" do
50
- it "returns true when substring is found" do
51
- result = described_class.execute("contains", ["hello world", "world"])
52
- expect(result).to be true
53
- end
54
-
55
- it "returns false when substring is not found" do
56
- result = described_class.execute("contains", ["hello world", "xyz"])
57
- expect(result).to be false
58
- end
59
- end
60
-
61
- describe "starts with" do
62
- it "returns true when string starts with prefix" do
63
- result = described_class.execute("starts with", ["hello world", "hello"])
64
- expect(result).to be true
65
- end
66
-
67
- it "returns false when string does not start with prefix" do
68
- result = described_class.execute("starts with", ["hello world", "world"])
69
- expect(result).to be false
70
- end
71
- end
72
-
73
- describe "ends with" do
74
- it "returns true when string ends with suffix" do
75
- result = described_class.execute("ends with", ["hello world", "world"])
76
- expect(result).to be true
77
- end
78
-
79
- it "returns false when string does not end with suffix" do
80
- result = described_class.execute("ends with", ["hello world", "hello"])
81
- expect(result).to be false
82
- end
83
- end
84
-
85
- describe "substring before" do
86
- it "returns substring before match" do
87
- result = described_class.execute("substring before", ["hello world", " "])
88
- expect(result).to eq("hello")
89
- end
90
-
91
- it "returns empty string when match not found" do
92
- result = described_class.execute("substring before", %w[hello x])
93
- expect(result).to eq("")
94
- end
95
- end
96
-
97
- describe "substring after" do
98
- it "returns substring after match" do
99
- result = described_class.execute("substring after", ["hello world", " "])
100
- expect(result).to eq("world")
101
- end
102
-
103
- it "returns empty string when match not found" do
104
- result = described_class.execute("substring after", %w[hello x])
105
- expect(result).to eq("")
106
- end
107
- end
108
-
109
- describe "replace" do
110
- it "replaces all occurrences" do
111
- result = described_class.execute("replace", ["hello world", "l", "L"])
112
- expect(result).to eq("heLLo worLd")
113
- end
114
- end
115
- end
116
-
117
- describe "Numeric Functions" do
118
- describe "abs" do
119
- it "returns absolute value of positive number" do
120
- result = described_class.execute("abs", [5])
121
- expect(result).to eq(5.0)
122
- end
123
-
124
- it "returns absolute value of negative number" do
125
- result = described_class.execute("abs", [-5])
126
- expect(result).to eq(5.0)
127
- end
128
- end
129
-
130
- describe "floor" do
131
- it "rounds down to integer" do
132
- result = described_class.execute("floor", [3.7])
133
- expect(result).to eq(3)
134
- end
135
-
136
- it "handles negative numbers" do
137
- result = described_class.execute("floor", [-3.2])
138
- expect(result).to eq(-4)
139
- end
140
- end
141
-
142
- describe "ceiling" do
143
- it "rounds up to integer" do
144
- result = described_class.execute("ceiling", [3.2])
145
- expect(result).to eq(4)
146
- end
147
-
148
- it "handles negative numbers" do
149
- result = described_class.execute("ceiling", [-3.7])
150
- expect(result).to eq(-3)
151
- end
152
- end
153
-
154
- describe "round" do
155
- it "rounds to nearest integer" do
156
- result = described_class.execute("round", [3.7])
157
- expect(result).to eq(4)
158
- end
159
-
160
- it "rounds to specified precision" do
161
- result = described_class.execute("round", [3.14159, 2])
162
- expect(result).to be_within(0.001).of(3.14)
163
- end
164
- end
165
-
166
- describe "sqrt" do
167
- it "calculates square root" do
168
- result = described_class.execute("sqrt", [16])
169
- expect(result).to eq(4.0)
170
- end
171
- end
172
-
173
- describe "modulo" do
174
- it "calculates remainder" do
175
- result = described_class.execute("modulo", [10, 3])
176
- expect(result).to eq(1.0)
177
- end
178
- end
179
-
180
- describe "odd" do
181
- it "returns true for odd numbers" do
182
- result = described_class.execute("odd", [5])
183
- expect(result).to be true
184
- end
185
-
186
- it "returns false for even numbers" do
187
- result = described_class.execute("odd", [4])
188
- expect(result).to be false
189
- end
190
- end
191
-
192
- describe "even" do
193
- it "returns true for even numbers" do
194
- result = described_class.execute("even", [4])
195
- expect(result).to be true
196
- end
197
-
198
- it "returns false for odd numbers" do
199
- result = described_class.execute("even", [5])
200
- expect(result).to be false
201
- end
202
- end
203
- end
204
-
205
- describe "List Functions" do
206
- describe "count" do
207
- it "returns length of list" do
208
- result = described_class.execute("count", [[1, 2, 3, 4, 5]])
209
- expect(result).to eq(5)
210
- end
211
-
212
- it "returns 0 for empty list" do
213
- result = described_class.execute("count", [[]])
214
- expect(result).to eq(0)
215
- end
216
-
217
- it "returns 0 for non-array" do
218
- result = described_class.execute("count", [42])
219
- expect(result).to eq(0)
220
- end
221
- end
222
-
223
- describe "sum" do
224
- it "calculates sum of list" do
225
- result = described_class.execute("sum", [[1, 2, 3, 4, 5]])
226
- expect(result).to eq(15.0)
227
- end
228
-
229
- it "returns 0 for empty list" do
230
- result = described_class.execute("sum", [[]])
231
- expect(result).to eq(0)
232
- end
233
- end
234
-
235
- describe "mean" do
236
- it "calculates average of list" do
237
- result = described_class.execute("mean", [[1, 2, 3, 4, 5]])
238
- expect(result).to eq(3.0)
239
- end
240
-
241
- it "returns 0 for empty list" do
242
- result = described_class.execute("mean", [[]])
243
- expect(result).to eq(0)
244
- end
245
- end
246
-
247
- describe "min" do
248
- it "returns minimum value from list" do
249
- result = described_class.execute("min", [[5, 2, 8, 1, 9]])
250
- expect(result).to eq(1.0)
251
- end
252
-
253
- it "returns minimum from multiple arguments" do
254
- result = described_class.execute("min", [5, 2, 8, 1, 9])
255
- expect(result).to eq(1.0)
256
- end
257
-
258
- it "returns nil for empty list" do
259
- result = described_class.execute("min", [[]])
260
- expect(result).to be_nil
261
- end
262
- end
263
-
264
- describe "max" do
265
- it "returns maximum value from list" do
266
- result = described_class.execute("max", [[5, 2, 8, 1, 9]])
267
- expect(result).to eq(9.0)
268
- end
269
-
270
- it "returns maximum from multiple arguments" do
271
- result = described_class.execute("max", [5, 2, 8, 1, 9])
272
- expect(result).to eq(9.0)
273
- end
274
- end
275
-
276
- describe "append" do
277
- it "appends items to list" do
278
- result = described_class.execute("append", [[1, 2], 3, 4])
279
- expect(result).to eq([1, 2, 3, 4])
280
- end
281
- end
282
-
283
- describe "reverse" do
284
- it "reverses list" do
285
- result = described_class.execute("reverse", [[1, 2, 3, 4, 5]])
286
- expect(result).to eq([5, 4, 3, 2, 1])
287
- end
288
- end
289
-
290
- describe "index of" do
291
- it "returns 1-based index of element" do
292
- result = described_class.execute("index of", [[10, 20, 30], 20])
293
- expect(result).to eq(2)
294
- end
295
-
296
- it "returns -1 when element not found" do
297
- result = described_class.execute("index of", [[10, 20, 30], 40])
298
- expect(result).to eq(-1)
299
- end
300
- end
301
-
302
- describe "distinct values" do
303
- it "removes duplicates" do
304
- result = described_class.execute("distinct values", [[1, 2, 2, 3, 3, 3]])
305
- expect(result).to eq([1, 2, 3])
306
- end
307
- end
308
- end
309
-
310
- describe "Boolean Functions" do
311
- describe "not" do
312
- it "negates true" do
313
- result = described_class.execute("not", [true])
314
- expect(result).to be false
315
- end
316
-
317
- it "negates false" do
318
- result = described_class.execute("not", [false])
319
- expect(result).to be true
320
- end
321
- end
322
-
323
- describe "all" do
324
- it "returns true when all items are true" do
325
- result = described_class.execute("all", [[true, true, true]])
326
- expect(result).to be true
327
- end
328
-
329
- it "returns false when any item is false" do
330
- result = described_class.execute("all", [[true, false, true]])
331
- expect(result).to be false
332
- end
333
- end
334
-
335
- describe "any" do
336
- it "returns true when any item is true" do
337
- result = described_class.execute("any", [[false, true, false]])
338
- expect(result).to be true
339
- end
340
-
341
- it "returns false when all items are false" do
342
- result = described_class.execute("any", [[false, false, false]])
343
- expect(result).to be false
344
- end
345
- end
346
- end
347
-
348
- describe "Date/Time Functions" do
349
- describe "date" do
350
- it "parses ISO 8601 date string" do
351
- result = described_class.execute("date", ["2024-01-15T10:30:00Z"])
352
- expect(result).to be_a(DecisionAgent::Dmn::Feel::Types::Date)
353
- end
354
- end
355
-
356
- describe "time" do
357
- it "parses ISO 8601 time string" do
358
- result = described_class.execute("time", ["2024-01-15T10:30:00Z"])
359
- expect(result).to be_a(DecisionAgent::Dmn::Feel::Types::Time)
360
- end
361
- end
362
-
363
- describe "duration" do
364
- it "parses ISO 8601 duration" do
365
- result = described_class.execute("duration", ["P1Y2M3DT4H5M6S"])
366
- expect(result).to be_a(DecisionAgent::Dmn::Feel::Types::Duration)
367
- expect(result.years).to eq(1)
368
- expect(result.months).to eq(2)
369
- end
370
- end
371
- end
372
-
373
- describe "Function Registry" do
374
- it "lists all registered functions" do
375
- functions = described_class.list
376
- expect(functions).to include("substring")
377
- expect(functions).to include("sum")
378
- expect(functions).to include("abs")
379
- end
380
-
381
- it "gets function by name" do
382
- func = described_class.get("substring")
383
- expect(func).not_to be_nil
384
- end
385
-
386
- it "raises error for unknown function" do
387
- expect do
388
- described_class.execute("unknown_func", [])
389
- end.to raise_error(DecisionAgent::Dmn::FeelFunctionError, /Unknown function/)
390
- end
391
- end
392
-
393
- describe "Argument Validation" do
394
- it "raises error for wrong argument count" do
395
- expect do
396
- described_class.execute("substring", ["hello"])
397
- end.to raise_error(DecisionAgent::Dmn::FeelFunctionError, /Wrong number of arguments/)
398
- end
399
- end
400
- end
@@ -1,274 +0,0 @@
1
- require "spec_helper"
2
- require "decision_agent/dmn/feel/simple_parser"
3
-
4
- RSpec.describe DecisionAgent::Dmn::Feel::SimpleParser do
5
- let(:parser) { described_class.new }
6
-
7
- describe ".can_parse?" do
8
- it "returns true for simple arithmetic" do
9
- expect(described_class.can_parse?("age + 5")).to be true
10
- expect(described_class.can_parse?("price * 1.1")).to be true
11
- end
12
-
13
- it "returns true for logical expressions" do
14
- expect(described_class.can_parse?("age >= 18 and status = active")).to be true
15
- end
16
-
17
- it "returns false for lists" do
18
- expect(described_class.can_parse?("[1, 2, 3]")).to be false
19
- end
20
-
21
- it "returns false for contexts" do
22
- expect(described_class.can_parse?("{x: 10}")).to be false
23
- end
24
-
25
- it "returns false for functions" do
26
- expect(described_class.can_parse?("sum(scores)")).to be false
27
- end
28
-
29
- it "returns false for quantified expressions" do
30
- expect(described_class.can_parse?("some x in list satisfies x > 5")).to be false
31
- end
32
- end
33
-
34
- describe "#parse" do
35
- describe "literals" do
36
- it "parses integer" do
37
- result = parser.parse("42")
38
- expect(result[:type]).to eq(:literal)
39
- expect(result[:value]).to eq(42)
40
- end
41
-
42
- it "parses float" do
43
- result = parser.parse("3.14")
44
- expect(result[:type]).to eq(:literal)
45
- expect(result[:value]).to eq(3.14)
46
- end
47
-
48
- it "parses negative number" do
49
- result = parser.parse("-10")
50
- expect(result[:type]).to eq(:literal)
51
- expect(result[:value]).to eq(-10)
52
- end
53
-
54
- it "parses string" do
55
- result = parser.parse('"hello"')
56
- expect(result[:type]).to eq(:literal)
57
- expect(result[:value]).to eq("hello")
58
- end
59
-
60
- it "parses boolean true" do
61
- result = parser.parse("true")
62
- expect(result[:type]).to eq(:boolean)
63
- expect(result[:value]).to be true
64
- end
65
-
66
- it "parses boolean false" do
67
- result = parser.parse("false")
68
- expect(result[:type]).to eq(:boolean)
69
- expect(result[:value]).to be false
70
- end
71
- end
72
-
73
- describe "field references" do
74
- it "parses field name" do
75
- result = parser.parse("age")
76
- expect(result[:type]).to eq(:field)
77
- expect(result[:name]).to eq("age")
78
- end
79
- end
80
-
81
- describe "arithmetic operators" do
82
- it "parses addition" do
83
- result = parser.parse("5 + 3")
84
- expect(result[:type]).to eq(:arithmetic)
85
- expect(result[:operator]).to eq("+")
86
- expect(result[:left][:value]).to eq(5)
87
- expect(result[:right][:value]).to eq(3)
88
- end
89
-
90
- it "parses subtraction" do
91
- result = parser.parse("10 - 3")
92
- expect(result[:type]).to eq(:arithmetic)
93
- expect(result[:operator]).to eq("-")
94
- end
95
-
96
- it "parses multiplication" do
97
- result = parser.parse("4 * 5")
98
- expect(result[:type]).to eq(:arithmetic)
99
- expect(result[:operator]).to eq("*")
100
- end
101
-
102
- it "parses division" do
103
- result = parser.parse("20 / 4")
104
- expect(result[:type]).to eq(:arithmetic)
105
- expect(result[:operator]).to eq("/")
106
- end
107
-
108
- it "parses exponentiation" do
109
- result = parser.parse("2 ** 3")
110
- expect(result[:type]).to eq(:arithmetic)
111
- expect(result[:operator]).to eq("**")
112
- end
113
-
114
- it "parses modulo" do
115
- result = parser.parse("10 % 3")
116
- expect(result[:type]).to eq(:arithmetic)
117
- expect(result[:operator]).to eq("%")
118
- end
119
- end
120
-
121
- describe "operator precedence" do
122
- it "respects multiplication before addition" do
123
- result = parser.parse("2 + 3 * 4")
124
- # Should be: 2 + (3 * 4)
125
- expect(result[:operator]).to eq("+")
126
- expect(result[:left][:value]).to eq(2)
127
- expect(result[:right][:operator]).to eq("*")
128
- end
129
-
130
- it "respects exponentiation before multiplication" do
131
- result = parser.parse("2 * 3 ** 2")
132
- # Should be: 2 * (3 ** 2)
133
- expect(result[:operator]).to eq("*")
134
- expect(result[:right][:operator]).to eq("**")
135
- end
136
-
137
- it "handles parentheses" do
138
- result = parser.parse("(2 + 3) * 4")
139
- # Should be: (2 + 3) * 4
140
- expect(result[:operator]).to eq("*")
141
- expect(result[:left][:operator]).to eq("+")
142
- end
143
- end
144
-
145
- describe "comparison operators" do
146
- it "parses greater than or equal" do
147
- result = parser.parse("age >= 18")
148
- expect(result[:type]).to eq(:comparison)
149
- expect(result[:operator]).to eq(">=")
150
- expect(result[:left][:name]).to eq("age")
151
- expect(result[:right][:value]).to eq(18)
152
- end
153
-
154
- it "parses less than or equal" do
155
- result = parser.parse("score <= 100")
156
- expect(result[:type]).to eq(:comparison)
157
- expect(result[:operator]).to eq("<=")
158
- end
159
-
160
- it "parses greater than" do
161
- result = parser.parse("price > 0")
162
- expect(result[:type]).to eq(:comparison)
163
- expect(result[:operator]).to eq(">")
164
- end
165
-
166
- it "parses less than" do
167
- result = parser.parse("age < 65")
168
- expect(result[:type]).to eq(:comparison)
169
- expect(result[:operator]).to eq("<")
170
- end
171
-
172
- it "parses not equal" do
173
- result = parser.parse("status != pending")
174
- expect(result[:type]).to eq(:comparison)
175
- expect(result[:operator]).to eq("!=")
176
- end
177
-
178
- it "parses equal" do
179
- result = parser.parse("status = active")
180
- expect(result[:type]).to eq(:comparison)
181
- expect(result[:operator]).to eq("=")
182
- end
183
- end
184
-
185
- describe "logical operators" do
186
- it "parses AND" do
187
- result = parser.parse("age >= 18 and score > 700")
188
- expect(result[:type]).to eq(:logical)
189
- expect(result[:operator]).to eq("and")
190
- expect(result[:left][:type]).to eq(:comparison)
191
- expect(result[:right][:type]).to eq(:comparison)
192
- end
193
-
194
- it "parses OR" do
195
- result = parser.parse("status = active or status = pending")
196
- expect(result[:type]).to eq(:logical)
197
- expect(result[:operator]).to eq("or")
198
- end
199
-
200
- it "parses NOT" do
201
- result = parser.parse("not active")
202
- expect(result[:type]).to eq(:logical)
203
- expect(result[:operator]).to eq("not")
204
- expect(result[:operand][:name]).to eq("active")
205
- end
206
-
207
- it "respects AND precedence over OR" do
208
- result = parser.parse("a or b and c")
209
- # Should be: a or (b and c)
210
- expect(result[:operator]).to eq("or")
211
- expect(result[:right][:operator]).to eq("and")
212
- end
213
- end
214
-
215
- describe "unary operators" do
216
- it "parses unary minus" do
217
- result = parser.parse("-5")
218
- expect(result[:type]).to eq(:literal)
219
- expect(result[:value]).to eq(-5)
220
- end
221
-
222
- it "parses unary minus with field" do
223
- result = parser.parse("-age")
224
- expect(result[:type]).to eq(:arithmetic)
225
- expect(result[:operator]).to eq("negate")
226
- expect(result[:operand][:name]).to eq("age")
227
- end
228
-
229
- it "parses unary plus (ignored)" do
230
- result = parser.parse("+5")
231
- expect(result[:value]).to eq(5)
232
- end
233
- end
234
-
235
- describe "complex expressions" do
236
- it "parses arithmetic with comparison" do
237
- result = parser.parse("age + 5 >= 18")
238
- expect(result[:type]).to eq(:comparison)
239
- expect(result[:left][:type]).to eq(:arithmetic)
240
- end
241
-
242
- it "parses multiple logical operations" do
243
- result = parser.parse("age >= 18 and age <= 65 and status = active")
244
- expect(result[:type]).to eq(:logical)
245
- expect(result[:operator]).to eq("and")
246
- end
247
-
248
- it "parses nested parentheses" do
249
- result = parser.parse("((age + 5) * 2) >= 40")
250
- expect(result[:type]).to eq(:comparison)
251
- end
252
- end
253
-
254
- describe "error handling" do
255
- it "raises error for empty expression" do
256
- expect do
257
- parser.parse("")
258
- end.to raise_error(DecisionAgent::Dmn::FeelParseError, /Empty expression/)
259
- end
260
-
261
- it "raises error for unbalanced parentheses" do
262
- expect do
263
- parser.parse("(age + 5")
264
- end.to raise_error(DecisionAgent::Dmn::FeelParseError)
265
- end
266
-
267
- it "raises error for unexpected character" do
268
- expect do
269
- parser.parse("age @ 5")
270
- end.to raise_error(DecisionAgent::Dmn::FeelParseError, /Unexpected character/)
271
- end
272
- end
273
- end
274
- end