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.
- checksums.yaml +4 -4
- data/README.md +272 -7
- 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/feel/evaluator.rb +28 -6
- data/lib/decision_agent/dsl/condition_evaluator.rb +982 -839
- data/lib/decision_agent/dsl/schema_validator.rb +51 -13
- data/lib/decision_agent/evaluators/dmn_evaluator.rb +106 -19
- 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/public/app.js +119 -0
- data/lib/decision_agent/web/public/index.html +49 -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 +65 -0
- data/lib/decision_agent/web/server.rb +594 -23
- data/lib/decision_agent.rb +60 -2
- metadata +53 -73
- 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 -655
- 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 -778
- 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/dmn/decision_graph_spec.rb +0 -282
- data/spec/dmn/decision_tree_spec.rb +0 -203
- data/spec/dmn/feel/errors_spec.rb +0 -18
- data/spec/dmn/feel/functions_spec.rb +0 -400
- data/spec/dmn/feel/simple_parser_spec.rb +0 -274
- data/spec/dmn/feel/types_spec.rb +0 -176
- data/spec/dmn/feel_parser_spec.rb +0 -489
- data/spec/dmn/hit_policy_spec.rb +0 -202
- data/spec/dmn/integration_spec.rb +0 -226
- 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 -1909
- data/spec/fixtures/dmn/complex_decision.dmn +0 -81
- data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
- data/spec/fixtures/dmn/simple_decision.dmn +0 -40
- 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 -501
- data/spec/monitoring/monitored_agent_spec.rb +0 -225
- 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 -493
- 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 -490
- 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 -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
|