decision_agent 0.3.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +234 -14
  3. data/lib/decision_agent/ab_testing/ab_test.rb +5 -1
  4. data/lib/decision_agent/ab_testing/ab_test_assignment.rb +2 -0
  5. data/lib/decision_agent/ab_testing/ab_test_manager.rb +2 -0
  6. data/lib/decision_agent/ab_testing/ab_testing_agent.rb +2 -0
  7. data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +2 -13
  8. data/lib/decision_agent/ab_testing/storage/adapter.rb +2 -0
  9. data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +2 -0
  10. data/lib/decision_agent/agent.rb +78 -9
  11. data/lib/decision_agent/audit/adapter.rb +2 -0
  12. data/lib/decision_agent/audit/logger_adapter.rb +2 -0
  13. data/lib/decision_agent/audit/null_adapter.rb +2 -0
  14. data/lib/decision_agent/auth/access_audit_logger.rb +2 -0
  15. data/lib/decision_agent/auth/authenticator.rb +2 -0
  16. data/lib/decision_agent/auth/password_reset_manager.rb +2 -0
  17. data/lib/decision_agent/auth/password_reset_token.rb +2 -0
  18. data/lib/decision_agent/auth/permission.rb +2 -0
  19. data/lib/decision_agent/auth/permission_checker.rb +2 -0
  20. data/lib/decision_agent/auth/rbac_adapter.rb +2 -0
  21. data/lib/decision_agent/auth/rbac_config.rb +2 -0
  22. data/lib/decision_agent/auth/role.rb +2 -0
  23. data/lib/decision_agent/auth/session.rb +2 -0
  24. data/lib/decision_agent/auth/session_manager.rb +2 -0
  25. data/lib/decision_agent/auth/user.rb +2 -0
  26. data/lib/decision_agent/context.rb +14 -0
  27. data/lib/decision_agent/decision.rb +113 -4
  28. data/lib/decision_agent/dmn/adapter.rb +2 -0
  29. data/lib/decision_agent/dmn/cache.rb +2 -2
  30. data/lib/decision_agent/dmn/decision_graph.rb +7 -7
  31. data/lib/decision_agent/dmn/decision_tree.rb +16 -8
  32. data/lib/decision_agent/dmn/errors.rb +2 -0
  33. data/lib/decision_agent/dmn/exporter.rb +2 -0
  34. data/lib/decision_agent/dmn/feel/evaluator.rb +130 -114
  35. data/lib/decision_agent/dmn/feel/functions.rb +2 -0
  36. data/lib/decision_agent/dmn/feel/parser.rb +2 -0
  37. data/lib/decision_agent/dmn/feel/simple_parser.rb +98 -77
  38. data/lib/decision_agent/dmn/feel/transformer.rb +56 -102
  39. data/lib/decision_agent/dmn/feel/types.rb +2 -0
  40. data/lib/decision_agent/dmn/importer.rb +2 -0
  41. data/lib/decision_agent/dmn/model.rb +2 -4
  42. data/lib/decision_agent/dmn/parser.rb +2 -0
  43. data/lib/decision_agent/dmn/testing.rb +3 -2
  44. data/lib/decision_agent/dmn/validator.rb +5 -3
  45. data/lib/decision_agent/dmn/visualizer.rb +7 -6
  46. data/lib/decision_agent/dsl/condition_evaluator.rb +242 -1375
  47. data/lib/decision_agent/dsl/helpers/cache_helpers.rb +82 -0
  48. data/lib/decision_agent/dsl/helpers/comparison_helpers.rb +98 -0
  49. data/lib/decision_agent/dsl/helpers/date_helpers.rb +91 -0
  50. data/lib/decision_agent/dsl/helpers/geospatial_helpers.rb +85 -0
  51. data/lib/decision_agent/dsl/helpers/operator_evaluation_helpers.rb +160 -0
  52. data/lib/decision_agent/dsl/helpers/parameter_parsing_helpers.rb +206 -0
  53. data/lib/decision_agent/dsl/helpers/template_helpers.rb +39 -0
  54. data/lib/decision_agent/dsl/helpers/utility_helpers.rb +45 -0
  55. data/lib/decision_agent/dsl/operators/base.rb +70 -0
  56. data/lib/decision_agent/dsl/operators/basic_comparison_operators.rb +80 -0
  57. data/lib/decision_agent/dsl/operators/collection_operators.rb +60 -0
  58. data/lib/decision_agent/dsl/operators/date_arithmetic_operators.rb +206 -0
  59. data/lib/decision_agent/dsl/operators/date_time_operators.rb +47 -0
  60. data/lib/decision_agent/dsl/operators/duration_operators.rb +149 -0
  61. data/lib/decision_agent/dsl/operators/financial_operators.rb +237 -0
  62. data/lib/decision_agent/dsl/operators/geospatial_operators.rb +106 -0
  63. data/lib/decision_agent/dsl/operators/mathematical_operators.rb +234 -0
  64. data/lib/decision_agent/dsl/operators/moving_window_operators.rb +135 -0
  65. data/lib/decision_agent/dsl/operators/numeric_operators.rb +120 -0
  66. data/lib/decision_agent/dsl/operators/rate_operators.rb +65 -0
  67. data/lib/decision_agent/dsl/operators/statistical_aggregations.rb +187 -0
  68. data/lib/decision_agent/dsl/operators/string_aggregations.rb +84 -0
  69. data/lib/decision_agent/dsl/operators/string_operators.rb +72 -0
  70. data/lib/decision_agent/dsl/operators/time_component_operators.rb +72 -0
  71. data/lib/decision_agent/dsl/rule_parser.rb +2 -0
  72. data/lib/decision_agent/dsl/schema_validator.rb +37 -14
  73. data/lib/decision_agent/errors.rb +2 -0
  74. data/lib/decision_agent/evaluation.rb +14 -2
  75. data/lib/decision_agent/evaluators/base.rb +2 -0
  76. data/lib/decision_agent/evaluators/dmn_evaluator.rb +108 -19
  77. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +56 -11
  78. data/lib/decision_agent/evaluators/static_evaluator.rb +2 -0
  79. data/lib/decision_agent/explainability/condition_trace.rb +85 -0
  80. data/lib/decision_agent/explainability/explainability_result.rb +50 -0
  81. data/lib/decision_agent/explainability/rule_trace.rb +41 -0
  82. data/lib/decision_agent/explainability/trace_collector.rb +26 -0
  83. data/lib/decision_agent/monitoring/alert_manager.rb +7 -16
  84. data/lib/decision_agent/monitoring/dashboard_server.rb +383 -250
  85. data/lib/decision_agent/monitoring/metrics_collector.rb +2 -0
  86. data/lib/decision_agent/monitoring/monitored_agent.rb +2 -0
  87. data/lib/decision_agent/monitoring/prometheus_exporter.rb +3 -1
  88. data/lib/decision_agent/replay/replay.rb +4 -1
  89. data/lib/decision_agent/scoring/base.rb +2 -0
  90. data/lib/decision_agent/scoring/consensus.rb +2 -0
  91. data/lib/decision_agent/scoring/max_weight.rb +2 -0
  92. data/lib/decision_agent/scoring/threshold.rb +2 -0
  93. data/lib/decision_agent/scoring/weighted_average.rb +2 -0
  94. data/lib/decision_agent/simulation/errors.rb +20 -0
  95. data/lib/decision_agent/simulation/impact_analyzer.rb +500 -0
  96. data/lib/decision_agent/simulation/monte_carlo_simulator.rb +638 -0
  97. data/lib/decision_agent/simulation/replay_engine.rb +488 -0
  98. data/lib/decision_agent/simulation/scenario_engine.rb +320 -0
  99. data/lib/decision_agent/simulation/scenario_library.rb +165 -0
  100. data/lib/decision_agent/simulation/shadow_test_engine.rb +274 -0
  101. data/lib/decision_agent/simulation/what_if_analyzer.rb +1008 -0
  102. data/lib/decision_agent/simulation.rb +19 -0
  103. data/lib/decision_agent/testing/batch_test_importer.rb +6 -2
  104. data/lib/decision_agent/testing/batch_test_runner.rb +5 -2
  105. data/lib/decision_agent/testing/test_coverage_analyzer.rb +2 -0
  106. data/lib/decision_agent/testing/test_result_comparator.rb +2 -0
  107. data/lib/decision_agent/testing/test_scenario.rb +2 -0
  108. data/lib/decision_agent/version.rb +3 -1
  109. data/lib/decision_agent/versioning/activerecord_adapter.rb +108 -43
  110. data/lib/decision_agent/versioning/adapter.rb +9 -0
  111. data/lib/decision_agent/versioning/file_storage_adapter.rb +19 -6
  112. data/lib/decision_agent/versioning/version_manager.rb +9 -0
  113. data/lib/decision_agent/web/dmn_editor/serialization.rb +74 -0
  114. data/lib/decision_agent/web/dmn_editor/xml_builder.rb +107 -0
  115. data/lib/decision_agent/web/dmn_editor.rb +8 -67
  116. data/lib/decision_agent/web/middleware/auth_middleware.rb +2 -0
  117. data/lib/decision_agent/web/middleware/permission_middleware.rb +3 -1
  118. data/lib/decision_agent/web/public/app.js +186 -26
  119. data/lib/decision_agent/web/public/batch_testing.html +80 -6
  120. data/lib/decision_agent/web/public/dmn-editor.html +2 -2
  121. data/lib/decision_agent/web/public/dmn-editor.js +74 -8
  122. data/lib/decision_agent/web/public/index.html +69 -3
  123. data/lib/decision_agent/web/public/login.html +1 -1
  124. data/lib/decision_agent/web/public/sample_batch.csv +11 -0
  125. data/lib/decision_agent/web/public/sample_impact.csv +11 -0
  126. data/lib/decision_agent/web/public/sample_replay.csv +11 -0
  127. data/lib/decision_agent/web/public/sample_rules.json +118 -0
  128. data/lib/decision_agent/web/public/sample_shadow.csv +11 -0
  129. data/lib/decision_agent/web/public/sample_whatif.csv +11 -0
  130. data/lib/decision_agent/web/public/simulation.html +146 -0
  131. data/lib/decision_agent/web/public/simulation_impact.html +495 -0
  132. data/lib/decision_agent/web/public/simulation_replay.html +547 -0
  133. data/lib/decision_agent/web/public/simulation_shadow.html +561 -0
  134. data/lib/decision_agent/web/public/simulation_whatif.html +549 -0
  135. data/lib/decision_agent/web/public/styles.css +65 -0
  136. data/lib/decision_agent/web/public/users.html +1 -1
  137. data/lib/decision_agent/web/rack_helpers.rb +106 -0
  138. data/lib/decision_agent/web/rack_request_helpers.rb +196 -0
  139. data/lib/decision_agent/web/server.rb +2126 -1374
  140. data/lib/decision_agent.rb +19 -1
  141. data/lib/generators/decision_agent/install/install_generator.rb +2 -0
  142. data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +2 -0
  143. data/lib/generators/decision_agent/install/templates/ab_test_model.rb +2 -0
  144. data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +2 -0
  145. data/lib/generators/decision_agent/install/templates/migration.rb +2 -0
  146. data/lib/generators/decision_agent/install/templates/rule.rb +2 -0
  147. data/lib/generators/decision_agent/install/templates/rule_version.rb +2 -0
  148. metadata +103 -89
  149. data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
  150. data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
  151. data/spec/ab_testing/ab_test_spec.rb +0 -270
  152. data/spec/ab_testing/ab_testing_agent_spec.rb +0 -655
  153. data/spec/ab_testing/storage/adapter_spec.rb +0 -64
  154. data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
  155. data/spec/activerecord_thread_safety_spec.rb +0 -553
  156. data/spec/advanced_operators_spec.rb +0 -3150
  157. data/spec/agent_spec.rb +0 -289
  158. data/spec/api_contract_spec.rb +0 -430
  159. data/spec/audit_adapters_spec.rb +0 -92
  160. data/spec/auth/access_audit_logger_spec.rb +0 -394
  161. data/spec/auth/authenticator_spec.rb +0 -112
  162. data/spec/auth/password_reset_spec.rb +0 -294
  163. data/spec/auth/permission_checker_spec.rb +0 -207
  164. data/spec/auth/permission_spec.rb +0 -73
  165. data/spec/auth/rbac_adapter_spec.rb +0 -778
  166. data/spec/auth/rbac_config_spec.rb +0 -82
  167. data/spec/auth/role_spec.rb +0 -51
  168. data/spec/auth/session_manager_spec.rb +0 -172
  169. data/spec/auth/session_spec.rb +0 -112
  170. data/spec/auth/user_spec.rb +0 -130
  171. data/spec/comprehensive_edge_cases_spec.rb +0 -1777
  172. data/spec/context_spec.rb +0 -127
  173. data/spec/decision_agent_spec.rb +0 -96
  174. data/spec/decision_spec.rb +0 -423
  175. data/spec/dmn/decision_graph_spec.rb +0 -282
  176. data/spec/dmn/decision_tree_spec.rb +0 -203
  177. data/spec/dmn/feel/errors_spec.rb +0 -18
  178. data/spec/dmn/feel/functions_spec.rb +0 -400
  179. data/spec/dmn/feel/simple_parser_spec.rb +0 -274
  180. data/spec/dmn/feel/types_spec.rb +0 -176
  181. data/spec/dmn/feel_parser_spec.rb +0 -489
  182. data/spec/dmn/hit_policy_spec.rb +0 -202
  183. data/spec/dmn/integration_spec.rb +0 -226
  184. data/spec/dsl/condition_evaluator_spec.rb +0 -774
  185. data/spec/dsl_validation_spec.rb +0 -648
  186. data/spec/edge_cases_spec.rb +0 -353
  187. data/spec/evaluation_spec.rb +0 -364
  188. data/spec/evaluation_validator_spec.rb +0 -165
  189. data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
  190. data/spec/examples.txt +0 -1909
  191. data/spec/fixtures/dmn/complex_decision.dmn +0 -81
  192. data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
  193. data/spec/fixtures/dmn/simple_decision.dmn +0 -40
  194. data/spec/issue_verification_spec.rb +0 -759
  195. data/spec/json_rule_evaluator_spec.rb +0 -587
  196. data/spec/monitoring/alert_manager_spec.rb +0 -378
  197. data/spec/monitoring/metrics_collector_spec.rb +0 -501
  198. data/spec/monitoring/monitored_agent_spec.rb +0 -225
  199. data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
  200. data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
  201. data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
  202. data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
  203. data/spec/performance_optimizations_spec.rb +0 -493
  204. data/spec/replay_edge_cases_spec.rb +0 -699
  205. data/spec/replay_spec.rb +0 -210
  206. data/spec/rfc8785_canonicalization_spec.rb +0 -215
  207. data/spec/scoring_spec.rb +0 -225
  208. data/spec/spec_helper.rb +0 -60
  209. data/spec/testing/batch_test_importer_spec.rb +0 -693
  210. data/spec/testing/batch_test_runner_spec.rb +0 -307
  211. data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
  212. data/spec/testing/test_result_comparator_spec.rb +0 -392
  213. data/spec/testing/test_scenario_spec.rb +0 -113
  214. data/spec/thread_safety_spec.rb +0 -490
  215. data/spec/thread_safety_spec.rb.broken +0 -878
  216. data/spec/versioning/adapter_spec.rb +0 -156
  217. data/spec/versioning_spec.rb +0 -1030
  218. data/spec/web/middleware/auth_middleware_spec.rb +0 -133
  219. data/spec/web/middleware/permission_middleware_spec.rb +0 -247
  220. data/spec/web_ui_rack_spec.rb +0 -2134
@@ -1,176 +0,0 @@
1
- require "spec_helper"
2
- require "decision_agent/dmn/feel/types"
3
-
4
- RSpec.describe DecisionAgent::Dmn::Feel::Types do
5
- describe DecisionAgent::Dmn::Feel::Types::Number do
6
- it "creates from integer" do
7
- num = DecisionAgent::Dmn::Feel::Types::Number.new(42)
8
- expect(num.to_ruby).to eq(42)
9
- expect(num.to_i).to eq(42)
10
- end
11
-
12
- it "creates from float" do
13
- num = DecisionAgent::Dmn::Feel::Types::Number.new(3.14)
14
- expect(num.to_ruby).to eq(3.14)
15
- expect(num.to_f).to be_within(0.001).of(3.14)
16
- end
17
-
18
- it "creates from string" do
19
- num = DecisionAgent::Dmn::Feel::Types::Number.new("42.5")
20
- expect(num.to_f).to be_within(0.001).of(42.5)
21
- end
22
-
23
- it "supports scale tracking" do
24
- num = DecisionAgent::Dmn::Feel::Types::Number.new(42, scale: 2)
25
- expect(num.scale).to eq(2)
26
- end
27
-
28
- it "raises error for invalid type" do
29
- expect do
30
- DecisionAgent::Dmn::Feel::Types::Number.new([])
31
- end.to raise_error(DecisionAgent::Dmn::FeelTypeError)
32
- end
33
- end
34
-
35
- describe DecisionAgent::Dmn::Feel::Types::Date do
36
- it "creates from Time object" do
37
- time = Time.new(2024, 1, 15)
38
- date = DecisionAgent::Dmn::Feel::Types::Date.new(time)
39
- expect(date.to_ruby).to eq(time)
40
- end
41
-
42
- it "creates from ISO 8601 string" do
43
- date = DecisionAgent::Dmn::Feel::Types::Date.new("2024-01-15T10:30:00Z")
44
- expect(date.to_ruby).to be_a(Time)
45
- end
46
-
47
- it "creates from date string" do
48
- date = DecisionAgent::Dmn::Feel::Types::Date.new("2024-01-15")
49
- expect(date.to_ruby).to be_a(Time)
50
- end
51
-
52
- it "raises error for invalid format" do
53
- expect do
54
- DecisionAgent::Dmn::Feel::Types::Date.new("invalid")
55
- end.to raise_error(DecisionAgent::Dmn::FeelTypeError)
56
- end
57
- end
58
-
59
- describe DecisionAgent::Dmn::Feel::Types::Time do
60
- it "creates from Time object" do
61
- time = Time.new(2024, 1, 15, 10, 30, 0)
62
- feel_time = DecisionAgent::Dmn::Feel::Types::Time.new(time)
63
- expect(feel_time.to_ruby).to eq(time)
64
- end
65
-
66
- it "creates from ISO 8601 string" do
67
- feel_time = DecisionAgent::Dmn::Feel::Types::Time.new("2024-01-15T10:30:00Z")
68
- expect(feel_time.to_ruby).to be_a(Time)
69
- end
70
- end
71
-
72
- describe DecisionAgent::Dmn::Feel::Types::Duration do
73
- it "parses ISO 8601 duration with years" do
74
- duration = DecisionAgent::Dmn::Feel::Types::Duration.parse("P1Y")
75
- expect(duration.years).to eq(1)
76
- expect(duration.months).to eq(0)
77
- end
78
-
79
- it "parses ISO 8601 duration with months" do
80
- duration = DecisionAgent::Dmn::Feel::Types::Duration.parse("P3M")
81
- expect(duration.months).to eq(3)
82
- end
83
-
84
- it "parses ISO 8601 duration with days" do
85
- duration = DecisionAgent::Dmn::Feel::Types::Duration.parse("P10D")
86
- expect(duration.days).to eq(10)
87
- end
88
-
89
- it "parses ISO 8601 duration with time components" do
90
- duration = DecisionAgent::Dmn::Feel::Types::Duration.parse("PT5H30M15S")
91
- expect(duration.hours).to eq(5)
92
- expect(duration.minutes).to eq(30)
93
- expect(duration.seconds).to eq(15)
94
- end
95
-
96
- it "parses complete ISO 8601 duration" do
97
- duration = DecisionAgent::Dmn::Feel::Types::Duration.parse("P1Y2M3DT4H5M6S")
98
- expect(duration.years).to eq(1)
99
- expect(duration.months).to eq(2)
100
- expect(duration.days).to eq(3)
101
- expect(duration.hours).to eq(4)
102
- expect(duration.minutes).to eq(5)
103
- expect(duration.seconds).to eq(6)
104
- end
105
-
106
- it "converts to seconds" do
107
- duration = DecisionAgent::Dmn::Feel::Types::Duration.parse("PT1H30M")
108
- expect(duration.to_seconds).to eq(5400) # 90 minutes
109
- end
110
-
111
- it "raises error for invalid format" do
112
- expect do
113
- DecisionAgent::Dmn::Feel::Types::Duration.parse("invalid")
114
- end.to raise_error(DecisionAgent::Dmn::FeelTypeError)
115
- end
116
-
117
- it "raises error for non-P prefix" do
118
- expect do
119
- DecisionAgent::Dmn::Feel::Types::Duration.parse("1Y2M")
120
- end.to raise_error(DecisionAgent::Dmn::FeelTypeError, /must start with 'P'/)
121
- end
122
- end
123
-
124
- describe DecisionAgent::Dmn::Feel::Types::List do
125
- it "wraps array" do
126
- list = DecisionAgent::Dmn::Feel::Types::List.new([1, 2, 3])
127
- expect(list.to_ruby).to eq([1, 2, 3])
128
- expect(list[0]).to eq(1)
129
- expect(list.length).to eq(3)
130
- end
131
- end
132
-
133
- describe DecisionAgent::Dmn::Feel::Types::Context do
134
- it "wraps hash with symbol keys" do
135
- ctx = DecisionAgent::Dmn::Feel::Types::Context.new({ "name" => "John", "age" => 30 })
136
- expect(ctx[:name]).to eq("John")
137
- expect(ctx[:age]).to eq(30)
138
- end
139
-
140
- it "converts string keys to symbols" do
141
- ctx = DecisionAgent::Dmn::Feel::Types::Context.new({ "x" => 10, "y" => 20 })
142
- expect(ctx.to_ruby).to eq({ x: 10, y: 20 })
143
- end
144
- end
145
-
146
- describe DecisionAgent::Dmn::Feel::Types::Converter do
147
- it "converts integer to Number" do
148
- result = DecisionAgent::Dmn::Feel::Types::Converter.to_feel_type(42)
149
- expect(result).to be_a(DecisionAgent::Dmn::Feel::Types::Number)
150
- expect(result.to_ruby).to eq(42)
151
- end
152
-
153
- it "converts array to List" do
154
- result = DecisionAgent::Dmn::Feel::Types::Converter.to_feel_type([1, 2, 3])
155
- expect(result).to be_a(DecisionAgent::Dmn::Feel::Types::List)
156
- expect(result.to_ruby).to eq([1, 2, 3])
157
- end
158
-
159
- it "converts hash to Context" do
160
- result = DecisionAgent::Dmn::Feel::Types::Converter.to_feel_type({ x: 10 })
161
- expect(result).to be_a(DecisionAgent::Dmn::Feel::Types::Context)
162
- expect(result.to_ruby).to eq({ x: 10 })
163
- end
164
-
165
- it "converts FEEL types to Ruby" do
166
- num = DecisionAgent::Dmn::Feel::Types::Number.new(42)
167
- result = DecisionAgent::Dmn::Feel::Types::Converter.to_ruby(num)
168
- expect(result).to eq(42)
169
- end
170
-
171
- it "returns non-FEEL types as-is" do
172
- result = DecisionAgent::Dmn::Feel::Types::Converter.to_ruby("hello")
173
- expect(result).to eq("hello")
174
- end
175
- end
176
- end
@@ -1,489 +0,0 @@
1
- require "spec_helper"
2
- require "decision_agent/dmn/feel/parser"
3
- require "decision_agent/dmn/feel/transformer"
4
- require "decision_agent/dmn/feel/evaluator"
5
-
6
- RSpec.describe "FEEL Parser and Evaluator" do
7
- let(:parser) { DecisionAgent::Dmn::Feel::Parser.new }
8
- let(:transformer) { DecisionAgent::Dmn::Feel::Transformer.new }
9
- let(:evaluator) { DecisionAgent::Dmn::Feel::Evaluator.new }
10
-
11
- describe "Literals" do
12
- it "parses numbers" do
13
- result = parser.parse("42")
14
- ast = transformer.apply(result)
15
- expect(ast[:type]).to eq(:number)
16
- expect(ast[:value]).to eq(42)
17
- end
18
-
19
- it "parses negative numbers" do
20
- result = parser.parse("-42")
21
- ast = transformer.apply(result)
22
- expect(ast[:type]).to eq(:number)
23
- expect(ast[:value]).to eq(-42)
24
- end
25
-
26
- it "parses floats" do
27
- result = parser.parse("3.14")
28
- ast = transformer.apply(result)
29
- expect(ast[:type]).to eq(:number)
30
- expect(ast[:value]).to eq(3.14)
31
- end
32
-
33
- it "parses strings" do
34
- result = parser.parse('"hello world"')
35
- ast = transformer.apply(result)
36
- expect(ast[:type]).to eq(:string)
37
- expect(ast[:value]).to eq("hello world")
38
- end
39
-
40
- it "parses booleans" do
41
- true_result = parser.parse("true")
42
- true_ast = transformer.apply(true_result)
43
- expect(true_ast[:type]).to eq(:boolean)
44
- expect(true_ast[:value]).to eq(true)
45
-
46
- false_result = parser.parse("false")
47
- false_ast = transformer.apply(false_result)
48
- expect(false_ast[:type]).to eq(:boolean)
49
- expect(false_ast[:value]).to eq(false)
50
- end
51
-
52
- it "parses null" do
53
- result = parser.parse("null")
54
- ast = transformer.apply(result)
55
- expect(ast[:type]).to eq(:null)
56
- expect(ast[:value]).to be_nil
57
- end
58
- end
59
-
60
- describe "Arithmetic Operations" do
61
- let(:context) { {} }
62
-
63
- it "evaluates addition" do
64
- result = evaluator.evaluate("5 + 3", "result", context)
65
- expect(result).to eq(8)
66
- end
67
-
68
- it "evaluates subtraction" do
69
- result = evaluator.evaluate("10 - 4", "result", context)
70
- expect(result).to eq(6)
71
- end
72
-
73
- it "evaluates multiplication" do
74
- result = evaluator.evaluate("6 * 7", "result", context)
75
- expect(result).to eq(42)
76
- end
77
-
78
- it "evaluates division" do
79
- result = evaluator.evaluate("20 / 4", "result", context)
80
- expect(result).to eq(5.0)
81
- end
82
-
83
- it "evaluates exponentiation" do
84
- result = evaluator.evaluate("2 ** 3", "result", context)
85
- expect(result).to eq(8)
86
- end
87
-
88
- it "evaluates modulo" do
89
- result = evaluator.evaluate("10 % 3", "result", context)
90
- expect(result).to eq(1)
91
- end
92
-
93
- it "respects operator precedence" do
94
- result = evaluator.evaluate("2 + 3 * 4", "result", context)
95
- expect(result).to eq(14)
96
- end
97
-
98
- it "evaluates parentheses" do
99
- result = evaluator.evaluate("(2 + 3) * 4", "result", context)
100
- expect(result).to eq(20)
101
- end
102
- end
103
-
104
- describe "Comparison Operations" do
105
- let(:context) { {} }
106
-
107
- it "evaluates equality" do
108
- expect(evaluator.evaluate("5 = 5", "result", context)).to eq(true)
109
- expect(evaluator.evaluate("5 = 3", "result", context)).to eq(false)
110
- end
111
-
112
- it "evaluates inequality" do
113
- expect(evaluator.evaluate("5 != 3", "result", context)).to eq(true)
114
- expect(evaluator.evaluate("5 != 5", "result", context)).to eq(false)
115
- end
116
-
117
- it "evaluates less than" do
118
- expect(evaluator.evaluate("3 < 5", "result", context)).to eq(true)
119
- expect(evaluator.evaluate("5 < 3", "result", context)).to eq(false)
120
- end
121
-
122
- it "evaluates greater than" do
123
- expect(evaluator.evaluate("5 > 3", "result", context)).to eq(true)
124
- expect(evaluator.evaluate("3 > 5", "result", context)).to eq(false)
125
- end
126
-
127
- it "evaluates less than or equal" do
128
- expect(evaluator.evaluate("3 <= 5", "result", context)).to eq(true)
129
- expect(evaluator.evaluate("5 <= 5", "result", context)).to eq(true)
130
- expect(evaluator.evaluate("7 <= 5", "result", context)).to eq(false)
131
- end
132
-
133
- it "evaluates greater than or equal" do
134
- expect(evaluator.evaluate("5 >= 3", "result", context)).to eq(true)
135
- expect(evaluator.evaluate("5 >= 5", "result", context)).to eq(true)
136
- expect(evaluator.evaluate("3 >= 5", "result", context)).to eq(false)
137
- end
138
- end
139
-
140
- describe "Logical Operations" do
141
- let(:context) { {} }
142
-
143
- it "evaluates AND" do
144
- expect(evaluator.evaluate("true and true", "result", context)).to eq(true)
145
- expect(evaluator.evaluate("true and false", "result", context)).to eq(false)
146
- expect(evaluator.evaluate("false and false", "result", context)).to eq(false)
147
- end
148
-
149
- it "evaluates OR" do
150
- expect(evaluator.evaluate("true or false", "result", context)).to eq(true)
151
- expect(evaluator.evaluate("false or true", "result", context)).to eq(true)
152
- expect(evaluator.evaluate("false or false", "result", context)).to eq(false)
153
- end
154
-
155
- it "evaluates NOT" do
156
- expect(evaluator.evaluate("not true", "result", context)).to eq(false)
157
- expect(evaluator.evaluate("not false", "result", context)).to eq(true)
158
- end
159
-
160
- it "evaluates complex logical expressions" do
161
- result = evaluator.evaluate("(5 > 3) and (10 < 20)", "result", context)
162
- expect(result).to eq(true)
163
- end
164
- end
165
-
166
- describe "Field References" do
167
- it "evaluates field references" do
168
- context = { age: 25 }
169
- result = evaluator.evaluate("age", "result", context)
170
- expect(result).to eq(25)
171
- end
172
-
173
- it "evaluates field references in comparisons" do
174
- context = { age: 25 }
175
- result = evaluator.evaluate("age >= 18", "age", context)
176
- expect(result).to eq(true)
177
- end
178
-
179
- it "evaluates field references in arithmetic" do
180
- context = { price: 100, quantity: 5 }
181
- result = evaluator.evaluate("price * quantity", "total", context)
182
- expect(result).to eq(500)
183
- end
184
- end
185
-
186
- describe "List Literals" do
187
- it "parses empty lists" do
188
- result = parser.parse("[]")
189
- ast = transformer.apply(result)
190
- expect(ast[:type]).to eq(:list_literal)
191
- end
192
-
193
- it "parses lists with elements" do
194
- result = parser.parse("[1, 2, 3]")
195
- ast = transformer.apply(result)
196
- expect(ast[:type]).to eq(:list_literal)
197
- end
198
-
199
- it "evaluates list literals" do
200
- context = {}
201
- result = evaluator.evaluate("[1, 2, 3]", "list", context)
202
- expect(result).to eq([1, 2, 3])
203
- end
204
- end
205
-
206
- describe "Context Literals" do
207
- it "parses empty contexts" do
208
- result = parser.parse("{}")
209
- ast = transformer.apply(result)
210
- expect(ast[:type]).to eq(:context_literal)
211
- end
212
-
213
- it "parses contexts with entries" do
214
- result = parser.parse('{ name: "John", age: 30 }')
215
- ast = transformer.apply(result)
216
- expect(ast[:type]).to eq(:context_literal)
217
- end
218
-
219
- it "evaluates context literals" do
220
- context = {}
221
- result = evaluator.evaluate("{ a: 1, b: 2 }", "ctx", context)
222
- expect(result).to eq({ a: 1, b: 2 })
223
- end
224
- end
225
-
226
- describe "Function Calls" do
227
- let(:context) { {} }
228
-
229
- it "evaluates string length function" do
230
- result = evaluator.evaluate('length("hello")', "result", context)
231
- expect(result).to eq(5)
232
- end
233
-
234
- it "evaluates substring function" do
235
- result = evaluator.evaluate('substring("hello", 2, 3)', "result", context)
236
- expect(result).to eq("ell")
237
- end
238
-
239
- it "evaluates upper case function" do
240
- result = evaluator.evaluate('upper("hello")', "result", context)
241
- expect(result).to eq("HELLO")
242
- end
243
-
244
- it "evaluates sum function" do
245
- result = evaluator.evaluate("sum([1, 2, 3, 4])", "result", context)
246
- expect(result).to eq(10.0)
247
- end
248
-
249
- it "evaluates mean function" do
250
- result = evaluator.evaluate("mean([10, 20, 30])", "result", context)
251
- expect(result).to eq(20.0)
252
- end
253
-
254
- it "evaluates min function" do
255
- result = evaluator.evaluate("min([5, 2, 8, 1])", "result", context)
256
- expect(result).to eq(1.0)
257
- end
258
-
259
- it "evaluates max function" do
260
- result = evaluator.evaluate("max([5, 2, 8, 1])", "result", context)
261
- expect(result).to eq(8.0)
262
- end
263
- end
264
-
265
- describe "If-Then-Else Conditionals" do
266
- let(:context) { {} }
267
-
268
- it "evaluates true condition" do
269
- result = evaluator.evaluate('if 5 > 3 then "big" else "small"', "result", context)
270
- expect(result).to eq("big")
271
- end
272
-
273
- it "evaluates false condition" do
274
- result = evaluator.evaluate('if 3 > 5 then "big" else "small"', "result", context)
275
- expect(result).to eq("small")
276
- end
277
-
278
- it "evaluates with field references" do
279
- context = { age: 25 }
280
- result = evaluator.evaluate('if age >= 18 then "adult" else "minor"', "status", context)
281
- expect(result).to eq("adult")
282
- end
283
-
284
- it "evaluates nested conditionals" do
285
- context = { score: 85 }
286
- result = evaluator.evaluate(
287
- 'if score >= 90 then "A" else if score >= 80 then "B" else "C"',
288
- "grade",
289
- context
290
- )
291
- expect(result).to eq("B")
292
- end
293
- end
294
-
295
- describe "Quantified Expressions" do
296
- it "evaluates 'some' expression - true case" do
297
- context = {}
298
- result = evaluator.evaluate("some x in [1, 5, 10] satisfies x > 8", "result", context)
299
- expect(result).to eq(true)
300
- end
301
-
302
- it "evaluates 'some' expression - false case" do
303
- context = {}
304
- result = evaluator.evaluate("some x in [1, 2, 3] satisfies x > 10", "result", context)
305
- expect(result).to eq(false)
306
- end
307
-
308
- it "evaluates 'every' expression - true case" do
309
- context = {}
310
- result = evaluator.evaluate("every x in [5, 10, 15] satisfies x > 0", "result", context)
311
- expect(result).to eq(true)
312
- end
313
-
314
- it "evaluates 'every' expression - false case" do
315
- context = {}
316
- result = evaluator.evaluate("every x in [1, 5, 10] satisfies x > 5", "result", context)
317
- expect(result).to eq(false)
318
- end
319
- end
320
-
321
- describe "For Expressions" do
322
- it "evaluates for expression with arithmetic" do
323
- context = {}
324
- result = evaluator.evaluate("for x in [1, 2, 3] return x * 2", "result", context)
325
- expect(result).to eq([2, 4, 6])
326
- end
327
-
328
- it "evaluates for expression with addition" do
329
- context = {}
330
- result = evaluator.evaluate("for x in [10, 20, 30] return x + 5", "result", context)
331
- expect(result).to eq([15, 25, 35])
332
- end
333
- end
334
-
335
- describe "Between Expressions" do
336
- it "evaluates between - true case" do
337
- context = {}
338
- result = evaluator.evaluate("5 between 1 and 10", "result", context)
339
- expect(result).to eq(true)
340
- end
341
-
342
- it "evaluates between - false case" do
343
- context = {}
344
- result = evaluator.evaluate("15 between 1 and 10", "result", context)
345
- expect(result).to eq(false)
346
- end
347
-
348
- it "evaluates between with field reference" do
349
- context = { age: 25 }
350
- result = evaluator.evaluate("age between 18 and 65", "working_age", context)
351
- expect(result).to eq(true)
352
- end
353
- end
354
-
355
- describe "Range Literals" do
356
- it "parses inclusive range" do
357
- result = parser.parse("[1..10]")
358
- ast = transformer.apply(result)
359
- expect(ast[:type]).to eq(:range)
360
- expect(ast[:start_inclusive]).to eq(true)
361
- expect(ast[:end_inclusive]).to eq(true)
362
- end
363
-
364
- it "parses exclusive start range" do
365
- result = parser.parse("(1..10]")
366
- ast = transformer.apply(result)
367
- expect(ast[:type]).to eq(:range)
368
- expect(ast[:start_inclusive]).to eq(false)
369
- expect(ast[:end_inclusive]).to eq(true)
370
- end
371
-
372
- it "parses exclusive end range" do
373
- result = parser.parse("[1..10)")
374
- ast = transformer.apply(result)
375
- expect(ast[:type]).to eq(:range)
376
- expect(ast[:start_inclusive]).to eq(true)
377
- expect(ast[:end_inclusive]).to eq(false)
378
- end
379
-
380
- it "parses fully exclusive range" do
381
- result = parser.parse("(1..10)")
382
- ast = transformer.apply(result)
383
- expect(ast[:type]).to eq(:range)
384
- expect(ast[:start_inclusive]).to eq(false)
385
- expect(ast[:end_inclusive]).to eq(false)
386
- end
387
- end
388
-
389
- describe "In Expressions" do
390
- it "evaluates in with list - true case" do
391
- context = {}
392
- result = evaluator.evaluate("5 in [1, 3, 5, 7]", "result", context)
393
- expect(result).to eq(true)
394
- end
395
-
396
- it "evaluates in with list - false case" do
397
- context = {}
398
- result = evaluator.evaluate("4 in [1, 3, 5, 7]", "result", context)
399
- expect(result).to eq(false)
400
- end
401
- end
402
-
403
- describe "Instance Of Expressions" do
404
- it "checks number type" do
405
- context = {}
406
- expect(evaluator.evaluate("42 instance of number", "result", context)).to eq(true)
407
- expect(evaluator.evaluate('"hello" instance of number', "result", context)).to eq(false)
408
- end
409
-
410
- it "checks string type" do
411
- context = {}
412
- expect(evaluator.evaluate('"hello" instance of string', "result", context)).to eq(true)
413
- expect(evaluator.evaluate("42 instance of string", "result", context)).to eq(false)
414
- end
415
-
416
- it "checks boolean type" do
417
- context = {}
418
- expect(evaluator.evaluate("true instance of boolean", "result", context)).to eq(true)
419
- expect(evaluator.evaluate("42 instance of boolean", "result", context)).to eq(false)
420
- end
421
-
422
- it "checks list type" do
423
- context = {}
424
- expect(evaluator.evaluate("[1, 2, 3] instance of list", "result", context)).to eq(true)
425
- expect(evaluator.evaluate("42 instance of list", "result", context)).to eq(false)
426
- end
427
-
428
- it "checks context type" do
429
- context = {}
430
- expect(evaluator.evaluate("{a: 1} instance of context", "result", context)).to eq(true)
431
- expect(evaluator.evaluate("42 instance of context", "result", context)).to eq(false)
432
- end
433
- end
434
-
435
- describe "Complex Expressions" do
436
- it "evaluates complex business rule" do
437
- context = {
438
- age: 25,
439
- income: 50_000,
440
- credit_score: 720
441
- }
442
-
443
- expr = "if age >= 18 and income >= 30000 and credit_score >= 650 then \"approved\" else \"denied\""
444
- result = evaluator.evaluate(expr, "loan_status", context)
445
- expect(result).to eq("approved")
446
- end
447
-
448
- it "evaluates nested arithmetic with comparisons" do
449
- context = {
450
- price: 100,
451
- quantity: 5,
452
- discount: 10
453
- }
454
-
455
- expr = "(price * quantity) - discount > 400"
456
- result = evaluator.evaluate(expr, "qualifies", context)
457
- expect(result).to eq(true)
458
- end
459
-
460
- it "combines lists and functions" do
461
- context = {}
462
- expr = "sum([1, 2, 3]) + max([4, 5, 6])"
463
- result = evaluator.evaluate(expr, "result", context)
464
- expect(result).to eq(12.0)
465
- end
466
-
467
- it "evaluates filter-like expression with quantifier" do
468
- context = {}
469
- expr = "some x in [10, 20, 30] satisfies x > 15"
470
- result = evaluator.evaluate(expr, "has_large", context)
471
- expect(result).to eq(true)
472
- end
473
- end
474
-
475
- describe "Error Handling" do
476
- it "raises error for invalid syntax" do
477
- expect do
478
- parser.parse("5 +")
479
- end.to raise_error(DecisionAgent::Dmn::FeelParseError)
480
- end
481
-
482
- it "falls back gracefully for unsupported expressions" do
483
- context = {}
484
- # Should fall back to literal equality
485
- result = evaluator.evaluate("unknown_syntax", "field", context)
486
- expect(result).to be_a(Hash) # Returns condition structure
487
- end
488
- end
489
- end