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
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Rack helpers for framework-agnostic web server
4
+ # These helpers provide convenience methods for pure Rack applications
5
+
6
+ require "rack"
7
+ require "rack/file"
8
+ require "uri"
9
+
10
+ module DecisionAgent
11
+ module Web
12
+ module RackHelpers
13
+ # Simple router for Rack applications
14
+ class Router
15
+ def initialize
16
+ @routes = []
17
+ @before_filters = []
18
+ end
19
+
20
+ def get(path, &block)
21
+ add_route("GET", path, block)
22
+ end
23
+
24
+ def post(path, &block)
25
+ add_route("POST", path, block)
26
+ end
27
+
28
+ def put(path, &block)
29
+ add_route("PUT", path, block)
30
+ end
31
+
32
+ def delete(path, &block)
33
+ add_route("DELETE", path, block)
34
+ end
35
+
36
+ def options(path, &block)
37
+ add_route("OPTIONS", path, block)
38
+ end
39
+
40
+ def before(&block)
41
+ @before_filters << block if block
42
+ end
43
+
44
+ def match(env)
45
+ method = env["REQUEST_METHOD"]
46
+ path = env["PATH_INFO"] || "/"
47
+ script_name = env["SCRIPT_NAME"] || ""
48
+
49
+ # Remove script_name prefix if present
50
+ path = path[script_name.length..] || "/" if script_name && !script_name.empty? && path.start_with?(script_name)
51
+
52
+ route = find_route(method, path)
53
+ return nil unless route
54
+
55
+ {
56
+ handler: route[:handler],
57
+ params: route[:params],
58
+ before_filters: @before_filters
59
+ }
60
+ end
61
+
62
+ private
63
+
64
+ def add_route(method, path_pattern, handler)
65
+ # Convert path patterns to regex
66
+ # Example: "/api/versions/:id" -> /^\/api\/versions\/(?<id>[^\/]+)$/
67
+ # Handle wildcard "*" for catch-all routes
68
+ regex_pattern = if path_pattern == "*"
69
+ ".*"
70
+ else
71
+ path_pattern
72
+ .gsub(%r{:[^/]+}) { |match| "(?<#{match[1..]}>[^/]+)" }
73
+ .gsub("*", ".*")
74
+ end
75
+ regex = /^#{regex_pattern}$/
76
+
77
+ @routes << {
78
+ method: method,
79
+ pattern: regex,
80
+ handler: handler,
81
+ path_pattern: path_pattern
82
+ }
83
+ end
84
+
85
+ def find_route(method, path)
86
+ # Try exact match first, then try routes in order
87
+ # More specific routes should be registered first
88
+ @routes.each do |route|
89
+ next unless [method, "*"].include?(route[:method])
90
+
91
+ match = route[:pattern].match(path)
92
+ next unless match
93
+
94
+ params = match.named_captures || {}
95
+ params.transform_keys!(&:to_sym) if params.any?
96
+ return {
97
+ handler: route[:handler],
98
+ params: params
99
+ }
100
+ end
101
+ nil
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Request/Response helpers for Rack applications
4
+ # Provides convenience methods for handling requests and responses
5
+
6
+ require "rack"
7
+ require "rack/utils"
8
+ require "uri"
9
+
10
+ module DecisionAgent
11
+ module Web
12
+ module RackRequestHelpers
13
+ # Context object that provides convenience methods in route handlers
14
+ class RequestContext
15
+ attr_reader :env, :request, :response_status, :response_headers, :response_body, :halted_response
16
+ attr_accessor :current_user, :current_session
17
+
18
+ def initialize(env, route_params = {})
19
+ @env = env
20
+ @request = Rack::Request.new(env)
21
+ @route_params = route_params
22
+ @response_status = 200
23
+ @response_headers = { "Content-Type" => "text/html" }
24
+ @response_body = []
25
+ @halted = false
26
+ @halted_response = nil
27
+ @params_hybrid = nil
28
+
29
+ # Merge route params, query params, and body params
30
+ # Convert all keys to symbols for consistency
31
+ route_params_sym = route_params.transform_keys(&:to_sym)
32
+ query_params_sym = query_params.transform_keys(&:to_sym)
33
+ body_params_sym = body_params.transform_keys(&:to_sym)
34
+ @params = route_params_sym.merge(query_params_sym).merge(body_params_sym)
35
+
36
+ # Handle multipart form data (file uploads)
37
+ content_type_header = @env["CONTENT_TYPE"] || ""
38
+ return unless content_type_header.include?("multipart/form-data")
39
+
40
+ # Rack::Request handles multipart automatically
41
+ multipart_params = @request.params
42
+ multipart_params_sym = multipart_params.transform_keys(&:to_sym)
43
+ @params.merge!(multipart_params_sym)
44
+ end
45
+
46
+ def params
47
+ # Return params with support for both symbol and string key access
48
+ @params ||= begin
49
+ hash = @params.dup
50
+ # Add string-key versions for all symbol keys
51
+ @params.each do |k, v|
52
+ hash[k.to_s] = v if k.is_a?(Symbol)
53
+ end
54
+ # Add symbol-key versions for all string keys
55
+ hash.to_a.each do |k, v|
56
+ hash[k.to_sym] = v if k.is_a?(String) && !hash.key?(k.to_sym)
57
+ end
58
+ # Create accessor that checks both
59
+ def hash.[](key)
60
+ super(key.to_sym) || super(key.to_s) || super
61
+ end
62
+ hash
63
+ end
64
+ end
65
+
66
+ def status(code)
67
+ @response_status = code
68
+ code
69
+ end
70
+
71
+ def content_type(type)
72
+ @response_headers["Content-Type"] = type
73
+ end
74
+
75
+ def headers
76
+ @response_headers
77
+ end
78
+
79
+ def body(str = nil)
80
+ if str
81
+ @response_body = [str.to_s]
82
+ str
83
+ else
84
+ @response_body
85
+ end
86
+ end
87
+
88
+ def json(obj)
89
+ content_type "application/json"
90
+ body(obj.to_json)
91
+ end
92
+
93
+ def halt(status_code, body = nil)
94
+ @halted = true
95
+ @response_status = status_code
96
+ if body
97
+ content_type "application/json" if @response_headers["Content-Type"] == "text/html"
98
+ @halted_response = [status_code, @response_headers.dup, [body.to_s]]
99
+ else
100
+ @halted_response = [status_code, @response_headers.dup, []]
101
+ end
102
+ end
103
+
104
+ def halted?
105
+ @halted
106
+ end
107
+
108
+ def send_file(filepath)
109
+ return unless File.exist?(filepath)
110
+
111
+ content = File.read(filepath)
112
+ ext = File.extname(filepath).downcase
113
+ mime_types = {
114
+ ".css" => "text/css",
115
+ ".js" => "application/javascript",
116
+ ".html" => "text/html",
117
+ ".json" => "application/json",
118
+ ".xml" => "application/xml",
119
+ ".svg" => "image/svg+xml"
120
+ }
121
+ content_type(mime_types[ext] || "application/octet-stream")
122
+ body(content)
123
+ end
124
+
125
+ def script_name
126
+ @request.script_name
127
+ end
128
+
129
+ def path_info
130
+ @request.path_info
131
+ end
132
+
133
+ def cookies
134
+ @request.cookies
135
+ end
136
+
137
+ def to_rack_response
138
+ if @halted && @halted_response
139
+ @halted_response
140
+ else
141
+ # Ensure body is an array
142
+ body_array = @response_body.is_a?(Array) ? @response_body : [@response_body.to_s]
143
+ body_array = [""] if body_array.empty?
144
+ [@response_status, @response_headers.dup, body_array]
145
+ end
146
+ end
147
+
148
+ private
149
+
150
+ def query_params
151
+ @request.params
152
+ end
153
+
154
+ def body_params
155
+ return {} unless @env["rack.input"]
156
+
157
+ # Read body if content type is JSON or form data
158
+ content_type_header = @env["CONTENT_TYPE"] || ""
159
+ body_input = @env["rack.input"].read
160
+ @env["rack.input"].rewind
161
+
162
+ return {} if body_input.nil? || body_input.empty?
163
+
164
+ if content_type_header.include?("application/json")
165
+ begin
166
+ parsed = JSON.parse(body_input)
167
+ # Convert string keys to symbol keys for consistency
168
+ parsed.is_a?(Hash) ? parsed.transform_keys(&:to_sym) : parsed
169
+ rescue JSON::ParserError
170
+ {}
171
+ end
172
+ elsif content_type_header.include?("application/x-www-form-urlencoded")
173
+ parsed = Rack::Utils.parse_nested_query(body_input)
174
+ parsed.is_a?(Hash) ? parsed.transform_keys(&:to_sym) : parsed
175
+ elsif content_type_header.include?("multipart/form-data")
176
+ # For multipart, use Rack::Request.params which handles it automatically
177
+ # This will be merged in initialize
178
+ {}
179
+ else
180
+ {}
181
+ end
182
+ end
183
+ end
184
+
185
+ # Helper to read request body as string
186
+ def self.read_body(env)
187
+ input = env["rack.input"]
188
+ return "" unless input
189
+
190
+ body = input.read
191
+ input.rewind
192
+ body.to_s
193
+ end
194
+ end
195
+ end
196
+ end