spikard 0.3.5 → 0.5.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 (142) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +674 -659
  4. data/ext/spikard_rb/Cargo.toml +17 -17
  5. data/ext/spikard_rb/extconf.rb +10 -10
  6. data/ext/spikard_rb/src/lib.rs +6 -6
  7. data/lib/spikard/app.rb +405 -386
  8. data/lib/spikard/background.rb +27 -27
  9. data/lib/spikard/config.rb +396 -396
  10. data/lib/spikard/converters.rb +13 -13
  11. data/lib/spikard/handler_wrapper.rb +113 -113
  12. data/lib/spikard/provide.rb +214 -214
  13. data/lib/spikard/response.rb +173 -173
  14. data/lib/spikard/schema.rb +243 -243
  15. data/lib/spikard/sse.rb +111 -111
  16. data/lib/spikard/streaming_response.rb +44 -44
  17. data/lib/spikard/testing.rb +256 -221
  18. data/lib/spikard/upload_file.rb +131 -131
  19. data/lib/spikard/version.rb +5 -5
  20. data/lib/spikard/websocket.rb +59 -59
  21. data/lib/spikard.rb +43 -43
  22. data/sig/spikard.rbs +366 -360
  23. data/vendor/crates/spikard-bindings-shared/Cargo.toml +63 -0
  24. data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +132 -0
  25. data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +752 -0
  26. data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +194 -0
  27. data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +246 -0
  28. data/vendor/crates/spikard-bindings-shared/src/error_response.rs +401 -0
  29. data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +238 -0
  30. data/vendor/crates/spikard-bindings-shared/src/lib.rs +24 -0
  31. data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +292 -0
  32. data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +616 -0
  33. data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +305 -0
  34. data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +248 -0
  35. data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +351 -0
  36. data/vendor/crates/spikard-bindings-shared/tests/comprehensive_coverage.rs +454 -0
  37. data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +383 -0
  38. data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +280 -0
  39. data/vendor/crates/spikard-core/Cargo.toml +40 -40
  40. data/vendor/crates/spikard-core/src/bindings/mod.rs +3 -3
  41. data/vendor/crates/spikard-core/src/bindings/response.rs +133 -133
  42. data/vendor/crates/spikard-core/src/debug.rs +127 -63
  43. data/vendor/crates/spikard-core/src/di/container.rs +702 -726
  44. data/vendor/crates/spikard-core/src/di/dependency.rs +273 -273
  45. data/vendor/crates/spikard-core/src/di/error.rs +118 -118
  46. data/vendor/crates/spikard-core/src/di/factory.rs +534 -538
  47. data/vendor/crates/spikard-core/src/di/graph.rs +506 -545
  48. data/vendor/crates/spikard-core/src/di/mod.rs +192 -192
  49. data/vendor/crates/spikard-core/src/di/resolved.rs +405 -411
  50. data/vendor/crates/spikard-core/src/di/value.rs +281 -283
  51. data/vendor/crates/spikard-core/src/errors.rs +69 -39
  52. data/vendor/crates/spikard-core/src/http.rs +415 -153
  53. data/vendor/crates/spikard-core/src/lib.rs +29 -29
  54. data/vendor/crates/spikard-core/src/lifecycle.rs +1186 -422
  55. data/vendor/crates/spikard-core/src/metadata.rs +389 -0
  56. data/vendor/crates/spikard-core/src/parameters.rs +2525 -722
  57. data/vendor/crates/spikard-core/src/problem.rs +344 -310
  58. data/vendor/crates/spikard-core/src/request_data.rs +1154 -189
  59. data/vendor/crates/spikard-core/src/router.rs +510 -249
  60. data/vendor/crates/spikard-core/src/schema_registry.rs +183 -183
  61. data/vendor/crates/spikard-core/src/type_hints.rs +304 -304
  62. data/vendor/crates/spikard-core/src/validation/error_mapper.rs +688 -0
  63. data/vendor/crates/spikard-core/src/{validation.rs → validation/mod.rs} +457 -699
  64. data/vendor/crates/spikard-http/Cargo.toml +64 -68
  65. data/vendor/crates/spikard-http/examples/sse-notifications.rs +148 -0
  66. data/vendor/crates/spikard-http/examples/websocket-chat.rs +92 -0
  67. data/vendor/crates/spikard-http/src/auth.rs +296 -247
  68. data/vendor/crates/spikard-http/src/background.rs +1860 -249
  69. data/vendor/crates/spikard-http/src/bindings/mod.rs +3 -3
  70. data/vendor/crates/spikard-http/src/bindings/response.rs +1 -1
  71. data/vendor/crates/spikard-http/src/body_metadata.rs +8 -8
  72. data/vendor/crates/spikard-http/src/cors.rs +1005 -490
  73. data/vendor/crates/spikard-http/src/debug.rs +128 -63
  74. data/vendor/crates/spikard-http/src/di_handler.rs +1668 -423
  75. data/vendor/crates/spikard-http/src/handler_response.rs +901 -190
  76. data/vendor/crates/spikard-http/src/handler_trait.rs +830 -228
  77. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +290 -284
  78. data/vendor/crates/spikard-http/src/lib.rs +534 -529
  79. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +230 -149
  80. data/vendor/crates/spikard-http/src/lifecycle.rs +1193 -428
  81. data/vendor/crates/spikard-http/src/middleware/mod.rs +540 -285
  82. data/vendor/crates/spikard-http/src/middleware/multipart.rs +912 -86
  83. data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +513 -147
  84. data/vendor/crates/spikard-http/src/middleware/validation.rs +735 -287
  85. data/vendor/crates/spikard-http/src/openapi/mod.rs +309 -309
  86. data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +535 -190
  87. data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +1363 -308
  88. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +665 -195
  89. data/vendor/crates/spikard-http/src/query_parser.rs +793 -369
  90. data/vendor/crates/spikard-http/src/response.rs +720 -399
  91. data/vendor/crates/spikard-http/src/server/handler.rs +1650 -87
  92. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +234 -98
  93. data/vendor/crates/spikard-http/src/server/mod.rs +1502 -805
  94. data/vendor/crates/spikard-http/src/server/request_extraction.rs +770 -119
  95. data/vendor/crates/spikard-http/src/server/routing_factory.rs +599 -0
  96. data/vendor/crates/spikard-http/src/sse.rs +1409 -447
  97. data/vendor/crates/spikard-http/src/testing/form.rs +52 -14
  98. data/vendor/crates/spikard-http/src/testing/multipart.rs +60 -60
  99. data/vendor/crates/spikard-http/src/testing/test_client.rs +283 -285
  100. data/vendor/crates/spikard-http/src/testing.rs +377 -377
  101. data/vendor/crates/spikard-http/src/websocket.rs +1375 -324
  102. data/vendor/crates/spikard-http/tests/background_behavior.rs +832 -0
  103. data/vendor/crates/spikard-http/tests/common/handlers.rs +309 -0
  104. data/vendor/crates/spikard-http/tests/common/mod.rs +26 -0
  105. data/vendor/crates/spikard-http/tests/di_integration.rs +192 -0
  106. data/vendor/crates/spikard-http/tests/doc_snippets.rs +5 -0
  107. data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +1093 -0
  108. data/vendor/crates/spikard-http/tests/multipart_behavior.rs +656 -0
  109. data/vendor/crates/spikard-http/tests/server_config_builder.rs +314 -0
  110. data/vendor/crates/spikard-http/tests/sse_behavior.rs +620 -0
  111. data/vendor/crates/spikard-http/tests/websocket_behavior.rs +663 -0
  112. data/vendor/crates/spikard-rb/Cargo.toml +48 -42
  113. data/vendor/crates/spikard-rb/build.rs +199 -8
  114. data/vendor/crates/spikard-rb/src/background.rs +63 -63
  115. data/vendor/crates/spikard-rb/src/config/mod.rs +5 -0
  116. data/vendor/crates/spikard-rb/src/{config.rs → config/server_config.rs} +285 -294
  117. data/vendor/crates/spikard-rb/src/conversion.rs +554 -453
  118. data/vendor/crates/spikard-rb/src/di/builder.rs +100 -0
  119. data/vendor/crates/spikard-rb/src/{di.rs → di/mod.rs} +375 -409
  120. data/vendor/crates/spikard-rb/src/handler.rs +618 -625
  121. data/vendor/crates/spikard-rb/src/integration/mod.rs +3 -0
  122. data/vendor/crates/spikard-rb/src/lib.rs +1810 -2771
  123. data/vendor/crates/spikard-rb/src/lifecycle.rs +275 -274
  124. data/vendor/crates/spikard-rb/src/metadata/mod.rs +5 -0
  125. data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +447 -0
  126. data/vendor/crates/spikard-rb/src/runtime/mod.rs +5 -0
  127. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +324 -0
  128. data/vendor/crates/spikard-rb/src/server.rs +308 -283
  129. data/vendor/crates/spikard-rb/src/sse.rs +231 -231
  130. data/vendor/crates/spikard-rb/src/{test_client.rs → testing/client.rs} +551 -404
  131. data/vendor/crates/spikard-rb/src/testing/mod.rs +7 -0
  132. data/vendor/crates/spikard-rb/src/{test_sse.rs → testing/sse.rs} +143 -143
  133. data/vendor/crates/spikard-rb/src/testing/websocket.rs +635 -0
  134. data/vendor/crates/spikard-rb/src/websocket.rs +374 -233
  135. metadata +46 -13
  136. data/vendor/crates/spikard-http/src/parameters.rs +0 -1
  137. data/vendor/crates/spikard-http/src/problem.rs +0 -1
  138. data/vendor/crates/spikard-http/src/router.rs +0 -1
  139. data/vendor/crates/spikard-http/src/schema_registry.rs +0 -1
  140. data/vendor/crates/spikard-http/src/type_hints.rs +0 -1
  141. data/vendor/crates/spikard-http/src/validation.rs +0 -1
  142. data/vendor/crates/spikard-rb/src/test_websocket.rs +0 -221
@@ -0,0 +1,447 @@
1
+ //! Route metadata extraction and building from Ruby objects.
2
+ //!
3
+ //! This module handles converting Ruby route definitions into structured
4
+ //! RouteMetadata that can be used by the Rust HTTP server.
5
+
6
+ use magnus::prelude::*;
7
+ use magnus::{Error, RArray, RHash, RString, Ruby, TryConvert, Value, r_hash::ForEach};
8
+ use serde_json::{Map as JsonMap, Value as JsonValue};
9
+ use spikard_http::{Route, RouteMetadata, SchemaRegistry};
10
+
11
+ /// Build route metadata from Ruby parameters
12
+ #[allow(clippy::too_many_arguments)]
13
+ pub fn build_route_metadata(
14
+ ruby: &Ruby,
15
+ method: String,
16
+ path: String,
17
+ handler_name: Option<String>,
18
+ request_schema_value: Value,
19
+ response_schema_value: Value,
20
+ parameter_schema_value: Value,
21
+ file_params_value: Value,
22
+ is_async: bool,
23
+ cors_value: Value,
24
+ body_param_name: Option<String>,
25
+ jsonrpc_method_value: Value,
26
+ handler_value: Value,
27
+ ) -> Result<Value, Error> {
28
+ let normalized_path = normalize_path_for_route(&path);
29
+ let final_handler_name = handler_name.unwrap_or_else(|| default_handler_name(&method, &normalized_path));
30
+
31
+ let json_module = ruby
32
+ .class_object()
33
+ .const_get("JSON")
34
+ .map_err(|_| Error::new(ruby.exception_runtime_error(), "JSON module not available"))?;
35
+
36
+ let request_schema = if request_schema_value.is_nil() {
37
+ None
38
+ } else {
39
+ Some(ruby_value_to_json(ruby, json_module, request_schema_value)?)
40
+ };
41
+ let response_schema = if response_schema_value.is_nil() {
42
+ None
43
+ } else {
44
+ Some(ruby_value_to_json(ruby, json_module, response_schema_value)?)
45
+ };
46
+ let parameter_schema = if parameter_schema_value.is_nil() {
47
+ None
48
+ } else {
49
+ Some(ruby_value_to_json(ruby, json_module, parameter_schema_value)?)
50
+ };
51
+ let file_params = if file_params_value.is_nil() {
52
+ None
53
+ } else {
54
+ Some(ruby_value_to_json(ruby, json_module, file_params_value)?)
55
+ };
56
+
57
+ let cors = parse_cors_config(ruby, cors_value)?;
58
+ let handler_dependencies = extract_handler_dependencies_from_ruby(ruby, handler_value)?;
59
+
60
+ #[cfg(feature = "di")]
61
+ let handler_deps_option = if handler_dependencies.is_empty() {
62
+ None
63
+ } else {
64
+ Some(handler_dependencies.clone())
65
+ };
66
+
67
+ let jsonrpc_method = if jsonrpc_method_value.is_nil() {
68
+ None
69
+ } else {
70
+ Some(ruby_value_to_json(ruby, json_module, jsonrpc_method_value)?)
71
+ };
72
+
73
+ let mut metadata = RouteMetadata {
74
+ method,
75
+ path: normalized_path,
76
+ handler_name: final_handler_name,
77
+ request_schema,
78
+ response_schema,
79
+ parameter_schema,
80
+ file_params,
81
+ is_async,
82
+ cors,
83
+ body_param_name,
84
+ #[cfg(feature = "di")]
85
+ handler_dependencies: handler_deps_option,
86
+ jsonrpc_method,
87
+ };
88
+
89
+ let registry = SchemaRegistry::new();
90
+ let route = Route::from_metadata(metadata.clone(), &registry).map_err(|err| {
91
+ Error::new(
92
+ ruby.exception_runtime_error(),
93
+ format!("Failed to build route metadata: {err}"),
94
+ )
95
+ })?;
96
+
97
+ if let Some(validator) = route.parameter_validator.as_ref() {
98
+ metadata.parameter_schema = Some(validator.schema().clone());
99
+ }
100
+
101
+ route_metadata_to_ruby(ruby, &metadata)
102
+ }
103
+
104
+ /// Convert a RouteMetadata to a Ruby hash
105
+ pub fn route_metadata_to_ruby(ruby: &Ruby, metadata: &RouteMetadata) -> Result<Value, Error> {
106
+ let hash = ruby.hash_new();
107
+
108
+ hash.aset(ruby.to_symbol("method"), ruby.str_new(&metadata.method))?;
109
+ hash.aset(ruby.to_symbol("path"), ruby.str_new(&metadata.path))?;
110
+ hash.aset(ruby.to_symbol("handler_name"), ruby.str_new(&metadata.handler_name))?;
111
+ let is_async_val: Value = if metadata.is_async {
112
+ ruby.qtrue().as_value()
113
+ } else {
114
+ ruby.qfalse().as_value()
115
+ };
116
+ hash.aset(ruby.to_symbol("is_async"), is_async_val)?;
117
+
118
+ hash.aset(
119
+ ruby.to_symbol("request_schema"),
120
+ option_json_to_ruby(ruby, &metadata.request_schema)?,
121
+ )?;
122
+ hash.aset(
123
+ ruby.to_symbol("response_schema"),
124
+ option_json_to_ruby(ruby, &metadata.response_schema)?,
125
+ )?;
126
+ hash.aset(
127
+ ruby.to_symbol("parameter_schema"),
128
+ option_json_to_ruby(ruby, &metadata.parameter_schema)?,
129
+ )?;
130
+ hash.aset(
131
+ ruby.to_symbol("file_params"),
132
+ option_json_to_ruby(ruby, &metadata.file_params)?,
133
+ )?;
134
+ hash.aset(
135
+ ruby.to_symbol("body_param_name"),
136
+ metadata
137
+ .body_param_name
138
+ .as_ref()
139
+ .map(|s| ruby.str_new(s).as_value())
140
+ .unwrap_or_else(|| ruby.qnil().as_value()),
141
+ )?;
142
+
143
+ hash.aset(ruby.to_symbol("cors"), cors_to_ruby(ruby, &metadata.cors)?)?;
144
+
145
+ #[cfg(feature = "di")]
146
+ {
147
+ if let Some(deps) = &metadata.handler_dependencies {
148
+ let array = ruby.ary_new();
149
+ for dep in deps {
150
+ array.push(ruby.str_new(dep))?;
151
+ }
152
+ hash.aset(ruby.to_symbol("handler_dependencies"), array)?;
153
+ } else {
154
+ hash.aset(ruby.to_symbol("handler_dependencies"), ruby.qnil())?;
155
+ }
156
+ }
157
+
158
+ hash.aset(
159
+ ruby.to_symbol("jsonrpc_method"),
160
+ option_json_to_ruby(ruby, &metadata.jsonrpc_method)?,
161
+ )?;
162
+
163
+ Ok(hash.as_value())
164
+ }
165
+
166
+ /// Normalize path for routes (convert :param to {param})
167
+ pub fn normalize_path_for_route(path: &str) -> String {
168
+ let has_trailing_slash = path.ends_with('/');
169
+ let segments = path.split('/').map(|segment| {
170
+ if let Some(stripped) = segment.strip_prefix(':') {
171
+ format!("{{{}}}", stripped)
172
+ } else {
173
+ segment.to_string()
174
+ }
175
+ });
176
+
177
+ let normalized = segments.collect::<Vec<_>>().join("/");
178
+ if has_trailing_slash && !normalized.ends_with('/') {
179
+ format!("{normalized}/")
180
+ } else {
181
+ normalized
182
+ }
183
+ }
184
+
185
+ /// Generate default handler name from method and path
186
+ pub fn default_handler_name(method: &str, path: &str) -> String {
187
+ let normalized_path: String = path
188
+ .chars()
189
+ .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' })
190
+ .collect();
191
+ let trimmed = normalized_path.trim_matches('_');
192
+ let final_segment = if trimmed.is_empty() { "root" } else { trimmed };
193
+ format!("{}_{}", method.to_ascii_lowercase(), final_segment)
194
+ }
195
+
196
+ /// Extract handler dependencies from a Ruby handler callable
197
+ pub fn extract_handler_dependencies_from_ruby(_ruby: &Ruby, handler_value: Value) -> Result<Vec<String>, Error> {
198
+ if handler_value.is_nil() {
199
+ return Ok(Vec::new());
200
+ }
201
+
202
+ let params_value: Value = handler_value.funcall("parameters", ())?;
203
+ let params = RArray::try_convert(params_value)?;
204
+
205
+ let mut dependencies = Vec::new();
206
+ for i in 0..params.len() {
207
+ let entry: Value = params.entry(i as isize)?;
208
+ if let Some(pair) = RArray::from_value(entry) {
209
+ if pair.len() < 2 {
210
+ continue;
211
+ }
212
+
213
+ let kind_val: Value = pair.entry(0)?;
214
+ let name_val: Value = pair.entry(1)?;
215
+
216
+ let kind_symbol: magnus::Symbol = magnus::Symbol::try_convert(kind_val)?;
217
+ let kind_name = kind_symbol.name().unwrap_or_default();
218
+
219
+ if kind_name == "key" || kind_name == "keyreq" {
220
+ if let Ok(sym) = magnus::Symbol::try_convert(name_val) {
221
+ if let Ok(name) = sym.name() {
222
+ dependencies.push(name.to_string());
223
+ }
224
+ } else {
225
+ dependencies.push(String::try_convert(name_val)?);
226
+ }
227
+ }
228
+ }
229
+ }
230
+
231
+ Ok(dependencies)
232
+ }
233
+
234
+ /// Parse CORS configuration from Ruby value
235
+ pub fn parse_cors_config(ruby: &Ruby, value: Value) -> Result<Option<spikard_http::CorsConfig>, Error> {
236
+ if value.is_nil() {
237
+ return Ok(None);
238
+ }
239
+
240
+ let hash = RHash::try_convert(value)?;
241
+ let lookup = |key: &str| -> Option<Value> {
242
+ hash.get(ruby.to_symbol(key))
243
+ .or_else(|| hash.get(ruby.str_new(key)))
244
+ };
245
+
246
+ let allowed_origins = lookup("allowed_origins")
247
+ .and_then(|v| Vec::<String>::try_convert(v).ok())
248
+ .unwrap_or_default();
249
+ let allowed_methods = lookup("allowed_methods")
250
+ .and_then(|v| Vec::<String>::try_convert(v).ok())
251
+ .unwrap_or_default();
252
+ let allowed_headers = lookup("allowed_headers")
253
+ .and_then(|v| Vec::<String>::try_convert(v).ok())
254
+ .unwrap_or_default();
255
+ let expose_headers = lookup("expose_headers")
256
+ .and_then(|v| Vec::<String>::try_convert(v).ok());
257
+ let max_age = lookup("max_age")
258
+ .and_then(|v| i64::try_convert(v).ok())
259
+ .map(|v| v as u32);
260
+ let allow_credentials = lookup("allow_credentials")
261
+ .and_then(|v| bool::try_convert(v).ok());
262
+
263
+ Ok(Some(spikard_http::CorsConfig {
264
+ allowed_origins,
265
+ allowed_methods,
266
+ allowed_headers,
267
+ expose_headers,
268
+ max_age,
269
+ allow_credentials,
270
+ }))
271
+ }
272
+
273
+ /// Convert an optional JSON value to Ruby
274
+ pub fn option_json_to_ruby(ruby: &Ruby, value: &Option<JsonValue>) -> Result<Value, Error> {
275
+ if let Some(json) = value {
276
+ json_to_ruby(ruby, json)
277
+ } else {
278
+ Ok(ruby.qnil().as_value())
279
+ }
280
+ }
281
+
282
+ /// Convert CORS config to Ruby hash
283
+ pub fn cors_to_ruby(ruby: &Ruby, cors: &Option<spikard_http::CorsConfig>) -> Result<Value, Error> {
284
+ if let Some(cors_config) = cors {
285
+ let hash = ruby.hash_new();
286
+ let origins = cors_config
287
+ .allowed_origins
288
+ .iter()
289
+ .map(|s| JsonValue::String(s.clone()))
290
+ .collect();
291
+ hash.aset(
292
+ ruby.to_symbol("allowed_origins"),
293
+ json_to_ruby(ruby, &JsonValue::Array(origins))?,
294
+ )?;
295
+ let methods = cors_config
296
+ .allowed_methods
297
+ .iter()
298
+ .map(|s| JsonValue::String(s.clone()))
299
+ .collect();
300
+ hash.aset(
301
+ ruby.to_symbol("allowed_methods"),
302
+ json_to_ruby(ruby, &JsonValue::Array(methods))?,
303
+ )?;
304
+ let headers = cors_config
305
+ .allowed_headers
306
+ .iter()
307
+ .map(|s| JsonValue::String(s.clone()))
308
+ .collect();
309
+ hash.aset(
310
+ ruby.to_symbol("allowed_headers"),
311
+ json_to_ruby(ruby, &JsonValue::Array(headers))?,
312
+ )?;
313
+ hash.aset(
314
+ ruby.to_symbol("expose_headers"),
315
+ if let Some(expose_headers) = &cors_config.expose_headers {
316
+ let expose = expose_headers.iter().map(|s| JsonValue::String(s.clone())).collect();
317
+ json_to_ruby(ruby, &JsonValue::Array(expose))?
318
+ } else {
319
+ ruby.qnil().as_value()
320
+ },
321
+ )?;
322
+ hash.aset(
323
+ ruby.to_symbol("max_age"),
324
+ if let Some(max_age) = cors_config.max_age {
325
+ ruby.integer_from_i64(max_age as i64).as_value()
326
+ } else {
327
+ ruby.qnil().as_value()
328
+ },
329
+ )?;
330
+ hash.aset(
331
+ ruby.to_symbol("allow_credentials"),
332
+ if let Some(allow_creds) = cors_config.allow_credentials {
333
+ if allow_creds {
334
+ ruby.qtrue().as_value()
335
+ } else {
336
+ ruby.qfalse().as_value()
337
+ }
338
+ } else {
339
+ ruby.qnil().as_value()
340
+ },
341
+ )?;
342
+ Ok(hash.as_value())
343
+ } else {
344
+ Ok(ruby.qnil().as_value())
345
+ }
346
+ }
347
+
348
+ /// Convert Ruby value to JSON
349
+ pub fn ruby_value_to_json(ruby: &Ruby, _json_module: Value, value: Value) -> Result<JsonValue, Error> {
350
+ if value.is_nil() {
351
+ return Ok(JsonValue::Null);
352
+ }
353
+
354
+ if value.is_kind_of(ruby.class_true_class()) {
355
+ return Ok(JsonValue::Bool(true));
356
+ }
357
+
358
+ if value.is_kind_of(ruby.class_false_class()) {
359
+ return Ok(JsonValue::Bool(false));
360
+ }
361
+
362
+ if value.is_kind_of(ruby.class_float()) {
363
+ let float_val = f64::try_convert(value)?;
364
+ if let Some(num) = serde_json::Number::from_f64(float_val) {
365
+ return Ok(JsonValue::Number(num));
366
+ }
367
+ }
368
+
369
+ if value.is_kind_of(ruby.class_integer()) {
370
+ if let Ok(int_val) = i64::try_convert(value) {
371
+ return Ok(JsonValue::Number(int_val.into()));
372
+ }
373
+ if let Ok(int_val) = u64::try_convert(value) {
374
+ return Ok(JsonValue::Number(int_val.into()));
375
+ }
376
+ }
377
+
378
+ if let Ok(str_val) = RString::try_convert(value) {
379
+ let slice = str_val.to_string()?;
380
+ return Ok(JsonValue::String(slice));
381
+ }
382
+
383
+ if let Some(array) = RArray::from_value(value) {
384
+ let mut items = Vec::with_capacity(array.len());
385
+ let slice = unsafe { array.as_slice() };
386
+ for elem in slice {
387
+ items.push(ruby_value_to_json(ruby, _json_module, *elem)?);
388
+ }
389
+ return Ok(JsonValue::Array(items));
390
+ }
391
+
392
+ if let Some(hash) = RHash::from_value(value) {
393
+ let mut map = JsonMap::new();
394
+ hash.foreach(|key: Value, val: Value| -> Result<ForEach, Error> {
395
+ let key_str: String = if let Ok(sym) = magnus::Symbol::try_convert(key) {
396
+ sym.name().map(|c| c.to_string()).unwrap_or_default()
397
+ } else {
398
+ String::try_convert(key)?
399
+ };
400
+ let json_val = ruby_value_to_json(ruby, _json_module, val)?;
401
+ map.insert(key_str, json_val);
402
+ Ok(ForEach::Continue)
403
+ })?;
404
+ return Ok(JsonValue::Object(map));
405
+ }
406
+
407
+ Err(Error::new(
408
+ ruby.exception_arg_error(),
409
+ "Unsupported Ruby value type for JSON conversion",
410
+ ))
411
+ }
412
+
413
+ /// Convert JSON to Ruby value
414
+ pub fn json_to_ruby(ruby: &Ruby, value: &JsonValue) -> Result<Value, Error> {
415
+ match value {
416
+ JsonValue::Null => Ok(ruby.qnil().as_value()),
417
+ JsonValue::Bool(b) => Ok(if *b {
418
+ ruby.qtrue().as_value()
419
+ } else {
420
+ ruby.qfalse().as_value()
421
+ }),
422
+ JsonValue::Number(num) => {
423
+ if let Some(i) = num.as_i64() {
424
+ Ok(ruby.integer_from_i64(i).as_value())
425
+ } else if let Some(f) = num.as_f64() {
426
+ Ok(ruby.float_from_f64(f).as_value())
427
+ } else {
428
+ Ok(ruby.qnil().as_value())
429
+ }
430
+ }
431
+ JsonValue::String(str_val) => Ok(ruby.str_new(str_val).as_value()),
432
+ JsonValue::Array(items) => {
433
+ let array = ruby.ary_new();
434
+ for item in items {
435
+ array.push(json_to_ruby(ruby, item)?)?;
436
+ }
437
+ Ok(array.as_value())
438
+ }
439
+ JsonValue::Object(map) => {
440
+ let hash = ruby.hash_new();
441
+ for (key, item) in map {
442
+ hash.aset(ruby.str_new(key), json_to_ruby(ruby, item)?)?;
443
+ }
444
+ Ok(hash.as_value())
445
+ }
446
+ }
447
+ }
@@ -0,0 +1,5 @@
1
+ //! HTTP server runtime initialization and management.
2
+
3
+ pub mod server_runner;
4
+
5
+ pub use server_runner::{normalize_route_metadata, run_server};