spikard 0.4.0-arm64-darwin-23

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