tina4ruby 3.11.15 → 3.11.17

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 (134) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -80
  3. data/LICENSE.txt +21 -21
  4. data/README.md +137 -137
  5. data/exe/tina4ruby +5 -5
  6. data/lib/tina4/ai.rb +696 -696
  7. data/lib/tina4/api.rb +189 -189
  8. data/lib/tina4/auth.rb +305 -305
  9. data/lib/tina4/auto_crud.rb +244 -244
  10. data/lib/tina4/cache.rb +154 -154
  11. data/lib/tina4/cli.rb +1449 -1449
  12. data/lib/tina4/constants.rb +46 -46
  13. data/lib/tina4/container.rb +74 -74
  14. data/lib/tina4/cors.rb +74 -74
  15. data/lib/tina4/crud.rb +692 -692
  16. data/lib/tina4/database/sqlite3_adapter.rb +165 -165
  17. data/lib/tina4/database.rb +625 -625
  18. data/lib/tina4/database_result.rb +208 -208
  19. data/lib/tina4/debug.rb +8 -8
  20. data/lib/tina4/dev.rb +14 -14
  21. data/lib/tina4/dev_admin.rb +1291 -935
  22. data/lib/tina4/dev_mailbox.rb +191 -191
  23. data/lib/tina4/drivers/firebird_driver.rb +124 -124
  24. data/lib/tina4/drivers/mongodb_driver.rb +561 -561
  25. data/lib/tina4/drivers/mssql_driver.rb +112 -112
  26. data/lib/tina4/drivers/mysql_driver.rb +90 -90
  27. data/lib/tina4/drivers/odbc_driver.rb +191 -191
  28. data/lib/tina4/drivers/postgres_driver.rb +116 -116
  29. data/lib/tina4/drivers/sqlite_driver.rb +122 -122
  30. data/lib/tina4/env.rb +95 -95
  31. data/lib/tina4/error_overlay.rb +252 -252
  32. data/lib/tina4/events.rb +109 -109
  33. data/lib/tina4/field_types.rb +154 -154
  34. data/lib/tina4/frond.rb +2087 -2025
  35. data/lib/tina4/gallery/auth/meta.json +1 -1
  36. data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +114 -114
  37. data/lib/tina4/gallery/database/meta.json +1 -1
  38. data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +43 -43
  39. data/lib/tina4/gallery/error-overlay/meta.json +1 -1
  40. data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +17 -17
  41. data/lib/tina4/gallery/orm/meta.json +1 -1
  42. data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +16 -16
  43. data/lib/tina4/gallery/queue/meta.json +1 -1
  44. data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +325 -325
  45. data/lib/tina4/gallery/rest-api/meta.json +1 -1
  46. data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +14 -14
  47. data/lib/tina4/gallery/templates/meta.json +1 -1
  48. data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +12 -12
  49. data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +257 -257
  50. data/lib/tina4/graphql.rb +966 -966
  51. data/lib/tina4/health.rb +39 -39
  52. data/lib/tina4/html_element.rb +170 -170
  53. data/lib/tina4/job.rb +80 -80
  54. data/lib/tina4/localization.rb +168 -168
  55. data/lib/tina4/log.rb +203 -203
  56. data/lib/tina4/mcp.rb +871 -696
  57. data/lib/tina4/messenger.rb +587 -587
  58. data/lib/tina4/metrics.rb +793 -793
  59. data/lib/tina4/middleware.rb +445 -445
  60. data/lib/tina4/migration.rb +451 -451
  61. data/lib/tina4/orm.rb +790 -790
  62. data/lib/tina4/plan.rb +471 -0
  63. data/lib/tina4/project_index.rb +366 -0
  64. data/lib/tina4/public/css/tina4.css +2463 -2463
  65. data/lib/tina4/public/css/tina4.min.css +1 -1
  66. data/lib/tina4/public/images/logo.svg +5 -5
  67. data/lib/tina4/public/js/frond.min.js +2 -2
  68. data/lib/tina4/public/js/tina4-dev-admin.js +1264 -565
  69. data/lib/tina4/public/js/tina4-dev-admin.min.js +1264 -480
  70. data/lib/tina4/public/js/tina4.min.js +92 -92
  71. data/lib/tina4/public/js/tina4js.min.js +48 -48
  72. data/lib/tina4/public/swagger/index.html +90 -90
  73. data/lib/tina4/public/swagger/oauth2-redirect.html +63 -63
  74. data/lib/tina4/query_builder.rb +380 -380
  75. data/lib/tina4/queue.rb +366 -366
  76. data/lib/tina4/queue_backends/kafka_backend.rb +80 -80
  77. data/lib/tina4/queue_backends/lite_backend.rb +298 -298
  78. data/lib/tina4/queue_backends/mongo_backend.rb +126 -126
  79. data/lib/tina4/queue_backends/rabbitmq_backend.rb +73 -73
  80. data/lib/tina4/rack_app.rb +817 -817
  81. data/lib/tina4/rate_limiter.rb +130 -130
  82. data/lib/tina4/request.rb +268 -268
  83. data/lib/tina4/response.rb +346 -346
  84. data/lib/tina4/response_cache.rb +551 -551
  85. data/lib/tina4/router.rb +406 -406
  86. data/lib/tina4/scss/tina4css/_alerts.scss +34 -34
  87. data/lib/tina4/scss/tina4css/_badges.scss +22 -22
  88. data/lib/tina4/scss/tina4css/_buttons.scss +69 -69
  89. data/lib/tina4/scss/tina4css/_cards.scss +49 -49
  90. data/lib/tina4/scss/tina4css/_forms.scss +156 -156
  91. data/lib/tina4/scss/tina4css/_grid.scss +81 -81
  92. data/lib/tina4/scss/tina4css/_modals.scss +84 -84
  93. data/lib/tina4/scss/tina4css/_nav.scss +149 -149
  94. data/lib/tina4/scss/tina4css/_reset.scss +94 -94
  95. data/lib/tina4/scss/tina4css/_tables.scss +54 -54
  96. data/lib/tina4/scss/tina4css/_typography.scss +55 -55
  97. data/lib/tina4/scss/tina4css/_utilities.scss +197 -197
  98. data/lib/tina4/scss/tina4css/_variables.scss +117 -117
  99. data/lib/tina4/scss/tina4css/base.scss +1 -1
  100. data/lib/tina4/scss/tina4css/colors.scss +48 -48
  101. data/lib/tina4/scss/tina4css/tina4.scss +17 -17
  102. data/lib/tina4/scss_compiler.rb +178 -178
  103. data/lib/tina4/seeder.rb +567 -567
  104. data/lib/tina4/service_runner.rb +303 -303
  105. data/lib/tina4/session.rb +297 -297
  106. data/lib/tina4/session_handlers/database_handler.rb +72 -72
  107. data/lib/tina4/session_handlers/file_handler.rb +67 -67
  108. data/lib/tina4/session_handlers/mongo_handler.rb +49 -49
  109. data/lib/tina4/session_handlers/redis_handler.rb +43 -43
  110. data/lib/tina4/session_handlers/valkey_handler.rb +43 -43
  111. data/lib/tina4/shutdown.rb +84 -84
  112. data/lib/tina4/sql_translation.rb +158 -158
  113. data/lib/tina4/swagger.rb +124 -124
  114. data/lib/tina4/template.rb +894 -894
  115. data/lib/tina4/templates/base.twig +26 -26
  116. data/lib/tina4/templates/errors/302.twig +14 -14
  117. data/lib/tina4/templates/errors/401.twig +9 -9
  118. data/lib/tina4/templates/errors/403.twig +29 -29
  119. data/lib/tina4/templates/errors/404.twig +29 -29
  120. data/lib/tina4/templates/errors/500.twig +38 -38
  121. data/lib/tina4/templates/errors/502.twig +9 -9
  122. data/lib/tina4/templates/errors/503.twig +12 -12
  123. data/lib/tina4/templates/errors/base.twig +37 -37
  124. data/lib/tina4/test_client.rb +159 -159
  125. data/lib/tina4/testing.rb +340 -340
  126. data/lib/tina4/validator.rb +174 -174
  127. data/lib/tina4/version.rb +1 -1
  128. data/lib/tina4/webserver.rb +312 -312
  129. data/lib/tina4/websocket.rb +343 -343
  130. data/lib/tina4/websocket_backplane.rb +190 -190
  131. data/lib/tina4/wsdl.rb +564 -564
  132. data/lib/tina4.rb +460 -458
  133. data/lib/tina4ruby.rb +4 -4
  134. metadata +5 -3
data/lib/tina4/request.rb CHANGED
@@ -1,268 +1,268 @@
1
- # frozen_string_literal: true
2
- require "uri"
3
- require "json"
4
-
5
- module Tina4
6
- # A Hash subclass that supports indifferent access (both string and symbol keys).
7
- # Used by Request#params so that params[:id] and params["id"] both work.
8
- class IndifferentHash < Hash
9
- def [](key)
10
- super(convert_key(key))
11
- end
12
-
13
- def []=(key, value)
14
- super(convert_key(key), value)
15
- end
16
-
17
- def fetch(key, *args, &block)
18
- super(convert_key(key), *args, &block)
19
- end
20
-
21
- def key?(key)
22
- super(convert_key(key))
23
- end
24
- alias has_key? key?
25
- alias include? key?
26
-
27
- def delete(key, &block)
28
- super(convert_key(key), &block)
29
- end
30
-
31
- private
32
-
33
- def convert_key(key)
34
- key.is_a?(Symbol) ? key.to_s : key
35
- end
36
- end
37
-
38
- class Request
39
- attr_reader :env, :method, :path, :query_string, :content_type,
40
- :path_params, :ip
41
- attr_accessor :user
42
-
43
- # Maximum upload size in bytes (default 10 MB). Override via TINA4_MAX_UPLOAD_SIZE env var.
44
- TINA4_MAX_UPLOAD_SIZE = Integer(ENV.fetch("TINA4_MAX_UPLOAD_SIZE", 10_485_760))
45
-
46
- class PayloadTooLarge < StandardError; end
47
-
48
- def initialize(env, path_params = {})
49
- @env = env
50
- @method = env["REQUEST_METHOD"]
51
- @path = env["PATH_INFO"] || "/"
52
- @query_string = env["QUERY_STRING"] || ""
53
- @content_type = env["CONTENT_TYPE"] || ""
54
- @path_params = path_params
55
-
56
- # Check upload size limit
57
- content_length = (env["CONTENT_LENGTH"] || 0).to_i
58
- if content_length > TINA4_MAX_UPLOAD_SIZE
59
- raise PayloadTooLarge,
60
- "Request body (#{content_length} bytes) exceeds TINA4_MAX_UPLOAD_SIZE (#{TINA4_MAX_UPLOAD_SIZE} bytes)"
61
- end
62
-
63
- # Client IP with X-Forwarded-For support
64
- @ip = extract_client_ip
65
-
66
- # Lazy-initialized fields (nil = not yet computed)
67
- @headers = nil
68
- @cookies = nil
69
- @session = nil
70
- @body_raw = nil
71
- @params = nil
72
- @files = nil
73
- @json_body = nil
74
- @query_hash = nil
75
- @body_parsed = nil
76
- end
77
-
78
- # Full URL reconstruction
79
- def url
80
- scheme = env["rack.url_scheme"] || "http"
81
- host = env["HTTP_HOST"] || env["SERVER_NAME"] || "localhost"
82
- port = env["SERVER_PORT"]
83
- url_str = "#{scheme}://#{host}"
84
- url_str += ":#{port}" if port && port != "80" && port != "443"
85
- url_str += @path
86
- url_str += "?#{@query_string}" unless @query_string.empty?
87
- url_str
88
- end
89
-
90
- # Lazy accessors
91
- def headers
92
- @headers ||= extract_headers
93
- end
94
-
95
- def cookies
96
- @cookies ||= parse_cookies
97
- end
98
-
99
- def session
100
- @session ||= Tina4::Session.new(@env)
101
- end
102
-
103
- # Raw body string
104
- def body
105
- @body_raw ||= read_body
106
- end
107
-
108
- # Parsed body (JSON or form data)
109
- def body_parsed
110
- @body_parsed ||= parse_body
111
- end
112
-
113
- # Parsed query string as hash
114
- def query
115
- @query_hash ||= parse_query_to_hash(@query_string)
116
- end
117
-
118
- def files
119
- @files ||= extract_files
120
- end
121
-
122
- # Merged params: query + body + path_params (path_params highest priority)
123
- # Supports both string and symbol key access (indifferent access).
124
- def params
125
- @params ||= build_params
126
- end
127
-
128
- # Look up a param by symbol or string key (indifferent access shortcut).
129
- def param(key, default = nil)
130
- params[key.to_s] || params[key.to_sym] || default
131
- end
132
-
133
- def [](key)
134
- params[key.to_s] || params[key.to_sym] || @path_params[key.to_sym]
135
- end
136
-
137
- def header(name)
138
- headers[name.to_s.downcase.gsub("-", "_")]
139
- end
140
-
141
- def json_body
142
- @json_body ||= begin
143
- JSON.parse(body)
144
- rescue JSON::ParserError, TypeError
145
- {}
146
- end
147
- end
148
-
149
- def bearer_token
150
- auth = header("authorization") || ""
151
- auth.sub(/\ABearer\s+/i, "") if auth =~ /\ABearer\s+/i
152
- end
153
-
154
- private
155
-
156
- def extract_client_ip
157
- # Check X-Forwarded-For first (proxy/load balancer)
158
- forwarded = @env["HTTP_X_FORWARDED_FOR"]
159
- if forwarded && !forwarded.empty?
160
- # Take the first (original client) IP
161
- forwarded.split(",").first.strip
162
- else
163
- @env["HTTP_X_REAL_IP"] || @env["REMOTE_ADDR"] || "127.0.0.1"
164
- end
165
- end
166
-
167
- def extract_headers
168
- h = {}
169
- @env.each do |key, value|
170
- if key.start_with?("HTTP_")
171
- h[key[5..-1].downcase] = value
172
- end
173
- end
174
- h
175
- end
176
-
177
- def parse_cookies
178
- cookie_str = @env["HTTP_COOKIE"]
179
- return {} unless cookie_str && !cookie_str.empty?
180
-
181
- result = {}
182
- cookie_str.split(";").each do |pair|
183
- key, value = pair.strip.split("=", 2)
184
- result[key] = value if key
185
- end
186
- result
187
- end
188
-
189
- def read_body
190
- input = @env["rack.input"]
191
- return "" unless input
192
- input.rewind if input.respond_to?(:rewind)
193
- data = input.read || ""
194
- input.rewind if input.respond_to?(:rewind)
195
- data
196
- end
197
-
198
- def parse_body
199
- if @content_type.include?("application/json")
200
- json_body
201
- elsif @content_type.include?("application/x-www-form-urlencoded")
202
- parse_query_to_hash(body)
203
- elsif @content_type.include?("multipart/form-data")
204
- # Extract form fields from Rack's parsed multipart data.
205
- # Files are handled separately by extract_files.
206
- result = {}
207
- form_hash = @env["rack.request.form_hash"] rescue nil
208
- if form_hash
209
- form_hash.each do |key, value|
210
- # Skip file entries (handled by extract_files)
211
- next if value.is_a?(Hash) && value[:tempfile]
212
- result[key] = value
213
- end
214
- end
215
- result
216
- else
217
- {}
218
- end
219
- end
220
-
221
- def build_params
222
- p = IndifferentHash.new
223
-
224
- # Query string params
225
- query.each { |k, v| p[k.to_s] = v }
226
-
227
- # Body params
228
- body_parsed.each { |k, v| p[k.to_s] = v }
229
-
230
- # Path params (highest priority)
231
- @path_params.each { |k, v| p[k.to_s] = v }
232
- p
233
- end
234
-
235
- def parse_query_to_hash(qs)
236
- result = {}
237
- return result if qs.nil? || qs.empty?
238
- qs.split("&").each do |pair|
239
- key, value = pair.split("=", 2)
240
- result[URI.decode_www_form_component(key.to_s)] = URI.decode_www_form_component(value.to_s)
241
- end
242
- result
243
- end
244
-
245
- def extract_files
246
- result = {}
247
- return result unless @content_type.include?("multipart/form-data")
248
- begin
249
- form_hash = @env["rack.request.form_hash"]
250
- if form_hash
251
- form_hash.each do |key, value|
252
- if value.is_a?(Hash) && value[:tempfile]
253
- result[key] = {
254
- filename: value[:filename],
255
- type: value[:type],
256
- tempfile: value[:tempfile],
257
- size: value[:tempfile].size
258
- }
259
- end
260
- end
261
- end
262
- rescue StandardError
263
- # Multipart parsing failed
264
- end
265
- result
266
- end
267
- end
268
- end
1
+ # frozen_string_literal: true
2
+ require "uri"
3
+ require "json"
4
+
5
+ module Tina4
6
+ # A Hash subclass that supports indifferent access (both string and symbol keys).
7
+ # Used by Request#params so that params[:id] and params["id"] both work.
8
+ class IndifferentHash < Hash
9
+ def [](key)
10
+ super(convert_key(key))
11
+ end
12
+
13
+ def []=(key, value)
14
+ super(convert_key(key), value)
15
+ end
16
+
17
+ def fetch(key, *args, &block)
18
+ super(convert_key(key), *args, &block)
19
+ end
20
+
21
+ def key?(key)
22
+ super(convert_key(key))
23
+ end
24
+ alias has_key? key?
25
+ alias include? key?
26
+
27
+ def delete(key, &block)
28
+ super(convert_key(key), &block)
29
+ end
30
+
31
+ private
32
+
33
+ def convert_key(key)
34
+ key.is_a?(Symbol) ? key.to_s : key
35
+ end
36
+ end
37
+
38
+ class Request
39
+ attr_reader :env, :method, :path, :query_string, :content_type,
40
+ :path_params, :ip
41
+ attr_accessor :user
42
+
43
+ # Maximum upload size in bytes (default 10 MB). Override via TINA4_MAX_UPLOAD_SIZE env var.
44
+ TINA4_MAX_UPLOAD_SIZE = Integer(ENV.fetch("TINA4_MAX_UPLOAD_SIZE", 10_485_760))
45
+
46
+ class PayloadTooLarge < StandardError; end
47
+
48
+ def initialize(env, path_params = {})
49
+ @env = env
50
+ @method = env["REQUEST_METHOD"]
51
+ @path = env["PATH_INFO"] || "/"
52
+ @query_string = env["QUERY_STRING"] || ""
53
+ @content_type = env["CONTENT_TYPE"] || ""
54
+ @path_params = path_params
55
+
56
+ # Check upload size limit
57
+ content_length = (env["CONTENT_LENGTH"] || 0).to_i
58
+ if content_length > TINA4_MAX_UPLOAD_SIZE
59
+ raise PayloadTooLarge,
60
+ "Request body (#{content_length} bytes) exceeds TINA4_MAX_UPLOAD_SIZE (#{TINA4_MAX_UPLOAD_SIZE} bytes)"
61
+ end
62
+
63
+ # Client IP with X-Forwarded-For support
64
+ @ip = extract_client_ip
65
+
66
+ # Lazy-initialized fields (nil = not yet computed)
67
+ @headers = nil
68
+ @cookies = nil
69
+ @session = nil
70
+ @body_raw = nil
71
+ @params = nil
72
+ @files = nil
73
+ @json_body = nil
74
+ @query_hash = nil
75
+ @body_parsed = nil
76
+ end
77
+
78
+ # Full URL reconstruction
79
+ def url
80
+ scheme = env["rack.url_scheme"] || "http"
81
+ host = env["HTTP_HOST"] || env["SERVER_NAME"] || "localhost"
82
+ port = env["SERVER_PORT"]
83
+ url_str = "#{scheme}://#{host}"
84
+ url_str += ":#{port}" if port && port != "80" && port != "443"
85
+ url_str += @path
86
+ url_str += "?#{@query_string}" unless @query_string.empty?
87
+ url_str
88
+ end
89
+
90
+ # Lazy accessors
91
+ def headers
92
+ @headers ||= extract_headers
93
+ end
94
+
95
+ def cookies
96
+ @cookies ||= parse_cookies
97
+ end
98
+
99
+ def session
100
+ @session ||= Tina4::Session.new(@env)
101
+ end
102
+
103
+ # Raw body string
104
+ def body
105
+ @body_raw ||= read_body
106
+ end
107
+
108
+ # Parsed body (JSON or form data)
109
+ def body_parsed
110
+ @body_parsed ||= parse_body
111
+ end
112
+
113
+ # Parsed query string as hash
114
+ def query
115
+ @query_hash ||= parse_query_to_hash(@query_string)
116
+ end
117
+
118
+ def files
119
+ @files ||= extract_files
120
+ end
121
+
122
+ # Merged params: query + body + path_params (path_params highest priority)
123
+ # Supports both string and symbol key access (indifferent access).
124
+ def params
125
+ @params ||= build_params
126
+ end
127
+
128
+ # Look up a param by symbol or string key (indifferent access shortcut).
129
+ def param(key, default = nil)
130
+ params[key.to_s] || params[key.to_sym] || default
131
+ end
132
+
133
+ def [](key)
134
+ params[key.to_s] || params[key.to_sym] || @path_params[key.to_sym]
135
+ end
136
+
137
+ def header(name)
138
+ headers[name.to_s.downcase.gsub("-", "_")]
139
+ end
140
+
141
+ def json_body
142
+ @json_body ||= begin
143
+ JSON.parse(body)
144
+ rescue JSON::ParserError, TypeError
145
+ {}
146
+ end
147
+ end
148
+
149
+ def bearer_token
150
+ auth = header("authorization") || ""
151
+ auth.sub(/\ABearer\s+/i, "") if auth =~ /\ABearer\s+/i
152
+ end
153
+
154
+ private
155
+
156
+ def extract_client_ip
157
+ # Check X-Forwarded-For first (proxy/load balancer)
158
+ forwarded = @env["HTTP_X_FORWARDED_FOR"]
159
+ if forwarded && !forwarded.empty?
160
+ # Take the first (original client) IP
161
+ forwarded.split(",").first.strip
162
+ else
163
+ @env["HTTP_X_REAL_IP"] || @env["REMOTE_ADDR"] || "127.0.0.1"
164
+ end
165
+ end
166
+
167
+ def extract_headers
168
+ h = {}
169
+ @env.each do |key, value|
170
+ if key.start_with?("HTTP_")
171
+ h[key[5..-1].downcase] = value
172
+ end
173
+ end
174
+ h
175
+ end
176
+
177
+ def parse_cookies
178
+ cookie_str = @env["HTTP_COOKIE"]
179
+ return {} unless cookie_str && !cookie_str.empty?
180
+
181
+ result = {}
182
+ cookie_str.split(";").each do |pair|
183
+ key, value = pair.strip.split("=", 2)
184
+ result[key] = value if key
185
+ end
186
+ result
187
+ end
188
+
189
+ def read_body
190
+ input = @env["rack.input"]
191
+ return "" unless input
192
+ input.rewind if input.respond_to?(:rewind)
193
+ data = input.read || ""
194
+ input.rewind if input.respond_to?(:rewind)
195
+ data
196
+ end
197
+
198
+ def parse_body
199
+ if @content_type.include?("application/json")
200
+ json_body
201
+ elsif @content_type.include?("application/x-www-form-urlencoded")
202
+ parse_query_to_hash(body)
203
+ elsif @content_type.include?("multipart/form-data")
204
+ # Extract form fields from Rack's parsed multipart data.
205
+ # Files are handled separately by extract_files.
206
+ result = {}
207
+ form_hash = @env["rack.request.form_hash"] rescue nil
208
+ if form_hash
209
+ form_hash.each do |key, value|
210
+ # Skip file entries (handled by extract_files)
211
+ next if value.is_a?(Hash) && value[:tempfile]
212
+ result[key] = value
213
+ end
214
+ end
215
+ result
216
+ else
217
+ {}
218
+ end
219
+ end
220
+
221
+ def build_params
222
+ p = IndifferentHash.new
223
+
224
+ # Query string params
225
+ query.each { |k, v| p[k.to_s] = v }
226
+
227
+ # Body params
228
+ body_parsed.each { |k, v| p[k.to_s] = v }
229
+
230
+ # Path params (highest priority)
231
+ @path_params.each { |k, v| p[k.to_s] = v }
232
+ p
233
+ end
234
+
235
+ def parse_query_to_hash(qs)
236
+ result = {}
237
+ return result if qs.nil? || qs.empty?
238
+ qs.split("&").each do |pair|
239
+ key, value = pair.split("=", 2)
240
+ result[URI.decode_www_form_component(key.to_s)] = URI.decode_www_form_component(value.to_s)
241
+ end
242
+ result
243
+ end
244
+
245
+ def extract_files
246
+ result = {}
247
+ return result unless @content_type.include?("multipart/form-data")
248
+ begin
249
+ form_hash = @env["rack.request.form_hash"]
250
+ if form_hash
251
+ form_hash.each do |key, value|
252
+ if value.is_a?(Hash) && value[:tempfile]
253
+ result[key] = {
254
+ filename: value[:filename],
255
+ type: value[:type],
256
+ tempfile: value[:tempfile],
257
+ size: value[:tempfile].size
258
+ }
259
+ end
260
+ end
261
+ end
262
+ rescue StandardError
263
+ # Multipart parsing failed
264
+ end
265
+ result
266
+ end
267
+ end
268
+ end