spikard 0.8.2 → 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 (115) 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 +11 -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 +63 -25
  13. data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +20 -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 +25 -22
  16. data/vendor/crates/spikard-bindings-shared/src/grpc_metadata.rs +14 -12
  17. data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +24 -10
  18. data/vendor/crates/spikard-bindings-shared/src/json_conversion.rs +829 -0
  19. data/vendor/crates/spikard-bindings-shared/src/lazy_cache.rs +587 -0
  20. data/vendor/crates/spikard-bindings-shared/src/lib.rs +7 -0
  21. data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +17 -11
  22. data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +51 -73
  23. data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +442 -4
  24. data/vendor/crates/spikard-bindings-shared/src/response_interpreter.rs +944 -0
  25. data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +22 -10
  26. data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +28 -10
  27. data/vendor/crates/spikard-bindings-shared/tests/config_extractor_behavior.rs +3 -2
  28. data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +13 -13
  29. data/vendor/crates/spikard-bindings-shared/tests/{comprehensive_coverage.rs → full_coverage.rs} +10 -5
  30. data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +14 -14
  31. data/vendor/crates/spikard-bindings-shared/tests/integration_tests.rs +669 -0
  32. data/vendor/crates/spikard-core/Cargo.toml +11 -3
  33. data/vendor/crates/spikard-core/src/bindings/response.rs +6 -9
  34. data/vendor/crates/spikard-core/src/debug.rs +2 -2
  35. data/vendor/crates/spikard-core/src/di/container.rs +2 -2
  36. data/vendor/crates/spikard-core/src/di/error.rs +1 -1
  37. data/vendor/crates/spikard-core/src/di/factory.rs +9 -5
  38. data/vendor/crates/spikard-core/src/di/graph.rs +1 -0
  39. data/vendor/crates/spikard-core/src/di/resolved.rs +25 -2
  40. data/vendor/crates/spikard-core/src/di/value.rs +2 -1
  41. data/vendor/crates/spikard-core/src/errors.rs +3 -0
  42. data/vendor/crates/spikard-core/src/http.rs +94 -18
  43. data/vendor/crates/spikard-core/src/lifecycle.rs +85 -61
  44. data/vendor/crates/spikard-core/src/parameters.rs +75 -54
  45. data/vendor/crates/spikard-core/src/problem.rs +19 -5
  46. data/vendor/crates/spikard-core/src/request_data.rs +16 -24
  47. data/vendor/crates/spikard-core/src/router.rs +26 -6
  48. data/vendor/crates/spikard-core/src/schema_registry.rs +25 -11
  49. data/vendor/crates/spikard-core/src/type_hints.rs +14 -7
  50. data/vendor/crates/spikard-core/src/validation/error_mapper.rs +30 -16
  51. data/vendor/crates/spikard-core/src/validation/mod.rs +46 -33
  52. data/vendor/crates/spikard-core/tests/di_dependency_defaults.rs +1 -1
  53. data/vendor/crates/spikard-core/tests/error_mapper.rs +2 -2
  54. data/vendor/crates/spikard-core/tests/parameters_edge_cases.rs +1 -1
  55. data/vendor/crates/spikard-core/tests/parameters_full.rs +1 -1
  56. data/vendor/crates/spikard-core/tests/parameters_schema_and_formats.rs +1 -1
  57. data/vendor/crates/spikard-core/tests/validation_coverage.rs +4 -4
  58. data/vendor/crates/spikard-http/Cargo.toml +11 -2
  59. data/vendor/crates/spikard-http/src/cors.rs +32 -11
  60. data/vendor/crates/spikard-http/src/di_handler.rs +12 -8
  61. data/vendor/crates/spikard-http/src/grpc/framing.rs +469 -0
  62. data/vendor/crates/spikard-http/src/grpc/handler.rs +887 -25
  63. data/vendor/crates/spikard-http/src/grpc/mod.rs +114 -22
  64. data/vendor/crates/spikard-http/src/grpc/service.rs +232 -2
  65. data/vendor/crates/spikard-http/src/grpc/streaming.rs +80 -2
  66. data/vendor/crates/spikard-http/src/handler_trait.rs +204 -27
  67. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +15 -15
  68. data/vendor/crates/spikard-http/src/jsonrpc/http_handler.rs +2 -2
  69. data/vendor/crates/spikard-http/src/jsonrpc/router.rs +2 -2
  70. data/vendor/crates/spikard-http/src/lib.rs +1 -1
  71. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +2 -2
  72. data/vendor/crates/spikard-http/src/lifecycle.rs +4 -4
  73. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +2 -0
  74. data/vendor/crates/spikard-http/src/server/fast_router.rs +186 -0
  75. data/vendor/crates/spikard-http/src/server/grpc_routing.rs +324 -23
  76. data/vendor/crates/spikard-http/src/server/handler.rs +33 -22
  77. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +21 -2
  78. data/vendor/crates/spikard-http/src/server/mod.rs +125 -20
  79. data/vendor/crates/spikard-http/src/server/request_extraction.rs +126 -44
  80. data/vendor/crates/spikard-http/src/server/routing_factory.rs +80 -69
  81. data/vendor/crates/spikard-http/tests/common/handlers.rs +2 -2
  82. data/vendor/crates/spikard-http/tests/common/test_builders.rs +12 -12
  83. data/vendor/crates/spikard-http/tests/di_handler_error_responses.rs +2 -2
  84. data/vendor/crates/spikard-http/tests/di_integration.rs +6 -6
  85. data/vendor/crates/spikard-http/tests/grpc_bidirectional_streaming.rs +430 -0
  86. data/vendor/crates/spikard-http/tests/grpc_client_streaming.rs +738 -0
  87. data/vendor/crates/spikard-http/tests/grpc_integration_test.rs +13 -9
  88. data/vendor/crates/spikard-http/tests/grpc_server_streaming.rs +974 -0
  89. data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +2 -2
  90. data/vendor/crates/spikard-http/tests/request_extraction_full.rs +4 -4
  91. data/vendor/crates/spikard-http/tests/server_config_builder.rs +2 -2
  92. data/vendor/crates/spikard-http/tests/server_cors_preflight.rs +1 -0
  93. data/vendor/crates/spikard-http/tests/server_openapi_jsonrpc_static.rs +140 -0
  94. data/vendor/crates/spikard-rb/Cargo.toml +11 -1
  95. data/vendor/crates/spikard-rb/build.rs +1 -0
  96. data/vendor/crates/spikard-rb/src/conversion.rs +138 -4
  97. data/vendor/crates/spikard-rb/src/grpc/handler.rs +706 -229
  98. data/vendor/crates/spikard-rb/src/grpc/mod.rs +6 -2
  99. data/vendor/crates/spikard-rb/src/gvl.rs +2 -2
  100. data/vendor/crates/spikard-rb/src/handler.rs +169 -91
  101. data/vendor/crates/spikard-rb/src/lib.rs +502 -62
  102. data/vendor/crates/spikard-rb/src/lifecycle.rs +31 -3
  103. data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +108 -43
  104. data/vendor/crates/spikard-rb/src/request.rs +117 -20
  105. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +52 -25
  106. data/vendor/crates/spikard-rb/src/server.rs +23 -14
  107. data/vendor/crates/spikard-rb/src/testing/client.rs +5 -4
  108. data/vendor/crates/spikard-rb/src/testing/sse.rs +1 -36
  109. data/vendor/crates/spikard-rb/src/testing/websocket.rs +3 -38
  110. data/vendor/crates/spikard-rb/src/websocket.rs +32 -23
  111. data/vendor/crates/spikard-rb-macros/Cargo.toml +9 -1
  112. data/vendor/crates/spikard-rb-macros/src/lib.rs +4 -5
  113. metadata +14 -4
  114. data/vendor/bundle/ruby/3.4.0/gems/diff-lcs-1.6.2/mise.toml +0 -5
  115. data/vendor/bundle/ruby/3.4.0/gems/rake-compiler-dock-1.10.0/build/buildkitd.toml +0 -2
@@ -15,7 +15,7 @@ pub struct ErrorResponseBuilder;
15
15
  impl ErrorResponseBuilder {
16
16
  /// Create a structured error response with status code and error details
17
17
  ///
18
- /// Returns a tuple of (StatusCode, JSON body as String)
18
+ /// Returns a tuple of (`StatusCode`, JSON body as String)
19
19
  ///
20
20
  /// # Arguments
21
21
  /// * `status` - HTTP status code
@@ -42,7 +42,7 @@ impl ErrorResponseBuilder {
42
42
 
43
43
  /// Create an error response with additional details
44
44
  ///
45
- /// Returns a tuple of (StatusCode, JSON body as String)
45
+ /// Returns a tuple of (`StatusCode`, JSON body as String)
46
46
  ///
47
47
  /// # Arguments
48
48
  /// * `status` - HTTP status code
@@ -79,17 +79,18 @@ impl ErrorResponseBuilder {
79
79
  (status, body)
80
80
  }
81
81
 
82
- /// Create an error response from a StructuredError
82
+ /// Create an error response from a `StructuredError`
83
83
  ///
84
- /// Returns a tuple of (StatusCode, JSON body as String)
84
+ /// Returns a tuple of (`StatusCode`, JSON body as String)
85
85
  ///
86
86
  /// # Arguments
87
87
  /// * `error` - The structured error
88
88
  ///
89
89
  /// # Note
90
- /// Uses INTERNAL_SERVER_ERROR as the default status code. Override with
90
+ /// Uses `INTERNAL_SERVER_ERROR` as the default status code. Override with
91
91
  /// `structured_error()` or `with_details()` for specific status codes.
92
- pub fn from_structured_error(error: StructuredError) -> (StatusCode, String) {
92
+ #[must_use]
93
+ pub fn from_structured_error(error: &StructuredError) -> (StatusCode, String) {
93
94
  let status = StatusCode::INTERNAL_SERVER_ERROR;
94
95
  let body = serde_json::to_string(&error)
95
96
  .unwrap_or_else(|_| r#"{"error":"serialization_failed","code":"internal_error","details":{}}"#.to_string());
@@ -98,8 +99,8 @@ impl ErrorResponseBuilder {
98
99
 
99
100
  /// Create a validation error response
100
101
  ///
101
- /// Converts ValidationError to RFC 9457 Problem Details format.
102
- /// Returns (StatusCode::UNPROCESSABLE_ENTITY, JSON body)
102
+ /// Converts `ValidationError` to RFC 9457 Problem Details format.
103
+ /// Returns (`StatusCode::UNPROCESSABLE_ENTITY`, JSON body)
103
104
  ///
104
105
  /// # Arguments
105
106
  /// * `validation_error` - The validation error containing one or more details
@@ -124,6 +125,7 @@ impl ErrorResponseBuilder {
124
125
  ///
125
126
  /// let (status, body) = ErrorResponseBuilder::validation_error(&validation_error);
126
127
  /// ```
128
+ #[must_use]
127
129
  pub fn validation_error(validation_error: &ValidationError) -> (StatusCode, String) {
128
130
  let problem = ProblemDetails::from_validation_error(validation_error);
129
131
  let status = problem.status_code();
@@ -136,7 +138,7 @@ impl ErrorResponseBuilder {
136
138
 
137
139
  /// Create an RFC 9457 Problem Details response
138
140
  ///
139
- /// Returns a tuple of (StatusCode, JSON body as String)
141
+ /// Returns a tuple of (`StatusCode`, JSON body as String)
140
142
  ///
141
143
  /// # Arguments
142
144
  /// * `problem` - The Problem Details object
@@ -150,6 +152,7 @@ impl ErrorResponseBuilder {
150
152
  /// let problem = ProblemDetails::not_found("User with id 123 not found");
151
153
  /// let (status, body) = ErrorResponseBuilder::problem_details_response(&problem);
152
154
  /// ```
155
+ #[must_use]
153
156
  pub fn problem_details_response(problem: &ProblemDetails) -> (StatusCode, String) {
154
157
  let status = problem.status_code();
155
158
  let body = serde_json::to_string(problem).unwrap_or_else(|_| {
@@ -161,70 +164,70 @@ impl ErrorResponseBuilder {
161
164
 
162
165
  /// Create a generic bad request error
163
166
  ///
164
- /// Returns (StatusCode::BAD_REQUEST, JSON body)
167
+ /// Returns (`StatusCode::BAD_REQUEST`, JSON body)
165
168
  pub fn bad_request(message: impl Into<String>) -> (StatusCode, String) {
166
169
  Self::structured_error(StatusCode::BAD_REQUEST, "bad_request", message)
167
170
  }
168
171
 
169
172
  /// Create a generic internal server error
170
173
  ///
171
- /// Returns (StatusCode::INTERNAL_SERVER_ERROR, JSON body)
174
+ /// Returns (`StatusCode::INTERNAL_SERVER_ERROR`, JSON body)
172
175
  pub fn internal_error(message: impl Into<String>) -> (StatusCode, String) {
173
176
  Self::structured_error(StatusCode::INTERNAL_SERVER_ERROR, "internal_error", message)
174
177
  }
175
178
 
176
179
  /// Create an unauthorized error
177
180
  ///
178
- /// Returns (StatusCode::UNAUTHORIZED, JSON body)
181
+ /// Returns (`StatusCode::UNAUTHORIZED`, JSON body)
179
182
  pub fn unauthorized(message: impl Into<String>) -> (StatusCode, String) {
180
183
  Self::structured_error(StatusCode::UNAUTHORIZED, "unauthorized", message)
181
184
  }
182
185
 
183
186
  /// Create a forbidden error
184
187
  ///
185
- /// Returns (StatusCode::FORBIDDEN, JSON body)
188
+ /// Returns (`StatusCode::FORBIDDEN`, JSON body)
186
189
  pub fn forbidden(message: impl Into<String>) -> (StatusCode, String) {
187
190
  Self::structured_error(StatusCode::FORBIDDEN, "forbidden", message)
188
191
  }
189
192
 
190
193
  /// Create a not found error
191
194
  ///
192
- /// Returns (StatusCode::NOT_FOUND, JSON body)
195
+ /// Returns (`StatusCode::NOT_FOUND`, JSON body)
193
196
  pub fn not_found(message: impl Into<String>) -> (StatusCode, String) {
194
197
  Self::structured_error(StatusCode::NOT_FOUND, "not_found", message)
195
198
  }
196
199
 
197
200
  /// Create a method not allowed error
198
201
  ///
199
- /// Returns (StatusCode::METHOD_NOT_ALLOWED, JSON body)
202
+ /// Returns (`StatusCode::METHOD_NOT_ALLOWED`, JSON body)
200
203
  pub fn method_not_allowed(message: impl Into<String>) -> (StatusCode, String) {
201
204
  Self::structured_error(StatusCode::METHOD_NOT_ALLOWED, "method_not_allowed", message)
202
205
  }
203
206
 
204
207
  /// Create an unprocessable entity error (validation failed)
205
208
  ///
206
- /// Returns (StatusCode::UNPROCESSABLE_ENTITY, JSON body)
209
+ /// Returns (`StatusCode::UNPROCESSABLE_ENTITY`, JSON body)
207
210
  pub fn unprocessable_entity(message: impl Into<String>) -> (StatusCode, String) {
208
211
  Self::structured_error(StatusCode::UNPROCESSABLE_ENTITY, "unprocessable_entity", message)
209
212
  }
210
213
 
211
214
  /// Create a conflict error
212
215
  ///
213
- /// Returns (StatusCode::CONFLICT, JSON body)
216
+ /// Returns (`StatusCode::CONFLICT`, JSON body)
214
217
  pub fn conflict(message: impl Into<String>) -> (StatusCode, String) {
215
218
  Self::structured_error(StatusCode::CONFLICT, "conflict", message)
216
219
  }
217
220
 
218
221
  /// Create a service unavailable error
219
222
  ///
220
- /// Returns (StatusCode::SERVICE_UNAVAILABLE, JSON body)
223
+ /// Returns (`StatusCode::SERVICE_UNAVAILABLE`, JSON body)
221
224
  pub fn service_unavailable(message: impl Into<String>) -> (StatusCode, String) {
222
225
  Self::structured_error(StatusCode::SERVICE_UNAVAILABLE, "service_unavailable", message)
223
226
  }
224
227
 
225
228
  /// Create a request timeout error
226
229
  ///
227
- /// Returns (StatusCode::REQUEST_TIMEOUT, JSON body)
230
+ /// Returns (`StatusCode::REQUEST_TIMEOUT`, JSON body)
228
231
  pub fn request_timeout(message: impl Into<String>) -> (StatusCode, String) {
229
232
  Self::structured_error(StatusCode::REQUEST_TIMEOUT, "request_timeout", message)
230
233
  }
@@ -257,7 +260,7 @@ mod tests {
257
260
  StatusCode::BAD_REQUEST,
258
261
  "validation_error",
259
262
  "Invalid email format",
260
- details.clone(),
263
+ details,
261
264
  );
262
265
  assert_eq!(status, StatusCode::BAD_REQUEST);
263
266
  let parsed: Value = serde_json::from_str(&body).unwrap();
@@ -270,7 +273,7 @@ mod tests {
270
273
  #[test]
271
274
  fn test_from_structured_error() {
272
275
  let error = StructuredError::simple("test_error", "Something went wrong");
273
- let (status, body) = ErrorResponseBuilder::from_structured_error(error);
276
+ let (status, body) = ErrorResponseBuilder::from_structured_error(&error);
274
277
  assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
275
278
  let parsed: Value = serde_json::from_str(&body).unwrap();
276
279
  assert_eq!(parsed["code"], "test_error");
@@ -284,7 +287,7 @@ mod tests {
284
287
  error_type: "missing".to_string(),
285
288
  loc: vec!["body".to_string(), "username".to_string()],
286
289
  msg: "Field required".to_string(),
287
- input: Value::String("".to_string()),
290
+ input: Value::String(String::new()),
288
291
  ctx: None,
289
292
  }],
290
293
  };
@@ -6,20 +6,20 @@
6
6
  use std::collections::HashMap;
7
7
  use tonic::metadata::{MetadataKey, MetadataMap, MetadataValue};
8
8
 
9
- /// Extract metadata from gRPC MetadataMap to a simple HashMap.
9
+ /// Extract metadata from gRPC `MetadataMap` to a simple `HashMap`.
10
10
  ///
11
- /// This function converts gRPC metadata to a language-agnostic HashMap format
11
+ /// This function converts gRPC metadata to a language-agnostic `HashMap` format
12
12
  /// that can be easily passed to language bindings. Only ASCII metadata is
13
13
  /// included; binary metadata is skipped with optional logging.
14
14
  ///
15
15
  /// # Arguments
16
16
  ///
17
- /// * `metadata` - The gRPC MetadataMap to extract from
17
+ /// * `metadata` - The gRPC `MetadataMap` to extract from
18
18
  /// * `log_binary_skip` - Whether to log when binary metadata is skipped
19
19
  ///
20
20
  /// # Returns
21
21
  ///
22
- /// A HashMap containing all ASCII metadata key-value pairs
22
+ /// A `HashMap` containing all ASCII metadata key-value pairs
23
23
  ///
24
24
  /// # Examples
25
25
  ///
@@ -55,19 +55,19 @@ pub fn extract_metadata_to_hashmap(metadata: &MetadataMap, log_binary_skip: bool
55
55
  map
56
56
  }
57
57
 
58
- /// Convert a HashMap to gRPC MetadataMap.
58
+ /// Convert a `HashMap` to gRPC `MetadataMap`.
59
59
  ///
60
- /// This function converts a language-agnostic HashMap into a gRPC MetadataMap
60
+ /// This function converts a language-agnostic `HashMap` into a gRPC `MetadataMap`
61
61
  /// that can be used in responses. All keys and values are validated and errors
62
62
  /// are returned if any are invalid.
63
63
  ///
64
64
  /// # Arguments
65
65
  ///
66
- /// * `map` - The HashMap to convert
66
+ /// * `map` - The `HashMap` to convert
67
67
  ///
68
68
  /// # Returns
69
69
  ///
70
- /// A Result containing the MetadataMap or an error message
70
+ /// A Result containing the `MetadataMap` or an error message
71
71
  ///
72
72
  /// # Errors
73
73
  ///
@@ -87,15 +87,17 @@ pub fn extract_metadata_to_hashmap(metadata: &MetadataMap, log_binary_skip: bool
87
87
  /// let metadata = hashmap_to_metadata(&map).unwrap();
88
88
  /// assert!(metadata.contains_key("content-type"));
89
89
  /// ```
90
- pub fn hashmap_to_metadata(map: &HashMap<String, String>) -> Result<MetadataMap, String> {
90
+ pub fn hashmap_to_metadata<S: std::hash::BuildHasher>(
91
+ map: &std::collections::HashMap<String, String, S>,
92
+ ) -> Result<MetadataMap, String> {
91
93
  let mut metadata = MetadataMap::new();
92
94
 
93
95
  for (key, value) in map {
94
- let metadata_key = MetadataKey::from_bytes(key.as_bytes())
95
- .map_err(|err| format!("Invalid metadata key '{}': {}", key, err))?;
96
+ let metadata_key =
97
+ MetadataKey::from_bytes(key.as_bytes()).map_err(|err| format!("Invalid metadata key '{key}': {err}"))?;
96
98
 
97
99
  let metadata_value =
98
- MetadataValue::try_from(value).map_err(|err| format!("Invalid metadata value for '{}': {}", key, err))?;
100
+ MetadataValue::try_from(value).map_err(|err| format!("Invalid metadata value for '{key}': {err}"))?;
99
101
 
100
102
  metadata.insert(metadata_key, metadata_value);
101
103
  }
@@ -33,7 +33,9 @@ pub enum HandlerError {
33
33
 
34
34
  impl From<ValidationError> for HandlerError {
35
35
  fn from(err: ValidationError) -> Self {
36
- HandlerError::Validation(format!("{:?}", err))
36
+ // PERFORMANCE: Avoid format! allocation for debug representation.
37
+ // Most callers just need the error type, not the full debug output.
38
+ Self::Validation(err.to_string())
37
39
  }
38
40
  }
39
41
 
@@ -54,6 +56,10 @@ pub trait LanguageHandler: Send + Sync {
54
56
  type Output: Send;
55
57
 
56
58
  /// Prepare request data for passing to the language handler
59
+ ///
60
+ /// # Errors
61
+ ///
62
+ /// Returns an error if request preparation fails.
57
63
  fn prepare_request(&self, request_data: &RequestData) -> Result<Self::Input, HandlerError>;
58
64
 
59
65
  /// Invoke the language-specific handler with the prepared input
@@ -63,6 +69,10 @@ pub trait LanguageHandler: Send + Sync {
63
69
  ) -> Pin<Box<dyn Future<Output = Result<Self::Output, HandlerError>> + Send + '_>>;
64
70
 
65
71
  /// Interpret the handler's output and convert it to an HTTP response
72
+ ///
73
+ /// # Errors
74
+ ///
75
+ /// Returns an error if response interpretation fails.
66
76
  fn interpret_response(&self, output: Self::Output) -> Result<Response<Body>, HandlerError>;
67
77
  }
68
78
 
@@ -81,7 +91,7 @@ pub struct HandlerExecutor<L: LanguageHandler> {
81
91
 
82
92
  impl<L: LanguageHandler> HandlerExecutor<L> {
83
93
  /// Create a new handler executor
84
- pub fn new(language_handler: Arc<L>, request_validator: Option<Arc<SchemaValidator>>) -> Self {
94
+ pub const fn new(language_handler: Arc<L>, request_validator: Option<Arc<SchemaValidator>>) -> Self {
85
95
  Self {
86
96
  language_handler,
87
97
  request_validator,
@@ -89,7 +99,7 @@ impl<L: LanguageHandler> HandlerExecutor<L> {
89
99
  }
90
100
 
91
101
  /// Create a handler executor with only a language handler
92
- pub fn with_handler(language_handler: Arc<L>) -> Self {
102
+ pub const fn with_handler(language_handler: Arc<L>) -> Self {
93
103
  Self {
94
104
  language_handler,
95
105
  request_validator: None,
@@ -97,6 +107,7 @@ impl<L: LanguageHandler> HandlerExecutor<L> {
97
107
  }
98
108
 
99
109
  /// Add request validation to this executor
110
+ #[must_use]
100
111
  pub fn with_request_validator(mut self, validator: Arc<SchemaValidator>) -> Self {
101
112
  self.request_validator = Some(validator);
102
113
  self
@@ -116,21 +127,24 @@ impl<L: LanguageHandler + 'static> Handler for HandlerExecutor<L> {
116
127
  return Err(ErrorResponseBuilder::validation_error(&validation_err));
117
128
  }
118
129
 
130
+ // PERFORMANCE: Avoid format! allocations in the hot path. ErrorResponseBuilder
131
+ // can accept &dyn Display or construct error messages directly, reducing
132
+ // string allocation overhead in typical error handling paths.
119
133
  let input = self
120
134
  .language_handler
121
135
  .prepare_request(&request_data)
122
- .map_err(|e| ErrorResponseBuilder::internal_error(format!("Failed to prepare request: {}", e)))?;
136
+ .map_err(|e| ErrorResponseBuilder::internal_error(format!("Failed to prepare request: {e}")))?;
123
137
 
124
138
  let output = self
125
139
  .language_handler
126
140
  .invoke_handler(input)
127
141
  .await
128
- .map_err(|e| ErrorResponseBuilder::internal_error(format!("Handler execution failed: {}", e)))?;
142
+ .map_err(|e| ErrorResponseBuilder::internal_error(format!("Handler execution failed: {e}")))?;
129
143
 
130
144
  let response = self
131
145
  .language_handler
132
146
  .interpret_response(output)
133
- .map_err(|e| ErrorResponseBuilder::internal_error(format!("Failed to interpret response: {}", e)))?;
147
+ .map_err(|e| ErrorResponseBuilder::internal_error(format!("Failed to interpret response: {e}")))?;
134
148
 
135
149
  Ok(response)
136
150
  })
@@ -172,10 +186,10 @@ mod tests {
172
186
  let request = Request::builder().body(Body::empty()).unwrap();
173
187
  let request_data = RequestData {
174
188
  path_params: Arc::new(std::collections::HashMap::new()),
175
- query_params: json!({}),
189
+ query_params: Arc::new(json!({})),
176
190
  validated_params: None,
177
191
  raw_query_params: Arc::new(std::collections::HashMap::new()),
178
- body: json!({}),
192
+ body: Arc::new(json!({})),
179
193
  raw_body: None,
180
194
  headers: Arc::new(std::collections::HashMap::new()),
181
195
  cookies: Arc::new(std::collections::HashMap::new()),
@@ -196,10 +210,10 @@ mod tests {
196
210
  let request = Request::builder().body(Body::empty()).unwrap();
197
211
  let request_data = RequestData {
198
212
  path_params: Arc::new(std::collections::HashMap::new()),
199
- query_params: json!({}),
213
+ query_params: Arc::new(json!({})),
200
214
  validated_params: None,
201
215
  raw_query_params: Arc::new(std::collections::HashMap::new()),
202
- body: json!({}),
216
+ body: Arc::new(json!({})),
203
217
  raw_body: None,
204
218
  headers: Arc::new(std::collections::HashMap::new()),
205
219
  cookies: Arc::new(std::collections::HashMap::new()),