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
@@ -100,6 +100,7 @@ mod tests {
100
100
  let request_data = RequestData {
101
101
  path_params: Arc::new(path_params),
102
102
  query_params: json!({"page": 1}),
103
+ validated_params: None,
103
104
  raw_query_params: Arc::new(HashMap::new()),
104
105
  body: json!({"test": "data"}),
105
106
  raw_body: None,
@@ -131,6 +132,7 @@ mod tests {
131
132
  let request_data = RequestData {
132
133
  path_params: Arc::new(HashMap::new()),
133
134
  query_params: Value::Null,
135
+ validated_params: None,
134
136
  raw_query_params: Arc::new(HashMap::new()),
135
137
  body: Value::Null,
136
138
  raw_body: None,
@@ -169,6 +171,7 @@ mod tests {
169
171
  let request_data = RequestData {
170
172
  path_params: Arc::new(HashMap::new()),
171
173
  query_params: json!({"api_key": "secret123"}),
174
+ validated_params: None,
172
175
  raw_query_params: Arc::new(raw_query_params.clone()),
173
176
  body: Value::Null,
174
177
  raw_body: None,
@@ -192,6 +195,7 @@ mod tests {
192
195
  let request_data_no_param = RequestData {
193
196
  path_params: Arc::new(HashMap::new()),
194
197
  query_params: Value::Null,
198
+ validated_params: None,
195
199
  raw_query_params: Arc::new(HashMap::new()),
196
200
  body: Value::Null,
197
201
  raw_body: None,
@@ -229,6 +233,7 @@ mod tests {
229
233
  let request_data = RequestData {
230
234
  path_params: Arc::new(path_params),
231
235
  query_params: json!({"filter": "active"}),
236
+ validated_params: None,
232
237
  raw_query_params: Arc::new(raw_query_params),
233
238
  body: json!({"name": "test"}),
234
239
  raw_body: None,
@@ -254,6 +259,7 @@ mod tests {
254
259
  let request_data = RequestData {
255
260
  path_params: Arc::new(HashMap::new()),
256
261
  query_params: Value::Null,
262
+ validated_params: None,
257
263
  raw_query_params: Arc::new(HashMap::new()),
258
264
  body: Value::Null,
259
265
  raw_body: None,
@@ -1,3 +1,5 @@
1
+ #![allow(clippy::pedantic, clippy::nursery)]
2
+ #![cfg_attr(test, allow(clippy::all))]
1
3
  //! Spikard HTTP Server
2
4
  //!
3
5
  //! Pure Rust HTTP server with language-agnostic handler trait.
@@ -13,20 +15,15 @@ pub mod debug;
13
15
  pub mod di_handler;
14
16
  pub mod handler_response;
15
17
  pub mod handler_trait;
18
+ pub mod jsonrpc;
16
19
  pub mod lifecycle;
17
20
  pub mod middleware;
18
21
  pub mod openapi;
19
- pub mod parameters;
20
- pub mod problem;
21
22
  pub mod query_parser;
22
23
  pub mod response;
23
- pub mod router;
24
- pub mod schema_registry;
25
24
  pub mod server;
26
25
  pub mod sse;
27
26
  pub mod testing;
28
- pub mod type_hints;
29
- pub mod validation;
30
27
  pub mod websocket;
31
28
 
32
29
  use serde::{Deserialize, Serialize};
@@ -44,20 +41,22 @@ pub use body_metadata::ResponseBodySize;
44
41
  pub use di_handler::DependencyInjectingHandler;
45
42
  pub use handler_response::HandlerResponse;
46
43
  pub use handler_trait::{Handler, HandlerResult, RequestData, ValidatedParams};
44
+ pub use jsonrpc::JsonRpcConfig;
47
45
  pub use lifecycle::{HookResult, LifecycleHook, LifecycleHooks, LifecycleHooksBuilder, request_hook, response_hook};
48
46
  pub use openapi::{ContactInfo, LicenseInfo, OpenApiConfig, SecuritySchemeInfo, ServerInfo};
49
- pub use parameters::ParameterValidator;
50
- pub use problem::{CONTENT_TYPE_PROBLEM_JSON, ProblemDetails};
51
47
  pub use response::Response;
52
- pub use router::{Route, RouteHandler, Router};
53
- pub use schema_registry::SchemaRegistry;
54
48
  pub use server::Server;
55
- pub use spikard_core::{CompressionConfig, CorsConfig, Method, RateLimitConfig, RouteMetadata};
49
+ pub use spikard_core::{
50
+ CompressionConfig, CorsConfig, Method, ParameterValidator, ProblemDetails, RateLimitConfig, Route, RouteHandler,
51
+ RouteMetadata, Router, SchemaRegistry, SchemaValidator,
52
+ };
56
53
  pub use sse::{SseEvent, SseEventProducer, SseState, sse_handler};
57
54
  pub use testing::{ResponseSnapshot, SnapshotError, snapshot_response};
58
- pub use validation::SchemaValidator;
59
55
  pub use websocket::{WebSocketHandler, WebSocketState, websocket_handler};
60
56
 
57
+ /// Reexport from spikard_core for convenience
58
+ pub use spikard_core::problem::CONTENT_TYPE_PROBLEM_JSON;
59
+
61
60
  /// JWT authentication configuration
62
61
  #[derive(Debug, Clone, Serialize, Deserialize)]
63
62
  pub struct JwtConfig {
@@ -139,10 +138,14 @@ pub struct ServerConfig {
139
138
  pub shutdown_timeout: u64,
140
139
  /// OpenAPI documentation configuration
141
140
  pub openapi: Option<crate::openapi::OpenApiConfig>,
141
+ /// JSON-RPC configuration
142
+ pub jsonrpc: Option<crate::jsonrpc::JsonRpcConfig>,
142
143
  /// Lifecycle hooks for request/response processing
143
144
  pub lifecycle_hooks: Option<std::sync::Arc<LifecycleHooks>>,
144
145
  /// Background task executor configuration
145
146
  pub background_tasks: BackgroundTaskConfig,
147
+ /// Enable per-request HTTP tracing (tower-http `TraceLayer`)
148
+ pub enable_http_trace: bool,
146
149
  /// Dependency injection container (requires 'di' feature)
147
150
  #[cfg(feature = "di")]
148
151
  pub di_container: Option<std::sync::Arc<spikard_core::di::DependencyContainer>>,
@@ -154,10 +157,10 @@ impl Default for ServerConfig {
154
157
  host: "127.0.0.1".to_string(),
155
158
  port: 8000,
156
159
  workers: 1,
157
- enable_request_id: true,
160
+ enable_request_id: false,
158
161
  max_body_size: Some(10 * 1024 * 1024),
159
- request_timeout: Some(30),
160
- compression: Some(CompressionConfig::default()),
162
+ request_timeout: None,
163
+ compression: None,
161
164
  rate_limit: None,
162
165
  jwt_auth: None,
163
166
  api_key_auth: None,
@@ -165,8 +168,10 @@ impl Default for ServerConfig {
165
168
  graceful_shutdown: true,
166
169
  shutdown_timeout: 30,
167
170
  openapi: None,
171
+ jsonrpc: None,
168
172
  lifecycle_hooks: None,
169
173
  background_tasks: BackgroundTaskConfig::default(),
174
+ enable_http_trace: false,
170
175
  #[cfg(feature = "di")]
171
176
  di_container: None,
172
177
  }
@@ -256,6 +261,12 @@ impl ServerConfigBuilder {
256
261
  self
257
262
  }
258
263
 
264
+ /// Enable or disable per-request HTTP tracing (tower-http `TraceLayer`)
265
+ pub fn enable_http_trace(mut self, enable: bool) -> Self {
266
+ self.config.enable_http_trace = enable;
267
+ self
268
+ }
269
+
259
270
  /// Set maximum request body size in bytes (None = unlimited, not recommended)
260
271
  pub fn max_body_size(mut self, size: Option<usize>) -> Self {
261
272
  self.config.max_body_size = size;
@@ -322,6 +333,12 @@ impl ServerConfigBuilder {
322
333
  self
323
334
  }
324
335
 
336
+ /// Set JSON-RPC configuration
337
+ pub fn jsonrpc(mut self, jsonrpc: Option<crate::jsonrpc::JsonRpcConfig>) -> Self {
338
+ self.config.jsonrpc = jsonrpc;
339
+ self
340
+ }
341
+
325
342
  /// Set lifecycle hooks for request/response processing
326
343
  pub fn lifecycle_hooks(mut self, hooks: Option<std::sync::Arc<LifecycleHooks>>) -> Self {
327
344
  self.config.lifecycle_hooks = hooks;
@@ -360,24 +377,16 @@ impl ServerConfigBuilder {
360
377
 
361
378
  let key_str = key.into();
362
379
 
363
- // Get or create DI container (mutable)
364
380
  let container = if let Some(container) = self.config.di_container.take() {
365
- // Try to get mutable access - this will only work if we're the only owner
366
- Arc::try_unwrap(container).unwrap_or_else(|_arc| {
367
- // If we can't unwrap, we lose existing dependencies
368
- // This is a fallback that shouldn't happen in normal builder usage (linear chaining)
369
- DependencyContainer::new()
370
- })
381
+ Arc::try_unwrap(container).unwrap_or_else(|_arc| DependencyContainer::new())
371
382
  } else {
372
383
  DependencyContainer::new()
373
384
  };
374
385
 
375
386
  let mut container = container;
376
387
 
377
- // Create ValueDependency
378
388
  let dep = ValueDependency::new(key_str.clone(), value);
379
389
 
380
- // Register (panic on error for builder pattern)
381
390
  container
382
391
  .register(key_str, Arc::new(dep))
383
392
  .expect("Failed to register dependency");
@@ -433,7 +442,6 @@ impl ServerConfigBuilder {
433
442
 
434
443
  let key_str = key.into();
435
444
 
436
- // Get or create DI container (mutable)
437
445
  let container = if let Some(container) = self.config.di_container.take() {
438
446
  Arc::try_unwrap(container).unwrap_or_else(|_| DependencyContainer::new())
439
447
  } else {
@@ -442,10 +450,8 @@ impl ServerConfigBuilder {
442
450
 
443
451
  let mut container = container;
444
452
 
445
- // Clone factory for the closure
446
453
  let factory_clone = factory.clone();
447
454
 
448
- // Create FactoryDependency using builder
449
455
  let dep = FactoryDependency::builder(key_str.clone())
450
456
  .factory(
451
457
  move |_req: &axum::http::Request<()>,
@@ -501,7 +507,6 @@ impl ServerConfigBuilder {
501
507
 
502
508
  let key = dependency.key().to_string();
503
509
 
504
- // Get or create DI container (mutable)
505
510
  let container = if let Some(container) = self.config.di_container.take() {
506
511
  Arc::try_unwrap(container).unwrap_or_else(|_| DependencyContainer::new())
507
512
  } else {
@@ -115,6 +115,11 @@ impl HookRegistry {
115
115
  #[cfg(test)]
116
116
  mod tests {
117
117
  use super::*;
118
+ use crate::lifecycle::HookResult;
119
+ use axum::body::Body;
120
+ use axum::http::{Request, Response, StatusCode};
121
+ use std::future::Future;
122
+ use std::pin::Pin;
118
123
 
119
124
  #[test]
120
125
  fn test_error_messages() {
@@ -146,4 +151,80 @@ mod tests {
146
151
  let deser_err = error::deserialize_failed("response", "malformed");
147
152
  assert!(deser_err.contains("response"));
148
153
  }
154
+
155
+ #[tokio::test]
156
+ async fn serial_extract_body_roundtrips_bytes() {
157
+ let body = Body::from("hello");
158
+ let bytes = serial::extract_body(body).await.expect("extract body");
159
+ assert_eq!(&bytes[..], b"hello");
160
+ }
161
+
162
+ #[test]
163
+ fn serial_parse_json_handles_empty_valid_and_invalid_json() {
164
+ let empty = serial::parse_json(&[]).expect("parse empty");
165
+ assert_eq!(empty, serde_json::Value::Null);
166
+
167
+ let valid = serial::parse_json(br#"{"ok":true}"#).expect("parse json");
168
+ assert_eq!(valid["ok"], true);
169
+
170
+ let invalid = serial::parse_json(b"not-json").expect("parse fallback");
171
+ assert_eq!(invalid, serde_json::Value::String("not-json".to_string()));
172
+ }
173
+
174
+ #[test]
175
+ fn hook_registry_registers_all_hooks_via_callback() {
176
+ struct NoopHook {
177
+ hook_name: String,
178
+ }
179
+
180
+ impl LifecycleHook<Request<Body>, Response<Body>> for NoopHook {
181
+ fn name(&self) -> &str {
182
+ &self.hook_name
183
+ }
184
+
185
+ fn execute_request<'a>(
186
+ &'a self,
187
+ req: Request<Body>,
188
+ ) -> Pin<Box<dyn Future<Output = Result<HookResult<Request<Body>, Response<Body>>, String>> + Send + 'a>>
189
+ {
190
+ Box::pin(async move { Ok(HookResult::Continue(req)) })
191
+ }
192
+
193
+ fn execute_response<'a>(
194
+ &'a self,
195
+ resp: Response<Body>,
196
+ ) -> Pin<Box<dyn Future<Output = Result<HookResult<Response<Body>, Response<Body>>, String>> + Send + 'a>>
197
+ {
198
+ Box::pin(async move { Ok(HookResult::Continue(resp)) })
199
+ }
200
+ }
201
+
202
+ let mut hooks = HttpLifecycleHooks::new();
203
+ assert!(hooks.is_empty());
204
+
205
+ let hook_list: Vec<Arc<dyn LifecycleHook<Request<Body>, Response<Body>>>> = vec![
206
+ Arc::new(NoopHook {
207
+ hook_name: "one".to_string(),
208
+ }),
209
+ Arc::new(NoopHook {
210
+ hook_name: "two".to_string(),
211
+ }),
212
+ ];
213
+
214
+ HookRegistry::register_from_list(&mut hooks, hook_list, "on_request", |hooks, hook| {
215
+ hooks.add_on_request(hook);
216
+ });
217
+
218
+ let dbg = format!("{:?}", hooks);
219
+ assert!(dbg.contains("on_request_count"));
220
+ assert!(dbg.contains("2"));
221
+
222
+ let req = Request::builder().body(Body::empty()).unwrap();
223
+ let result = futures::executor::block_on(hooks.execute_on_request(req)).expect("hook run");
224
+ assert!(matches!(result, HookResult::Continue(_)));
225
+
226
+ let resp = Response::builder().status(StatusCode::OK).body(Body::empty()).unwrap();
227
+ let resp = futures::executor::block_on(hooks.execute_on_response(resp)).expect("hook run");
228
+ assert_eq!(resp.status(), StatusCode::OK);
229
+ }
149
230
  }