spikard 0.8.3 → 0.10.1

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 (106) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +19 -10
  3. data/ext/spikard_rb/Cargo.lock +234 -162
  4. data/ext/spikard_rb/Cargo.toml +3 -3
  5. data/ext/spikard_rb/extconf.rb +4 -3
  6. data/lib/spikard/config.rb +88 -12
  7. data/lib/spikard/testing.rb +3 -1
  8. data/lib/spikard/version.rb +1 -1
  9. data/lib/spikard.rb +11 -0
  10. data/vendor/crates/spikard-bindings-shared/Cargo.toml +3 -6
  11. data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +8 -8
  12. data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +2 -2
  13. data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +4 -4
  14. data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +10 -4
  15. data/vendor/crates/spikard-bindings-shared/src/error_response.rs +3 -3
  16. data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +10 -5
  17. data/vendor/crates/spikard-bindings-shared/src/json_conversion.rs +829 -0
  18. data/vendor/crates/spikard-bindings-shared/src/lazy_cache.rs +587 -0
  19. data/vendor/crates/spikard-bindings-shared/src/lib.rs +7 -0
  20. data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +11 -11
  21. data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +9 -37
  22. data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +436 -3
  23. data/vendor/crates/spikard-bindings-shared/src/response_interpreter.rs +944 -0
  24. data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +4 -4
  25. data/vendor/crates/spikard-bindings-shared/tests/config_extractor_behavior.rs +3 -2
  26. data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +13 -13
  27. data/vendor/crates/spikard-bindings-shared/tests/{comprehensive_coverage.rs → full_coverage.rs} +10 -5
  28. data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +14 -14
  29. data/vendor/crates/spikard-bindings-shared/tests/integration_tests.rs +669 -0
  30. data/vendor/crates/spikard-core/Cargo.toml +3 -3
  31. data/vendor/crates/spikard-core/src/di/container.rs +1 -1
  32. data/vendor/crates/spikard-core/src/di/factory.rs +2 -2
  33. data/vendor/crates/spikard-core/src/di/resolved.rs +2 -2
  34. data/vendor/crates/spikard-core/src/di/value.rs +1 -1
  35. data/vendor/crates/spikard-core/src/http.rs +75 -0
  36. data/vendor/crates/spikard-core/src/lifecycle.rs +43 -43
  37. data/vendor/crates/spikard-core/src/parameters.rs +14 -19
  38. data/vendor/crates/spikard-core/src/problem.rs +1 -1
  39. data/vendor/crates/spikard-core/src/request_data.rs +7 -16
  40. data/vendor/crates/spikard-core/src/router.rs +6 -0
  41. data/vendor/crates/spikard-core/src/schema_registry.rs +2 -3
  42. data/vendor/crates/spikard-core/src/type_hints.rs +3 -2
  43. data/vendor/crates/spikard-core/src/validation/error_mapper.rs +1 -1
  44. data/vendor/crates/spikard-core/src/validation/mod.rs +1 -1
  45. data/vendor/crates/spikard-core/tests/di_dependency_defaults.rs +1 -1
  46. data/vendor/crates/spikard-core/tests/error_mapper.rs +2 -2
  47. data/vendor/crates/spikard-core/tests/parameters_edge_cases.rs +1 -1
  48. data/vendor/crates/spikard-core/tests/parameters_full.rs +1 -1
  49. data/vendor/crates/spikard-core/tests/parameters_schema_and_formats.rs +1 -1
  50. data/vendor/crates/spikard-core/tests/validation_coverage.rs +4 -4
  51. data/vendor/crates/spikard-http/Cargo.toml +4 -2
  52. data/vendor/crates/spikard-http/src/cors.rs +32 -11
  53. data/vendor/crates/spikard-http/src/di_handler.rs +12 -8
  54. data/vendor/crates/spikard-http/src/grpc/framing.rs +469 -0
  55. data/vendor/crates/spikard-http/src/grpc/handler.rs +887 -25
  56. data/vendor/crates/spikard-http/src/grpc/mod.rs +114 -22
  57. data/vendor/crates/spikard-http/src/grpc/service.rs +232 -2
  58. data/vendor/crates/spikard-http/src/grpc/streaming.rs +80 -2
  59. data/vendor/crates/spikard-http/src/handler_trait.rs +204 -27
  60. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +15 -15
  61. data/vendor/crates/spikard-http/src/jsonrpc/http_handler.rs +2 -2
  62. data/vendor/crates/spikard-http/src/jsonrpc/router.rs +2 -2
  63. data/vendor/crates/spikard-http/src/lib.rs +1 -1
  64. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +2 -2
  65. data/vendor/crates/spikard-http/src/lifecycle.rs +4 -4
  66. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +2 -0
  67. data/vendor/crates/spikard-http/src/server/fast_router.rs +186 -0
  68. data/vendor/crates/spikard-http/src/server/grpc_routing.rs +324 -23
  69. data/vendor/crates/spikard-http/src/server/handler.rs +33 -22
  70. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +21 -2
  71. data/vendor/crates/spikard-http/src/server/mod.rs +125 -20
  72. data/vendor/crates/spikard-http/src/server/request_extraction.rs +126 -44
  73. data/vendor/crates/spikard-http/src/server/routing_factory.rs +80 -69
  74. data/vendor/crates/spikard-http/tests/common/handlers.rs +2 -2
  75. data/vendor/crates/spikard-http/tests/common/test_builders.rs +12 -12
  76. data/vendor/crates/spikard-http/tests/di_handler_error_responses.rs +2 -2
  77. data/vendor/crates/spikard-http/tests/di_integration.rs +6 -6
  78. data/vendor/crates/spikard-http/tests/grpc_bidirectional_streaming.rs +430 -0
  79. data/vendor/crates/spikard-http/tests/grpc_client_streaming.rs +738 -0
  80. data/vendor/crates/spikard-http/tests/grpc_integration_test.rs +13 -9
  81. data/vendor/crates/spikard-http/tests/grpc_server_streaming.rs +974 -0
  82. data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +2 -2
  83. data/vendor/crates/spikard-http/tests/request_extraction_full.rs +4 -4
  84. data/vendor/crates/spikard-http/tests/server_config_builder.rs +2 -2
  85. data/vendor/crates/spikard-http/tests/server_cors_preflight.rs +1 -0
  86. data/vendor/crates/spikard-http/tests/server_openapi_jsonrpc_static.rs +140 -0
  87. data/vendor/crates/spikard-rb/Cargo.toml +3 -1
  88. data/vendor/crates/spikard-rb/src/conversion.rs +138 -4
  89. data/vendor/crates/spikard-rb/src/grpc/handler.rs +706 -229
  90. data/vendor/crates/spikard-rb/src/grpc/mod.rs +6 -2
  91. data/vendor/crates/spikard-rb/src/gvl.rs +2 -2
  92. data/vendor/crates/spikard-rb/src/handler.rs +169 -91
  93. data/vendor/crates/spikard-rb/src/lib.rs +444 -62
  94. data/vendor/crates/spikard-rb/src/lifecycle.rs +29 -1
  95. data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +108 -43
  96. data/vendor/crates/spikard-rb/src/request.rs +117 -20
  97. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +52 -25
  98. data/vendor/crates/spikard-rb/src/server.rs +23 -14
  99. data/vendor/crates/spikard-rb/src/testing/client.rs +5 -4
  100. data/vendor/crates/spikard-rb/src/testing/sse.rs +1 -36
  101. data/vendor/crates/spikard-rb/src/testing/websocket.rs +3 -38
  102. data/vendor/crates/spikard-rb/src/websocket.rs +32 -23
  103. data/vendor/crates/spikard-rb-macros/Cargo.toml +1 -1
  104. metadata +14 -4
  105. data/vendor/bundle/ruby/3.4.0/gems/diff-lcs-1.6.2/mise.toml +0 -5
  106. data/vendor/bundle/ruby/3.4.0/gems/rake-compiler-dock-1.10.0/build/buildkitd.toml +0 -2
@@ -5,14 +5,52 @@
5
5
 
6
6
  use axum::{
7
7
  body::Body,
8
- http::{Request, Response, StatusCode},
8
+ http::{HeaderValue, Request, Response, StatusCode, header::HeaderName},
9
9
  };
10
+ use bytes::Bytes;
10
11
  use serde::{Deserialize, Serialize};
11
12
  use serde_json::Value;
12
13
  use std::collections::HashMap;
13
14
  use std::future::Future;
14
15
  use std::pin::Pin;
15
16
 
17
+ /// Pre-built response for routes that always return the same content.
18
+ ///
19
+ /// When a route is registered with a `StaticResponse`, the server bypasses
20
+ /// handler execution, validation, lifecycle hooks, and request extraction —
21
+ /// returning the cached response directly. This is ideal for health-check
22
+ /// endpoints or any route whose output never changes at runtime.
23
+ #[derive(Clone, Debug)]
24
+ pub struct StaticResponse {
25
+ pub status: u16,
26
+ pub headers: Vec<(HeaderName, HeaderValue)>,
27
+ pub body: Bytes,
28
+ pub content_type: HeaderValue,
29
+ }
30
+
31
+ impl StaticResponse {
32
+ /// Build an `axum::response::Response` from this static response.
33
+ ///
34
+ /// This is the single canonical path for constructing HTTP responses from
35
+ /// static data — used by the FastRouter middleware, the Axum fallback
36
+ /// handler, and the `StaticResponseHandler::call` fallback.
37
+ pub fn to_response(&self) -> Response<Body> {
38
+ let status = StatusCode::from_u16(self.status).unwrap_or(StatusCode::OK);
39
+ let mut builder = Response::builder()
40
+ .status(status)
41
+ .header(axum::http::header::CONTENT_TYPE, self.content_type.clone());
42
+ for (name, value) in &self.headers {
43
+ builder = builder.header(name.clone(), value.clone());
44
+ }
45
+ builder.body(Body::from(self.body.clone())).unwrap_or_else(|_| {
46
+ Response::builder()
47
+ .status(StatusCode::INTERNAL_SERVER_ERROR)
48
+ .body(Body::from("Failed to build static response"))
49
+ .expect("fallback response must build")
50
+ })
51
+ }
52
+ }
53
+
16
54
  /// Request data extracted from HTTP request
17
55
  /// This is the language-agnostic representation passed to handlers
18
56
  ///
@@ -25,11 +63,11 @@ use std::pin::Pin;
25
63
  #[derive(Debug, Clone)]
26
64
  pub struct RequestData {
27
65
  pub path_params: std::sync::Arc<HashMap<String, String>>,
28
- pub query_params: Value,
66
+ pub query_params: std::sync::Arc<Value>,
29
67
  /// Validated parameters produced by ParameterValidator (query/path/header/cookie combined).
30
- pub validated_params: Option<Value>,
68
+ pub validated_params: Option<std::sync::Arc<Value>>,
31
69
  pub raw_query_params: std::sync::Arc<HashMap<String, Vec<String>>>,
32
- pub body: Value,
70
+ pub body: std::sync::Arc<Value>,
33
71
  pub raw_body: Option<bytes::Bytes>,
34
72
  pub headers: std::sync::Arc<HashMap<String, String>>,
35
73
  pub cookies: std::sync::Arc<HashMap<String, String>>,
@@ -53,10 +91,10 @@ impl Serialize for RequestData {
53
91
 
54
92
  let mut state = serializer.serialize_struct("RequestData", field_count)?;
55
93
  state.serialize_field("path_params", &*self.path_params)?;
56
- state.serialize_field("query_params", &self.query_params)?;
57
- state.serialize_field("validated_params", &self.validated_params)?;
94
+ state.serialize_field("query_params", &*self.query_params)?;
95
+ state.serialize_field("validated_params", &self.validated_params.as_deref())?;
58
96
  state.serialize_field("raw_query_params", &*self.raw_query_params)?;
59
- state.serialize_field("body", &self.body)?;
97
+ state.serialize_field("body", &*self.body)?;
60
98
  state.serialize_field("raw_body", &self.raw_body.as_ref().map(|b| b.as_ref()))?;
61
99
  state.serialize_field("headers", &*self.headers)?;
62
100
  state.serialize_field("cookies", &*self.cookies)?;
@@ -124,16 +162,16 @@ impl<'de> Deserialize<'de> for RequestData {
124
162
  path_params = Some(std::sync::Arc::new(map.next_value()?));
125
163
  }
126
164
  Field::QueryParams => {
127
- query_params = Some(map.next_value()?);
165
+ query_params = Some(std::sync::Arc::new(map.next_value()?));
128
166
  }
129
167
  Field::RawQueryParams => {
130
168
  raw_query_params = Some(std::sync::Arc::new(map.next_value()?));
131
169
  }
132
170
  Field::ValidatedParams => {
133
- validated_params = Some(map.next_value()?);
171
+ validated_params = Some(std::sync::Arc::new(map.next_value()?));
134
172
  }
135
173
  Field::Body => {
136
- body = Some(map.next_value()?);
174
+ body = Some(std::sync::Arc::new(map.next_value()?));
137
175
  }
138
176
  Field::RawBody => {
139
177
  let bytes_vec: Option<Vec<u8>> = map.next_value()?;
@@ -269,6 +307,69 @@ pub trait Handler: Send + Sync {
269
307
  fn wants_request_extensions(&self) -> bool {
270
308
  false
271
309
  }
310
+
311
+ /// Return a pre-built static response if this handler always produces the
312
+ /// same output. When `Some`, the server bypasses the full middleware
313
+ /// pipeline and serves the pre-built response directly.
314
+ fn static_response(&self) -> Option<StaticResponse> {
315
+ None
316
+ }
317
+ }
318
+
319
+ /// A no-op handler that declares a static response.
320
+ ///
321
+ /// Language bindings create this handler when a route is registered with
322
+ /// `static_response` configuration. The handler's `call()` method is never
323
+ /// invoked — the server uses the `static_response()` return value instead.
324
+ pub struct StaticResponseHandler {
325
+ response: StaticResponse,
326
+ }
327
+
328
+ impl StaticResponseHandler {
329
+ /// Create a new static response handler.
330
+ pub fn new(response: StaticResponse) -> Self {
331
+ Self { response }
332
+ }
333
+
334
+ /// Build a `StaticResponse` from common parameters.
335
+ ///
336
+ /// Convenience constructor for language bindings that pass status, body,
337
+ /// content-type, and optional extra headers.
338
+ pub fn from_parts(
339
+ status: u16,
340
+ body: impl Into<Bytes>,
341
+ content_type: Option<&str>,
342
+ extra_headers: Vec<(HeaderName, HeaderValue)>,
343
+ ) -> Self {
344
+ let ct = content_type
345
+ .and_then(|s| HeaderValue::from_str(s).ok())
346
+ .unwrap_or_else(|| HeaderValue::from_static("text/plain; charset=utf-8"));
347
+ Self {
348
+ response: StaticResponse {
349
+ status,
350
+ headers: extra_headers,
351
+ body: body.into(),
352
+ content_type: ct,
353
+ },
354
+ }
355
+ }
356
+ }
357
+
358
+ impl Handler for StaticResponseHandler {
359
+ fn call(
360
+ &self,
361
+ _request: Request<Body>,
362
+ _request_data: RequestData,
363
+ ) -> Pin<Box<dyn Future<Output = HandlerResult> + Send + '_>> {
364
+ // This should never be called — the server fast-path intercepts first.
365
+ // Provide a working fallback just in case.
366
+ let resp = self.response.to_response();
367
+ Box::pin(async move { Ok(resp) })
368
+ }
369
+
370
+ fn static_response(&self) -> Option<StaticResponse> {
371
+ Some(self.response.clone())
372
+ }
272
373
  }
273
374
 
274
375
  /// Validated parameters from request (path, query, headers, cookies)
@@ -285,10 +386,10 @@ mod tests {
285
386
  fn minimal_request_data() -> RequestData {
286
387
  RequestData {
287
388
  path_params: std::sync::Arc::new(HashMap::new()),
288
- query_params: Value::Object(serde_json::Map::new()),
389
+ query_params: std::sync::Arc::new(Value::Object(serde_json::Map::new())),
289
390
  validated_params: None,
290
391
  raw_query_params: std::sync::Arc::new(HashMap::new()),
291
- body: Value::Null,
392
+ body: std::sync::Arc::new(Value::Null),
292
393
  raw_body: None,
293
394
  headers: std::sync::Arc::new(HashMap::new()),
294
395
  cookies: std::sync::Arc::new(HashMap::new()),
@@ -341,7 +442,7 @@ mod tests {
341
442
  });
342
443
 
343
444
  let data = RequestData {
344
- query_params,
445
+ query_params: std::sync::Arc::new(query_params),
345
446
  ..minimal_request_data()
346
447
  };
347
448
 
@@ -416,7 +517,7 @@ mod tests {
416
517
  });
417
518
 
418
519
  let data = RequestData {
419
- body,
520
+ body: std::sync::Arc::new(body),
420
521
  ..minimal_request_data()
421
522
  };
422
523
 
@@ -481,7 +582,7 @@ mod tests {
481
582
  });
482
583
 
483
584
  let data = RequestData {
484
- body,
585
+ body: std::sync::Arc::new(body),
485
586
  ..minimal_request_data()
486
587
  };
487
588
 
@@ -512,10 +613,10 @@ mod tests {
512
613
 
513
614
  let data = RequestData {
514
615
  path_params: std::sync::Arc::new(path_params),
515
- query_params: serde_json::json!({"page": 1}),
616
+ query_params: std::sync::Arc::new(serde_json::json!({"page": 1})),
516
617
  validated_params: None,
517
618
  raw_query_params: std::sync::Arc::new(raw_query_params),
518
- body,
619
+ body: std::sync::Arc::new(body),
519
620
  raw_body: Some(raw_body),
520
621
  headers: std::sync::Arc::new(headers),
521
622
  cookies: std::sync::Arc::new(cookies),
@@ -642,14 +743,14 @@ mod tests {
642
743
  map.insert("id".to_string(), "999".to_string());
643
744
  map
644
745
  }),
645
- query_params: serde_json::json!({"limit": 50, "offset": 10}),
746
+ query_params: std::sync::Arc::new(serde_json::json!({"limit": 50, "offset": 10})),
646
747
  validated_params: None,
647
748
  raw_query_params: std::sync::Arc::new({
648
749
  let mut map = HashMap::new();
649
750
  map.insert("sort".to_string(), vec!["name".to_string(), "date".to_string()]);
650
751
  map
651
752
  }),
652
- body: serde_json::json!({"title": "New Post", "content": "Hello World"}),
753
+ body: std::sync::Arc::new(serde_json::json!({"title": "New Post", "content": "Hello World"})),
653
754
  raw_body: None,
654
755
  headers: std::sync::Arc::new({
655
756
  let mut map = HashMap::new();
@@ -689,7 +790,7 @@ mod tests {
689
790
  }
690
791
 
691
792
  let data = RequestData {
692
- body: Value::Object(large_object),
793
+ body: std::sync::Arc::new(Value::Object(large_object)),
693
794
  ..minimal_request_data()
694
795
  };
695
796
 
@@ -704,10 +805,10 @@ mod tests {
704
805
  fn test_request_data_empty_collections() {
705
806
  let data = RequestData {
706
807
  path_params: std::sync::Arc::new(HashMap::new()),
707
- query_params: Value::Object(serde_json::Map::new()),
808
+ query_params: std::sync::Arc::new(Value::Object(serde_json::Map::new())),
708
809
  validated_params: None,
709
810
  raw_query_params: std::sync::Arc::new(HashMap::new()),
710
- body: Value::Object(serde_json::Map::new()),
811
+ body: std::sync::Arc::new(Value::Object(serde_json::Map::new())),
711
812
  raw_body: None,
712
813
  headers: std::sync::Arc::new(HashMap::new()),
713
814
  cookies: std::sync::Arc::new(HashMap::new()),
@@ -738,7 +839,7 @@ mod tests {
738
839
  method: "POST".to_string(),
739
840
  path: "/api/v1/users\\test".to_string(),
740
841
  headers: std::sync::Arc::new(headers),
741
- body: serde_json::json!({"note": "Contains\nnewline"}),
842
+ body: std::sync::Arc::new(serde_json::json!({"note": "Contains\nnewline"})),
742
843
  ..minimal_request_data()
743
844
  };
744
845
 
@@ -779,7 +880,7 @@ mod tests {
779
880
  #[test]
780
881
  fn test_request_data_serialization_null_body() {
781
882
  let data = RequestData {
782
- body: Value::Null,
883
+ body: std::sync::Arc::new(Value::Null),
783
884
  ..minimal_request_data()
784
885
  };
785
886
 
@@ -791,7 +892,7 @@ mod tests {
791
892
  #[test]
792
893
  fn test_request_data_serialization_array_body() {
793
894
  let data = RequestData {
794
- body: serde_json::json!([1, 2, 3, "four", {"five": 5}]),
895
+ body: std::sync::Arc::new(serde_json::json!([1, 2, 3, "four", {"five": 5}])),
795
896
  ..minimal_request_data()
796
897
  };
797
898
 
@@ -807,12 +908,12 @@ mod tests {
807
908
  #[test]
808
909
  fn test_request_data_serialization_numeric_edge_cases() {
809
910
  let data = RequestData {
810
- body: serde_json::json!({
911
+ body: std::sync::Arc::new(serde_json::json!({
811
912
  "zero": 0,
812
913
  "negative": -42,
813
914
  "large": 9223372036854775807i64,
814
915
  "float": 3.14159
815
- }),
916
+ })),
816
917
  ..minimal_request_data()
817
918
  };
818
919
 
@@ -835,4 +936,80 @@ mod tests {
835
936
  assert_eq!(validated.params.get("id").unwrap(), &Value::String("123".to_string()));
836
937
  assert_eq!(validated.params.get("active").unwrap(), &Value::Bool(true));
837
938
  }
939
+
940
+ #[test]
941
+ fn test_static_response_handler_new() {
942
+ let sr = StaticResponse {
943
+ status: 200,
944
+ headers: vec![],
945
+ body: Bytes::from("OK"),
946
+ content_type: HeaderValue::from_static("text/plain"),
947
+ };
948
+ let handler = StaticResponseHandler::new(sr);
949
+ let resp = handler.static_response();
950
+ assert!(resp.is_some());
951
+ let resp = resp.unwrap();
952
+ assert_eq!(resp.status, 200);
953
+ assert_eq!(resp.body.as_ref(), b"OK");
954
+ }
955
+
956
+ #[test]
957
+ fn test_static_response_handler_from_parts_defaults() {
958
+ let handler = StaticResponseHandler::from_parts(204, "No Content", None, vec![]);
959
+ let resp = handler.static_response().unwrap();
960
+ assert_eq!(resp.status, 204);
961
+ assert_eq!(resp.body.as_ref(), b"No Content");
962
+ assert_eq!(resp.content_type, "text/plain; charset=utf-8");
963
+ }
964
+
965
+ #[test]
966
+ fn test_static_response_handler_from_parts_custom_content_type() {
967
+ let handler = StaticResponseHandler::from_parts(200, r#"{"ok":true}"#, Some("application/json"), vec![]);
968
+ let resp = handler.static_response().unwrap();
969
+ assert_eq!(resp.content_type, "application/json");
970
+ }
971
+
972
+ #[test]
973
+ fn test_static_response_handler_from_parts_extra_headers() {
974
+ let handler = StaticResponseHandler::from_parts(
975
+ 200,
976
+ "OK",
977
+ None,
978
+ vec![(HeaderName::from_static("x-custom"), HeaderValue::from_static("value"))],
979
+ );
980
+ let resp = handler.static_response().unwrap();
981
+ assert_eq!(resp.headers.len(), 1);
982
+ assert_eq!(resp.headers[0].0, "x-custom");
983
+ assert_eq!(resp.headers[0].1, "value");
984
+ }
985
+
986
+ #[tokio::test]
987
+ async fn test_static_response_handler_call_fallback() {
988
+ use http_body_util::BodyExt;
989
+
990
+ let handler = StaticResponseHandler::from_parts(201, "created", Some("text/plain"), vec![]);
991
+ let request = Request::builder().body(Body::empty()).unwrap();
992
+ let result = handler.call(request, minimal_request_data()).await;
993
+ assert!(result.is_ok());
994
+ let response = result.unwrap();
995
+ assert_eq!(response.status(), StatusCode::CREATED);
996
+ assert_eq!(response.headers().get("content-type").unwrap(), "text/plain");
997
+ let body = response.into_body().collect().await.unwrap().to_bytes();
998
+ assert_eq!(body.as_ref(), b"created");
999
+ }
1000
+
1001
+ #[test]
1002
+ fn test_default_handler_static_response_is_none() {
1003
+ struct DummyHandler;
1004
+ impl Handler for DummyHandler {
1005
+ fn call(
1006
+ &self,
1007
+ _: Request<Body>,
1008
+ _: RequestData,
1009
+ ) -> Pin<Box<dyn Future<Output = HandlerResult> + Send + '_>> {
1010
+ Box::pin(async { Err((StatusCode::OK, String::new())) })
1011
+ }
1012
+ }
1013
+ assert!(DummyHandler.static_response().is_none());
1014
+ }
838
1015
  }
@@ -99,10 +99,10 @@ mod tests {
99
99
 
100
100
  let request_data = RequestData {
101
101
  path_params: Arc::new(path_params),
102
- query_params: json!({"page": 1}),
102
+ query_params: Arc::new(json!({"page": 1})),
103
103
  validated_params: None,
104
104
  raw_query_params: Arc::new(HashMap::new()),
105
- body: json!({"test": "data"}),
105
+ body: Arc::new(json!({"test": "data"})),
106
106
  raw_body: None,
107
107
  headers: Arc::new(headers),
108
108
  cookies: Arc::new(HashMap::new()),
@@ -131,10 +131,10 @@ mod tests {
131
131
 
132
132
  let request_data = RequestData {
133
133
  path_params: Arc::new(HashMap::new()),
134
- query_params: Value::Null,
134
+ query_params: Arc::new(Value::Null),
135
135
  validated_params: None,
136
136
  raw_query_params: Arc::new(HashMap::new()),
137
- body: Value::Null,
137
+ body: Arc::new(Value::Null),
138
138
  raw_body: None,
139
139
  headers: Arc::new(HashMap::new()),
140
140
  cookies: Arc::new(HashMap::new()),
@@ -170,10 +170,10 @@ mod tests {
170
170
 
171
171
  let request_data = RequestData {
172
172
  path_params: Arc::new(HashMap::new()),
173
- query_params: json!({"api_key": "secret123"}),
173
+ query_params: Arc::new(json!({"api_key": "secret123"})),
174
174
  validated_params: None,
175
175
  raw_query_params: Arc::new(raw_query_params.clone()),
176
- body: Value::Null,
176
+ body: Arc::new(Value::Null),
177
177
  raw_body: None,
178
178
  headers: Arc::new(HashMap::new()),
179
179
  cookies: Arc::new(HashMap::new()),
@@ -194,10 +194,10 @@ mod tests {
194
194
 
195
195
  let request_data_no_param = RequestData {
196
196
  path_params: Arc::new(HashMap::new()),
197
- query_params: Value::Null,
197
+ query_params: Arc::new(Value::Null),
198
198
  validated_params: None,
199
199
  raw_query_params: Arc::new(HashMap::new()),
200
- body: Value::Null,
200
+ body: Arc::new(Value::Null),
201
201
  raw_body: None,
202
202
  headers: Arc::new(HashMap::new()),
203
203
  cookies: Arc::new(HashMap::new()),
@@ -232,10 +232,10 @@ mod tests {
232
232
 
233
233
  let request_data = RequestData {
234
234
  path_params: Arc::new(path_params),
235
- query_params: json!({"filter": "active"}),
235
+ query_params: Arc::new(json!({"filter": "active"})),
236
236
  validated_params: None,
237
237
  raw_query_params: Arc::new(raw_query_params),
238
- body: json!({"name": "test"}),
238
+ body: Arc::new(json!({"name": "test"})),
239
239
  raw_body: None,
240
240
  headers: Arc::new(HashMap::new()),
241
241
  cookies: Arc::new(HashMap::new()),
@@ -251,17 +251,17 @@ mod tests {
251
251
  assert_eq!(deserialized.method, "PUT");
252
252
  assert_eq!(deserialized.path, "/users/42");
253
253
  assert_eq!(deserialized.path_params.get("user_id").unwrap(), "42");
254
- assert_eq!(deserialized.body, json!({"name": "test"}));
254
+ assert_eq!(*deserialized.body, json!({"name": "test"}));
255
255
  }
256
256
 
257
257
  #[test]
258
258
  fn test_request_data_default_values() {
259
259
  let request_data = RequestData {
260
260
  path_params: Arc::new(HashMap::new()),
261
- query_params: Value::Null,
261
+ query_params: Arc::new(Value::Null),
262
262
  validated_params: None,
263
263
  raw_query_params: Arc::new(HashMap::new()),
264
- body: Value::Null,
264
+ body: Arc::new(Value::Null),
265
265
  raw_body: None,
266
266
  headers: Arc::new(HashMap::new()),
267
267
  cookies: Arc::new(HashMap::new()),
@@ -278,8 +278,8 @@ mod tests {
278
278
  assert!(request_data.raw_query_params.is_empty());
279
279
  assert!(request_data.headers.is_empty());
280
280
  assert!(request_data.cookies.is_empty());
281
- assert_eq!(request_data.body, Value::Null);
282
- assert_eq!(request_data.query_params, Value::Null);
281
+ assert_eq!(*request_data.body, Value::Null);
282
+ assert_eq!(*request_data.query_params, Value::Null);
283
283
  }
284
284
 
285
285
  #[test]
@@ -159,10 +159,10 @@ pub async fn handle_jsonrpc(
159
159
  fn create_jsonrpc_request_data(headers: &HeaderMap, uri: &axum::http::Uri) -> RequestData {
160
160
  RequestData {
161
161
  path_params: Arc::new(HashMap::new()),
162
- query_params: serde_json::json!({}),
162
+ query_params: Arc::new(serde_json::json!({})),
163
163
  validated_params: None,
164
164
  raw_query_params: Arc::new(HashMap::new()),
165
- body: serde_json::json!({}),
165
+ body: Arc::new(serde_json::json!({})),
166
166
  raw_body: None,
167
167
  headers: Arc::new(extract_headers(headers)),
168
168
  cookies: Arc::new(HashMap::new()),
@@ -348,10 +348,10 @@ mod tests {
348
348
  fn create_test_request_data() -> RequestData {
349
349
  RequestData {
350
350
  path_params: Arc::new(HashMap::new()),
351
- query_params: Value::Object(serde_json::Map::new()),
351
+ query_params: Arc::new(Value::Object(serde_json::Map::new())),
352
352
  validated_params: None,
353
353
  raw_query_params: Arc::new(HashMap::new()),
354
- body: Value::Null,
354
+ body: Arc::new(Value::Null),
355
355
  raw_body: None,
356
356
  headers: Arc::new(HashMap::new()),
357
357
  cookies: Arc::new(HashMap::new()),
@@ -45,7 +45,7 @@ pub use grpc::{
45
45
  StreamingRequest, StreamingResponse,
46
46
  };
47
47
  pub use handler_response::HandlerResponse;
48
- pub use handler_trait::{Handler, HandlerResult, RequestData, ValidatedParams};
48
+ pub use handler_trait::{Handler, HandlerResult, RequestData, StaticResponse, StaticResponseHandler, ValidatedParams};
49
49
  pub use jsonrpc::JsonRpcConfig;
50
50
  pub use lifecycle::{HookResult, LifecycleHook, LifecycleHooks, LifecycleHooksBuilder, request_hook, response_hook};
51
51
  pub use openapi::{ContactInfo, LicenseInfo, OpenApiConfig, SecuritySchemeInfo, ServerInfo};
@@ -183,7 +183,7 @@ mod tests {
183
183
  }
184
184
 
185
185
  fn execute_request<'a>(
186
- &'a self,
186
+ &self,
187
187
  req: Request<Body>,
188
188
  ) -> Pin<Box<dyn Future<Output = Result<HookResult<Request<Body>, Response<Body>>, String>> + Send + 'a>>
189
189
  {
@@ -191,7 +191,7 @@ mod tests {
191
191
  }
192
192
 
193
193
  fn execute_response<'a>(
194
- &'a self,
194
+ &self,
195
195
  resp: Response<Body>,
196
196
  ) -> Pin<Box<dyn Future<Output = Result<HookResult<Response<Body>, Response<Body>>, String>> + Send + 'a>>
197
197
  {
@@ -70,7 +70,7 @@ mod tests {
70
70
  }
71
71
 
72
72
  fn execute_request<'a>(
73
- &'a self,
73
+ &self,
74
74
  req: Request<Body>,
75
75
  ) -> Pin<Box<dyn Future<Output = Result<HookResult<Request<Body>, Response<Body>>, String>> + Send + 'a>>
76
76
  {
@@ -78,7 +78,7 @@ mod tests {
78
78
  }
79
79
 
80
80
  fn execute_response<'a>(
81
- &'a self,
81
+ &self,
82
82
  resp: Response<Body>,
83
83
  ) -> Pin<Box<dyn Future<Output = Result<HookResult<Response<Body>, Response<Body>>, String>> + Send + 'a>>
84
84
  {
@@ -97,7 +97,7 @@ mod tests {
97
97
  }
98
98
 
99
99
  fn execute_request<'a>(
100
- &'a self,
100
+ &self,
101
101
  _req: Request<Body>,
102
102
  ) -> Pin<Box<dyn Future<Output = Result<HookResult<Request<Body>, Response<Body>>, String>> + Send + 'a>>
103
103
  {
@@ -111,7 +111,7 @@ mod tests {
111
111
  }
112
112
 
113
113
  fn execute_response<'a>(
114
- &'a self,
114
+ &self,
115
115
  _resp: Response<Body>,
116
116
  ) -> Pin<Box<dyn Future<Output = Result<HookResult<Response<Body>, Response<Body>>, String>> + Send + 'a>>
117
117
  {
@@ -188,6 +188,7 @@ mod tests {
188
188
  #[cfg(feature = "di")]
189
189
  handler_dependencies: None,
190
190
  jsonrpc_method: None,
191
+ static_response: None,
191
192
  }
192
193
  }
193
194
 
@@ -654,6 +655,7 @@ mod tests {
654
655
  #[cfg(feature = "di")]
655
656
  handler_dependencies: None,
656
657
  jsonrpc_method: None,
658
+ static_response: None,
657
659
  };
658
660
 
659
661
  let result = route_to_operation(&route);