spikard 0.3.6 → 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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +21 -6
  3. data/ext/spikard_rb/Cargo.toml +2 -2
  4. data/lib/spikard/app.rb +33 -14
  5. data/lib/spikard/testing.rb +47 -12
  6. data/lib/spikard/version.rb +1 -1
  7. data/vendor/crates/spikard-bindings-shared/Cargo.toml +63 -0
  8. data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +132 -0
  9. data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +752 -0
  10. data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +194 -0
  11. data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +246 -0
  12. data/vendor/crates/spikard-bindings-shared/src/error_response.rs +401 -0
  13. data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +238 -0
  14. data/vendor/crates/spikard-bindings-shared/src/lib.rs +24 -0
  15. data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +292 -0
  16. data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +616 -0
  17. data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +305 -0
  18. data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +248 -0
  19. data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +351 -0
  20. data/vendor/crates/spikard-bindings-shared/tests/comprehensive_coverage.rs +454 -0
  21. data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +383 -0
  22. data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +280 -0
  23. data/vendor/crates/spikard-core/Cargo.toml +4 -4
  24. data/vendor/crates/spikard-core/src/debug.rs +64 -0
  25. data/vendor/crates/spikard-core/src/di/container.rs +3 -27
  26. data/vendor/crates/spikard-core/src/di/factory.rs +1 -5
  27. data/vendor/crates/spikard-core/src/di/graph.rs +8 -47
  28. data/vendor/crates/spikard-core/src/di/mod.rs +1 -1
  29. data/vendor/crates/spikard-core/src/di/resolved.rs +1 -7
  30. data/vendor/crates/spikard-core/src/di/value.rs +2 -4
  31. data/vendor/crates/spikard-core/src/errors.rs +30 -0
  32. data/vendor/crates/spikard-core/src/http.rs +262 -0
  33. data/vendor/crates/spikard-core/src/lib.rs +1 -1
  34. data/vendor/crates/spikard-core/src/lifecycle.rs +764 -0
  35. data/vendor/crates/spikard-core/src/metadata.rs +389 -0
  36. data/vendor/crates/spikard-core/src/parameters.rs +1962 -159
  37. data/vendor/crates/spikard-core/src/problem.rs +34 -0
  38. data/vendor/crates/spikard-core/src/request_data.rs +966 -1
  39. data/vendor/crates/spikard-core/src/router.rs +263 -2
  40. data/vendor/crates/spikard-core/src/validation/error_mapper.rs +688 -0
  41. data/vendor/crates/spikard-core/src/{validation.rs → validation/mod.rs} +26 -268
  42. data/vendor/crates/spikard-http/Cargo.toml +12 -16
  43. data/vendor/crates/spikard-http/examples/sse-notifications.rs +148 -0
  44. data/vendor/crates/spikard-http/examples/websocket-chat.rs +92 -0
  45. data/vendor/crates/spikard-http/src/auth.rs +65 -16
  46. data/vendor/crates/spikard-http/src/background.rs +1614 -3
  47. data/vendor/crates/spikard-http/src/cors.rs +515 -0
  48. data/vendor/crates/spikard-http/src/debug.rs +65 -0
  49. data/vendor/crates/spikard-http/src/di_handler.rs +1322 -77
  50. data/vendor/crates/spikard-http/src/handler_response.rs +711 -0
  51. data/vendor/crates/spikard-http/src/handler_trait.rs +607 -5
  52. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +6 -0
  53. data/vendor/crates/spikard-http/src/lib.rs +33 -28
  54. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +81 -0
  55. data/vendor/crates/spikard-http/src/lifecycle.rs +765 -0
  56. data/vendor/crates/spikard-http/src/middleware/mod.rs +372 -117
  57. data/vendor/crates/spikard-http/src/middleware/multipart.rs +836 -10
  58. data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +409 -43
  59. data/vendor/crates/spikard-http/src/middleware/validation.rs +513 -65
  60. data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +345 -0
  61. data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +1055 -0
  62. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +473 -3
  63. data/vendor/crates/spikard-http/src/query_parser.rs +455 -31
  64. data/vendor/crates/spikard-http/src/response.rs +321 -0
  65. data/vendor/crates/spikard-http/src/server/handler.rs +1572 -9
  66. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +136 -0
  67. data/vendor/crates/spikard-http/src/server/mod.rs +875 -178
  68. data/vendor/crates/spikard-http/src/server/request_extraction.rs +674 -23
  69. data/vendor/crates/spikard-http/src/server/routing_factory.rs +599 -0
  70. data/vendor/crates/spikard-http/src/sse.rs +983 -21
  71. data/vendor/crates/spikard-http/src/testing/form.rs +38 -0
  72. data/vendor/crates/spikard-http/src/testing/test_client.rs +0 -2
  73. data/vendor/crates/spikard-http/src/testing.rs +7 -7
  74. data/vendor/crates/spikard-http/src/websocket.rs +1055 -4
  75. data/vendor/crates/spikard-http/tests/background_behavior.rs +832 -0
  76. data/vendor/crates/spikard-http/tests/common/handlers.rs +309 -0
  77. data/vendor/crates/spikard-http/tests/common/mod.rs +26 -0
  78. data/vendor/crates/spikard-http/tests/di_integration.rs +192 -0
  79. data/vendor/crates/spikard-http/tests/doc_snippets.rs +5 -0
  80. data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +1093 -0
  81. data/vendor/crates/spikard-http/tests/multipart_behavior.rs +656 -0
  82. data/vendor/crates/spikard-http/tests/server_config_builder.rs +314 -0
  83. data/vendor/crates/spikard-http/tests/sse_behavior.rs +620 -0
  84. data/vendor/crates/spikard-http/tests/websocket_behavior.rs +663 -0
  85. data/vendor/crates/spikard-rb/Cargo.toml +10 -4
  86. data/vendor/crates/spikard-rb/build.rs +196 -5
  87. data/vendor/crates/spikard-rb/src/config/mod.rs +5 -0
  88. data/vendor/crates/spikard-rb/src/{config.rs → config/server_config.rs} +100 -109
  89. data/vendor/crates/spikard-rb/src/conversion.rs +121 -20
  90. data/vendor/crates/spikard-rb/src/di/builder.rs +100 -0
  91. data/vendor/crates/spikard-rb/src/{di.rs → di/mod.rs} +12 -46
  92. data/vendor/crates/spikard-rb/src/handler.rs +100 -107
  93. data/vendor/crates/spikard-rb/src/integration/mod.rs +3 -0
  94. data/vendor/crates/spikard-rb/src/lib.rs +467 -1428
  95. data/vendor/crates/spikard-rb/src/lifecycle.rs +1 -0
  96. data/vendor/crates/spikard-rb/src/metadata/mod.rs +5 -0
  97. data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +447 -0
  98. data/vendor/crates/spikard-rb/src/runtime/mod.rs +5 -0
  99. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +324 -0
  100. data/vendor/crates/spikard-rb/src/server.rs +47 -22
  101. data/vendor/crates/spikard-rb/src/{test_client.rs → testing/client.rs} +187 -40
  102. data/vendor/crates/spikard-rb/src/testing/mod.rs +7 -0
  103. data/vendor/crates/spikard-rb/src/testing/websocket.rs +635 -0
  104. data/vendor/crates/spikard-rb/src/websocket.rs +178 -37
  105. metadata +46 -13
  106. data/vendor/crates/spikard-http/src/parameters.rs +0 -1
  107. data/vendor/crates/spikard-http/src/problem.rs +0 -1
  108. data/vendor/crates/spikard-http/src/router.rs +0 -1
  109. data/vendor/crates/spikard-http/src/schema_registry.rs +0 -1
  110. data/vendor/crates/spikard-http/src/type_hints.rs +0 -1
  111. data/vendor/crates/spikard-http/src/validation.rs +0 -1
  112. data/vendor/crates/spikard-rb/src/test_websocket.rs +0 -221
  113. /data/vendor/crates/spikard-rb/src/{test_sse.rs → testing/sse.rs} +0 -0
@@ -33,6 +33,7 @@ impl RubyLifecycleHook {
33
33
  }
34
34
 
35
35
  /// Mark Ruby values for GC
36
+ #[allow(dead_code)]
36
37
  pub fn mark(&self, marker: &Marker) {
37
38
  if let Ok(ruby) = magnus::Ruby::get() {
38
39
  marker.mark(self.func.get_inner_with(&ruby));
@@ -0,0 +1,5 @@
1
+ //! Route metadata extraction and building from Ruby definitions.
2
+
3
+ pub mod route_extraction;
4
+
5
+ pub use route_extraction::{build_route_metadata, json_to_ruby, ruby_value_to_json};
@@ -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};