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
@@ -7,6 +7,7 @@
7
7
  #![allow(dead_code)]
8
8
 
9
9
  use axum::http::Method;
10
+ use axum::Router;
10
11
  use axum_test::{TestServer, TestServerConfig, Transport};
11
12
  use bytes::Bytes;
12
13
  use cookie::Cookie;
@@ -14,16 +15,22 @@ use magnus::prelude::*;
14
15
  use magnus::{Error, RHash, Ruby, Value, gc::Marker};
15
16
  use serde_json::Value as JsonValue;
16
17
  use spikard_http::testing::{
17
- MultipartFilePart, SnapshotError, build_multipart_body, encode_urlencoded_body, snapshot_response,
18
+ MultipartFilePart,
19
+ ResponseSnapshot,
20
+ SnapshotError,
21
+ build_multipart_body,
22
+ encode_urlencoded_body,
23
+ snapshot_response,
18
24
  };
19
25
  use spikard_http::{Route, RouteMetadata};
20
26
  use std::cell::RefCell;
21
27
  use std::collections::HashMap;
22
28
  use std::sync::Arc;
29
+ use std::time::Duration;
30
+ use url::Url;
23
31
 
24
32
  use crate::conversion::{parse_request_config, response_to_ruby};
25
33
  use crate::handler::RubyHandler;
26
- use crate::server::GLOBAL_RUNTIME;
27
34
 
28
35
  /// Request configuration built from Ruby options hash.
29
36
  pub struct RequestConfig {
@@ -55,6 +62,12 @@ pub struct TestResponseData {
55
62
  #[derive(Debug)]
56
63
  pub struct NativeRequestError(pub String);
57
64
 
65
+ #[derive(Debug)]
66
+ enum WebSocketConnectError {
67
+ Timeout,
68
+ Other(String),
69
+ }
70
+
58
71
  /// Inner client state containing the test servers and handlers.
59
72
  pub struct ClientInner {
60
73
  pub http_server: Arc<TestServer>,
@@ -95,6 +108,7 @@ impl NativeTestClient {
95
108
  ws_handlers: Value,
96
109
  sse_producers: Value,
97
110
  ) -> Result<(), Error> {
111
+ trace_step("initialize:start");
98
112
  let metadata: Vec<RouteMetadata> = serde_json::from_str(&routes_json)
99
113
  .map_err(|err| Error::new(ruby.exception_arg_error(), format!("Invalid routes JSON: {err}")))?;
100
114
 
@@ -128,6 +142,7 @@ impl NativeTestClient {
128
142
  route_metadata_vec.push(meta);
129
143
  }
130
144
 
145
+ trace_step("initialize:build_router");
131
146
  let mut router = spikard_http::server::build_router_with_handlers_and_config(
132
147
  prepared_routes,
133
148
  server_config,
@@ -137,6 +152,7 @@ impl NativeTestClient {
137
152
 
138
153
  let mut ws_endpoints = Vec::new();
139
154
  if !ws_handlers.is_nil() {
155
+ trace_step("initialize:ws_handlers");
140
156
  let ws_hash = RHash::from_value(ws_handlers)
141
157
  .ok_or_else(|| Error::new(ruby.exception_arg_error(), "WebSocket handlers must be a Hash"))?;
142
158
 
@@ -160,6 +176,7 @@ impl NativeTestClient {
160
176
 
161
177
  let mut sse_endpoints = Vec::new();
162
178
  if !sse_producers.is_nil() {
179
+ trace_step("initialize:sse_producers");
163
180
  let sse_hash = RHash::from_value(sse_producers)
164
181
  .ok_or_else(|| Error::new(ruby.exception_arg_error(), "SSE producers must be a Hash"))?;
165
182
 
@@ -196,28 +213,19 @@ impl NativeTestClient {
196
213
  );
197
214
  }
198
215
 
199
- let http_server = GLOBAL_RUNTIME
200
- .block_on(async { TestServer::new(router.clone()) })
201
- .map_err(|err| {
202
- Error::new(
203
- ruby.exception_runtime_error(),
204
- format!("Failed to initialise test server: {err}"),
205
- )
206
- })?;
216
+ trace_step("initialize:test_server_http");
217
+ let timeout_duration = test_server_timeout();
218
+ let http_server = init_test_server(router.clone(), timeout_duration, "test server", ruby)?;
207
219
 
220
+ trace_step("initialize:test_server_ws");
208
221
  let ws_config = TestServerConfig {
209
222
  transport: Some(Transport::HttpRandomPort),
210
223
  ..Default::default()
211
224
  };
212
- let transport_server = GLOBAL_RUNTIME
213
- .block_on(async { TestServer::new_with_config(router, ws_config) })
214
- .map_err(|err| {
215
- Error::new(
216
- ruby.exception_runtime_error(),
217
- format!("Failed to initialise WebSocket transport server: {err}"),
218
- )
219
- })?;
225
+ let transport_server =
226
+ init_test_server_with_config(router, ws_config, timeout_duration, "WebSocket transport server", ruby)?;
220
227
 
228
+ trace_step("initialize:done");
221
229
  *this.inner.borrow_mut() = Some(ClientInner {
222
230
  http_server: Arc::new(http_server),
223
231
  transport_server: Arc::new(transport_server),
@@ -251,13 +259,21 @@ impl NativeTestClient {
251
259
 
252
260
  let request_config = parse_request_config(ruby, options)?;
253
261
 
254
- let response = GLOBAL_RUNTIME
255
- .block_on(execute_request(
256
- inner.http_server.clone(),
257
- http_method,
258
- path.clone(),
259
- request_config,
260
- ))
262
+ let runtime = crate::server::global_runtime(ruby)?;
263
+ let server = inner.http_server.clone();
264
+ let path_value = path.clone();
265
+ let request_config_value = request_config;
266
+ let response = crate::call_without_gvl!(
267
+ block_on_request,
268
+ args: (
269
+ runtime, &tokio::runtime::Runtime,
270
+ server, Arc<TestServer>,
271
+ http_method, Method,
272
+ path_value, String,
273
+ request_config_value, RequestConfig
274
+ ),
275
+ return_type: Result<TestResponseData, NativeRequestError>
276
+ )
261
277
  .map_err(|err| {
262
278
  Error::new(
263
279
  ruby.exception_runtime_error(),
@@ -285,16 +301,31 @@ impl NativeTestClient {
285
301
 
286
302
  drop(inner_borrow);
287
303
 
288
- let handle =
289
- GLOBAL_RUNTIME.spawn(async move { spikard_http::testing::connect_websocket(&server, &path).await });
290
-
291
- let ws = GLOBAL_RUNTIME.block_on(async {
292
- handle
293
- .await
294
- .map_err(|e| Error::new(ruby.exception_runtime_error(), format!("WebSocket task failed: {}", e)))
304
+ let timeout_duration = websocket_timeout();
305
+ let ws = crate::call_without_gvl!(
306
+ block_on_websocket_connect,
307
+ args: (
308
+ server, Arc<TestServer>,
309
+ path, String,
310
+ timeout_duration, Duration
311
+ ),
312
+ return_type: Result<crate::testing::websocket::WebSocketConnection, WebSocketConnectError>
313
+ )
314
+ .map_err(|err| match err {
315
+ WebSocketConnectError::Timeout => Error::new(
316
+ ruby.exception_runtime_error(),
317
+ format!(
318
+ "WebSocket connect timed out after {}ms",
319
+ timeout_duration.as_millis()
320
+ ),
321
+ ),
322
+ WebSocketConnectError::Other(message) => Error::new(
323
+ ruby.exception_runtime_error(),
324
+ format!("WebSocket connect failed: {}", message),
325
+ ),
295
326
  })?;
296
327
 
297
- let ws_conn = crate::test_websocket::WebSocketTestConnection::new(ws);
328
+ let ws_conn = crate::testing::websocket::WebSocketTestConnection::new(ws);
298
329
  Ok(ruby.obj_wrap(ws_conn).as_value())
299
330
  }
300
331
 
@@ -305,14 +336,41 @@ impl NativeTestClient {
305
336
  .as_ref()
306
337
  .ok_or_else(|| Error::new(ruby.exception_runtime_error(), "TestClient not initialised"))?;
307
338
 
308
- let response = GLOBAL_RUNTIME
309
- .block_on(async {
310
- let axum_response = inner.transport_server.get(&path).await;
311
- snapshot_response(axum_response).await
312
- })
313
- .map_err(|e| Error::new(ruby.exception_runtime_error(), format!("SSE request failed: {}", e)))?;
339
+ let runtime = crate::server::global_runtime(ruby)?;
340
+ let server = inner.http_server.clone();
341
+ let http_method = Method::GET;
342
+ let request_config = RequestConfig {
343
+ query: None,
344
+ headers: HashMap::new(),
345
+ cookies: HashMap::new(),
346
+ body: None,
347
+ };
348
+ let response = crate::call_without_gvl!(
349
+ block_on_request,
350
+ args: (
351
+ runtime, &tokio::runtime::Runtime,
352
+ server, Arc<TestServer>,
353
+ http_method, Method,
354
+ path, String,
355
+ request_config, RequestConfig
356
+ ),
357
+ return_type: Result<TestResponseData, NativeRequestError>
358
+ )
359
+ .map_err(|err| {
360
+ Error::new(
361
+ ruby.exception_runtime_error(),
362
+ format!("SSE request failed: {}", err.0),
363
+ )
364
+ })?;
314
365
 
315
- crate::test_sse::sse_stream_from_response(ruby, &response)
366
+ let body = response.body_text.unwrap_or_default().into_bytes();
367
+ let snapshot = ResponseSnapshot {
368
+ status: response.status,
369
+ headers: response.headers,
370
+ body,
371
+ };
372
+
373
+ crate::testing::sse::sse_stream_from_response(ruby, &snapshot)
316
374
  }
317
375
 
318
376
  /// GC mark hook so Ruby keeps handler closures alive.
@@ -327,6 +385,95 @@ impl NativeTestClient {
327
385
  }
328
386
  }
329
387
 
388
+ fn websocket_timeout() -> Duration {
389
+ const DEFAULT_TIMEOUT_MS: u64 = 30_000;
390
+ let timeout_ms = std::env::var("SPIKARD_RB_WS_TIMEOUT_MS")
391
+ .ok()
392
+ .and_then(|value| value.parse::<u64>().ok())
393
+ .unwrap_or(DEFAULT_TIMEOUT_MS);
394
+ Duration::from_millis(timeout_ms)
395
+ }
396
+
397
+ fn block_on_request(
398
+ runtime: &tokio::runtime::Runtime,
399
+ server: Arc<TestServer>,
400
+ method: Method,
401
+ path: String,
402
+ config: RequestConfig,
403
+ ) -> Result<TestResponseData, NativeRequestError> {
404
+ runtime.block_on(execute_request(server, method, path, config))
405
+ }
406
+
407
+ fn block_on_websocket_connect(
408
+ server: Arc<TestServer>,
409
+ path: String,
410
+ timeout_duration: Duration,
411
+ ) -> Result<crate::testing::websocket::WebSocketConnection, WebSocketConnectError> {
412
+ let url = server
413
+ .server_url(&path)
414
+ .map_err(|err| WebSocketConnectError::Other(err.to_string()))?;
415
+ let ws_url = to_ws_url(url)?;
416
+
417
+ match crate::testing::websocket::WebSocketConnection::connect(ws_url, timeout_duration) {
418
+ Ok(ws) => Ok(ws),
419
+ Err(crate::testing::websocket::WebSocketIoError::Timeout) => Err(WebSocketConnectError::Timeout),
420
+ Err(err) => Err(WebSocketConnectError::Other(format!("{:?}", err))),
421
+ }
422
+ }
423
+
424
+ fn to_ws_url(mut url: Url) -> Result<Url, WebSocketConnectError> {
425
+ let scheme = match url.scheme() {
426
+ "https" => "wss",
427
+ _ => "ws",
428
+ };
429
+ url.set_scheme(scheme)
430
+ .map_err(|_| WebSocketConnectError::Other("Failed to set WebSocket scheme".to_string()))?;
431
+ Ok(url)
432
+ }
433
+
434
+ fn test_server_timeout() -> Duration {
435
+ const DEFAULT_TIMEOUT_MS: u64 = 30_000;
436
+ let timeout_ms = std::env::var("SPIKARD_RB_TESTSERVER_TIMEOUT_MS")
437
+ .ok()
438
+ .and_then(|value| value.parse::<u64>().ok())
439
+ .unwrap_or(DEFAULT_TIMEOUT_MS);
440
+ Duration::from_millis(timeout_ms)
441
+ }
442
+
443
+ fn trace_step(message: &str) {
444
+ if std::env::var("SPIKARD_RB_TEST_TRACE").ok().as_deref() == Some("1") {
445
+ eprintln!("[spikard-rb-test] {}", message);
446
+ }
447
+ }
448
+
449
+ fn init_test_server(router: Router, _timeout: Duration, label: &str, ruby: &Ruby) -> Result<TestServer, Error> {
450
+ let runtime = crate::server::global_runtime(ruby)?;
451
+ let _guard = runtime.enter();
452
+ TestServer::new(router).map_err(|err| {
453
+ Error::new(
454
+ ruby.exception_runtime_error(),
455
+ format!("Failed to initialise {label}: {err}"),
456
+ )
457
+ })
458
+ }
459
+
460
+ fn init_test_server_with_config(
461
+ router: Router,
462
+ config: TestServerConfig,
463
+ _timeout: Duration,
464
+ label: &str,
465
+ ruby: &Ruby,
466
+ ) -> Result<TestServer, Error> {
467
+ let runtime = crate::server::global_runtime(ruby)?;
468
+ let _guard = runtime.enter();
469
+ TestServer::new_with_config(router, config).map_err(|err| {
470
+ Error::new(
471
+ ruby.exception_runtime_error(),
472
+ format!("Failed to initialise {label}: {err}"),
473
+ )
474
+ })
475
+ }
476
+
330
477
  /// Execute an HTTP request against a test server.
331
478
  ///
332
479
  /// Handles method routing, query params, headers, cookies, and various body formats.
@@ -0,0 +1,7 @@
1
+ //! Testing utilities for Ruby bindings.
2
+ //!
3
+ //! This module provides testing client implementations for HTTP, WebSocket, and SSE testing.
4
+
5
+ pub mod client;
6
+ pub mod sse;
7
+ pub mod websocket;