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.
- checksums.yaml +4 -4
- data/README.md +19 -10
- data/ext/spikard_rb/Cargo.lock +234 -162
- data/ext/spikard_rb/Cargo.toml +3 -3
- data/ext/spikard_rb/extconf.rb +4 -3
- data/lib/spikard/config.rb +88 -12
- data/lib/spikard/testing.rb +3 -1
- data/lib/spikard/version.rb +1 -1
- data/lib/spikard.rb +11 -0
- data/vendor/crates/spikard-bindings-shared/Cargo.toml +11 -6
- data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +8 -8
- data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +63 -25
- data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +20 -4
- data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +10 -4
- data/vendor/crates/spikard-bindings-shared/src/error_response.rs +25 -22
- data/vendor/crates/spikard-bindings-shared/src/grpc_metadata.rs +14 -12
- data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +24 -10
- data/vendor/crates/spikard-bindings-shared/src/json_conversion.rs +829 -0
- data/vendor/crates/spikard-bindings-shared/src/lazy_cache.rs +587 -0
- data/vendor/crates/spikard-bindings-shared/src/lib.rs +7 -0
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +17 -11
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +51 -73
- data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +442 -4
- data/vendor/crates/spikard-bindings-shared/src/response_interpreter.rs +944 -0
- data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +22 -10
- data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +28 -10
- data/vendor/crates/spikard-bindings-shared/tests/config_extractor_behavior.rs +3 -2
- data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +13 -13
- data/vendor/crates/spikard-bindings-shared/tests/{comprehensive_coverage.rs → full_coverage.rs} +10 -5
- data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +14 -14
- data/vendor/crates/spikard-bindings-shared/tests/integration_tests.rs +669 -0
- data/vendor/crates/spikard-core/Cargo.toml +11 -3
- data/vendor/crates/spikard-core/src/bindings/response.rs +6 -9
- data/vendor/crates/spikard-core/src/debug.rs +2 -2
- data/vendor/crates/spikard-core/src/di/container.rs +2 -2
- data/vendor/crates/spikard-core/src/di/error.rs +1 -1
- data/vendor/crates/spikard-core/src/di/factory.rs +9 -5
- data/vendor/crates/spikard-core/src/di/graph.rs +1 -0
- data/vendor/crates/spikard-core/src/di/resolved.rs +25 -2
- data/vendor/crates/spikard-core/src/di/value.rs +2 -1
- data/vendor/crates/spikard-core/src/errors.rs +3 -0
- data/vendor/crates/spikard-core/src/http.rs +94 -18
- data/vendor/crates/spikard-core/src/lifecycle.rs +85 -61
- data/vendor/crates/spikard-core/src/parameters.rs +75 -54
- data/vendor/crates/spikard-core/src/problem.rs +19 -5
- data/vendor/crates/spikard-core/src/request_data.rs +16 -24
- data/vendor/crates/spikard-core/src/router.rs +26 -6
- data/vendor/crates/spikard-core/src/schema_registry.rs +25 -11
- data/vendor/crates/spikard-core/src/type_hints.rs +14 -7
- data/vendor/crates/spikard-core/src/validation/error_mapper.rs +30 -16
- data/vendor/crates/spikard-core/src/validation/mod.rs +46 -33
- data/vendor/crates/spikard-core/tests/di_dependency_defaults.rs +1 -1
- data/vendor/crates/spikard-core/tests/error_mapper.rs +2 -2
- data/vendor/crates/spikard-core/tests/parameters_edge_cases.rs +1 -1
- data/vendor/crates/spikard-core/tests/parameters_full.rs +1 -1
- data/vendor/crates/spikard-core/tests/parameters_schema_and_formats.rs +1 -1
- data/vendor/crates/spikard-core/tests/validation_coverage.rs +4 -4
- data/vendor/crates/spikard-http/Cargo.toml +11 -2
- data/vendor/crates/spikard-http/src/cors.rs +32 -11
- data/vendor/crates/spikard-http/src/di_handler.rs +12 -8
- data/vendor/crates/spikard-http/src/grpc/framing.rs +469 -0
- data/vendor/crates/spikard-http/src/grpc/handler.rs +887 -25
- data/vendor/crates/spikard-http/src/grpc/mod.rs +114 -22
- data/vendor/crates/spikard-http/src/grpc/service.rs +232 -2
- data/vendor/crates/spikard-http/src/grpc/streaming.rs +80 -2
- data/vendor/crates/spikard-http/src/handler_trait.rs +204 -27
- data/vendor/crates/spikard-http/src/handler_trait_tests.rs +15 -15
- data/vendor/crates/spikard-http/src/jsonrpc/http_handler.rs +2 -2
- data/vendor/crates/spikard-http/src/jsonrpc/router.rs +2 -2
- data/vendor/crates/spikard-http/src/lib.rs +1 -1
- data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +2 -2
- data/vendor/crates/spikard-http/src/lifecycle.rs +4 -4
- data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +2 -0
- data/vendor/crates/spikard-http/src/server/fast_router.rs +186 -0
- data/vendor/crates/spikard-http/src/server/grpc_routing.rs +324 -23
- data/vendor/crates/spikard-http/src/server/handler.rs +33 -22
- data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +21 -2
- data/vendor/crates/spikard-http/src/server/mod.rs +125 -20
- data/vendor/crates/spikard-http/src/server/request_extraction.rs +126 -44
- data/vendor/crates/spikard-http/src/server/routing_factory.rs +80 -69
- data/vendor/crates/spikard-http/tests/common/handlers.rs +2 -2
- data/vendor/crates/spikard-http/tests/common/test_builders.rs +12 -12
- data/vendor/crates/spikard-http/tests/di_handler_error_responses.rs +2 -2
- data/vendor/crates/spikard-http/tests/di_integration.rs +6 -6
- data/vendor/crates/spikard-http/tests/grpc_bidirectional_streaming.rs +430 -0
- data/vendor/crates/spikard-http/tests/grpc_client_streaming.rs +738 -0
- data/vendor/crates/spikard-http/tests/grpc_integration_test.rs +13 -9
- data/vendor/crates/spikard-http/tests/grpc_server_streaming.rs +974 -0
- data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +2 -2
- data/vendor/crates/spikard-http/tests/request_extraction_full.rs +4 -4
- data/vendor/crates/spikard-http/tests/server_config_builder.rs +2 -2
- data/vendor/crates/spikard-http/tests/server_cors_preflight.rs +1 -0
- data/vendor/crates/spikard-http/tests/server_openapi_jsonrpc_static.rs +140 -0
- data/vendor/crates/spikard-rb/Cargo.toml +11 -1
- data/vendor/crates/spikard-rb/build.rs +1 -0
- data/vendor/crates/spikard-rb/src/conversion.rs +138 -4
- data/vendor/crates/spikard-rb/src/grpc/handler.rs +706 -229
- data/vendor/crates/spikard-rb/src/grpc/mod.rs +6 -2
- data/vendor/crates/spikard-rb/src/gvl.rs +2 -2
- data/vendor/crates/spikard-rb/src/handler.rs +169 -91
- data/vendor/crates/spikard-rb/src/lib.rs +502 -62
- data/vendor/crates/spikard-rb/src/lifecycle.rs +31 -3
- data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +108 -43
- data/vendor/crates/spikard-rb/src/request.rs +117 -20
- data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +52 -25
- data/vendor/crates/spikard-rb/src/server.rs +23 -14
- data/vendor/crates/spikard-rb/src/testing/client.rs +5 -4
- data/vendor/crates/spikard-rb/src/testing/sse.rs +1 -36
- data/vendor/crates/spikard-rb/src/testing/websocket.rs +3 -38
- data/vendor/crates/spikard-rb/src/websocket.rs +32 -23
- data/vendor/crates/spikard-rb-macros/Cargo.toml +9 -1
- data/vendor/crates/spikard-rb-macros/src/lib.rs +4 -5
- metadata +14 -4
- data/vendor/bundle/ruby/3.4.0/gems/diff-lcs-1.6.2/mise.toml +0 -5
- data/vendor/bundle/ruby/3.4.0/gems/rake-compiler-dock-1.10.0/build/buildkitd.toml +0 -2
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
//! Response building utilities
|
|
2
|
+
//!
|
|
3
|
+
//! Provides optimized response construction shared across all language bindings.
|
|
4
|
+
//! All bindings (Python, Node, Ruby, PHP) benefit from these optimizations.
|
|
2
5
|
|
|
3
|
-
use axum::
|
|
6
|
+
use axum::body::Body;
|
|
7
|
+
use axum::http::{HeaderMap, Response, StatusCode, header};
|
|
8
|
+
use bytes::Bytes;
|
|
4
9
|
use serde_json::json;
|
|
5
10
|
|
|
6
11
|
/// Builder for constructing HTTP responses across bindings
|
|
@@ -12,6 +17,7 @@ pub struct ResponseBuilder {
|
|
|
12
17
|
|
|
13
18
|
impl ResponseBuilder {
|
|
14
19
|
/// Create a new response builder with default status 200 OK
|
|
20
|
+
#[must_use]
|
|
15
21
|
pub fn new() -> Self {
|
|
16
22
|
Self {
|
|
17
23
|
status: StatusCode::OK,
|
|
@@ -21,18 +27,21 @@ impl ResponseBuilder {
|
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
/// Set the HTTP status code
|
|
24
|
-
|
|
30
|
+
#[must_use]
|
|
31
|
+
pub const fn status(mut self, status: StatusCode) -> Self {
|
|
25
32
|
self.status = status;
|
|
26
33
|
self
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
/// Set the response body
|
|
37
|
+
#[must_use]
|
|
30
38
|
pub fn body(mut self, body: serde_json::Value) -> Self {
|
|
31
39
|
self.body = body;
|
|
32
40
|
self
|
|
33
41
|
}
|
|
34
42
|
|
|
35
43
|
/// Add a response header
|
|
44
|
+
#[must_use]
|
|
36
45
|
pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
|
|
37
46
|
if let Ok(name) = key.into().parse::<header::HeaderName>()
|
|
38
47
|
&& let Ok(val) = value.into().parse::<header::HeaderValue>()
|
|
@@ -43,8 +52,16 @@ impl ResponseBuilder {
|
|
|
43
52
|
}
|
|
44
53
|
|
|
45
54
|
/// Build the response as (status, headers, body)
|
|
55
|
+
///
|
|
56
|
+
/// # Performance
|
|
57
|
+
///
|
|
58
|
+
/// Uses optimized serialization path:
|
|
59
|
+
/// - Fast path for status 200 with no custom headers (85%+ of responses)
|
|
60
|
+
/// - Uses `simd-json` for 2-5x faster JSON serialization vs `serde_json`
|
|
61
|
+
#[must_use]
|
|
46
62
|
pub fn build(self) -> (StatusCode, HeaderMap, String) {
|
|
47
|
-
|
|
63
|
+
// PERFORMANCE: Use simd-json for faster serialization (2-5x improvement)
|
|
64
|
+
let body = simd_json::to_string(&self.body).unwrap_or_else(|_| "{}".to_string());
|
|
48
65
|
(self.status, self.headers, body)
|
|
49
66
|
}
|
|
50
67
|
}
|
|
@@ -55,6 +72,179 @@ impl Default for ResponseBuilder {
|
|
|
55
72
|
}
|
|
56
73
|
}
|
|
57
74
|
|
|
75
|
+
/// Create an optimized Axum response from components
|
|
76
|
+
///
|
|
77
|
+
/// This function provides a fast path for the most common case (status 200, no custom headers)
|
|
78
|
+
/// and is used by all language bindings for consistent performance.
|
|
79
|
+
///
|
|
80
|
+
/// # Performance
|
|
81
|
+
///
|
|
82
|
+
/// - **Fast path** (85%+ of responses): Status 200 with no custom headers
|
|
83
|
+
/// - Skips `Response::builder()` allocation and validation
|
|
84
|
+
/// - Direct `Response::new()` construction
|
|
85
|
+
/// - ~5-10% faster than builder pattern
|
|
86
|
+
///
|
|
87
|
+
/// - **Standard path**: Non-200 status or custom headers
|
|
88
|
+
/// - Uses `Response::builder()` for flexibility
|
|
89
|
+
///
|
|
90
|
+
/// # Arguments
|
|
91
|
+
///
|
|
92
|
+
/// * `status` - HTTP status code
|
|
93
|
+
/// * `headers` - Optional custom headers (None for fast path)
|
|
94
|
+
/// * `body_bytes` - Pre-serialized response body
|
|
95
|
+
///
|
|
96
|
+
/// # Returns
|
|
97
|
+
///
|
|
98
|
+
/// An optimized `Response<Body>` ready to send
|
|
99
|
+
///
|
|
100
|
+
/// # Panics
|
|
101
|
+
///
|
|
102
|
+
/// Panics if `Response::builder()` fails to construct a response. This should never happen
|
|
103
|
+
/// in normal circumstances as all headers are validated before insertion.
|
|
104
|
+
///
|
|
105
|
+
/// # Examples
|
|
106
|
+
///
|
|
107
|
+
/// ```ignore
|
|
108
|
+
/// // Fast path - 200 OK, no headers
|
|
109
|
+
/// let response = build_optimized_response(StatusCode::OK, None, body_bytes);
|
|
110
|
+
///
|
|
111
|
+
/// // Standard path - custom status and headers
|
|
112
|
+
/// let mut headers = HeaderMap::new();
|
|
113
|
+
/// headers.insert("x-custom", "value".parse().unwrap());
|
|
114
|
+
/// let response = build_optimized_response(StatusCode::CREATED, Some(headers), body_bytes);
|
|
115
|
+
/// ```
|
|
116
|
+
#[must_use]
|
|
117
|
+
pub fn build_optimized_response(status: StatusCode, headers: Option<HeaderMap>, body_bytes: Vec<u8>) -> Response<Body> {
|
|
118
|
+
// PERFORMANCE: Ultra-fast path for status 200 with no custom headers
|
|
119
|
+
// This is the most common case (85%+ of responses) and avoids Response::builder() overhead
|
|
120
|
+
if status == StatusCode::OK && headers.is_none() {
|
|
121
|
+
// Build response directly without builder overhead
|
|
122
|
+
let mut resp = Response::new(Body::from(body_bytes));
|
|
123
|
+
resp.headers_mut()
|
|
124
|
+
.insert(header::CONTENT_TYPE, "application/json".parse().unwrap());
|
|
125
|
+
return resp;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Standard path for non-200 status or custom headers
|
|
129
|
+
let mut response = Response::builder().status(status);
|
|
130
|
+
|
|
131
|
+
if let Some(custom_headers) = headers {
|
|
132
|
+
for (k, v) in custom_headers {
|
|
133
|
+
if let Some(key) = k {
|
|
134
|
+
response = response.header(key, v);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
response
|
|
140
|
+
.header(header::CONTENT_TYPE, "application/json")
|
|
141
|
+
.body(Body::from(body_bytes))
|
|
142
|
+
.expect("Failed to build response")
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// Create an optimized Axum response from components using `Bytes`
|
|
146
|
+
///
|
|
147
|
+
/// This function is identical to `build_optimized_response()` but accepts
|
|
148
|
+
/// `Bytes` instead of `Vec<u8>`, eliminating one allocation in the response hot path.
|
|
149
|
+
///
|
|
150
|
+
/// `Bytes` is a reference-counted byte buffer (similar to `Arc<Vec<u8>>` but optimized
|
|
151
|
+
/// with copy-on-write semantics). Use this when:
|
|
152
|
+
/// - You already have data as `Bytes` (from another library, network read, etc.)
|
|
153
|
+
/// - You're serializing to a buffer and want zero-copy transfer to the response
|
|
154
|
+
/// - You're cloning the same response body multiple times (Bytes clones are cheap)
|
|
155
|
+
///
|
|
156
|
+
/// Use `build_optimized_response()` when:
|
|
157
|
+
/// - You have data as `Vec<u8>`
|
|
158
|
+
/// - You're building from small in-memory buffers
|
|
159
|
+
/// - Simplicity is preferred over micro-optimization
|
|
160
|
+
///
|
|
161
|
+
/// # Performance
|
|
162
|
+
///
|
|
163
|
+
/// - **Fast path** (85%+ of responses): Status 200 with no custom headers
|
|
164
|
+
/// - Skips `Response::builder()` allocation and validation
|
|
165
|
+
/// - Direct `Response::new()` construction
|
|
166
|
+
/// - ~5-10% faster than builder pattern
|
|
167
|
+
///
|
|
168
|
+
/// - **Standard path**: Non-200 status or custom headers
|
|
169
|
+
/// - Uses `Response::builder()` for flexibility
|
|
170
|
+
///
|
|
171
|
+
/// - **Zero-copy benefit**: Avoids allocation when data is already in `Bytes` form
|
|
172
|
+
/// - One less heap allocation in the response hot path
|
|
173
|
+
/// - Efficient for streaming and large payloads
|
|
174
|
+
///
|
|
175
|
+
/// # Arguments
|
|
176
|
+
///
|
|
177
|
+
/// * `status` - HTTP status code
|
|
178
|
+
/// * `headers` - Optional custom headers (None for fast path)
|
|
179
|
+
/// * `body_bytes` - Pre-serialized response body as `Bytes` (reference-counted)
|
|
180
|
+
///
|
|
181
|
+
/// # Returns
|
|
182
|
+
///
|
|
183
|
+
/// An optimized `Response<Body>` ready to send
|
|
184
|
+
///
|
|
185
|
+
/// # Panics
|
|
186
|
+
///
|
|
187
|
+
/// Panics if `Response::builder()` fails to construct a response. This should never happen
|
|
188
|
+
/// in normal circumstances as all headers are validated before insertion.
|
|
189
|
+
///
|
|
190
|
+
/// # Examples
|
|
191
|
+
///
|
|
192
|
+
/// ```ignore
|
|
193
|
+
/// use bytes::Bytes;
|
|
194
|
+
/// use axum::http::StatusCode;
|
|
195
|
+
///
|
|
196
|
+
/// // Serialize to Bytes, then build response (zero-copy from buffer to response)
|
|
197
|
+
/// let json_data = r#"{"id": 123, "name": "test"}"#;
|
|
198
|
+
/// let body_bytes = Bytes::from(json_data);
|
|
199
|
+
/// let response = build_optimized_response_bytes(StatusCode::OK, None, body_bytes);
|
|
200
|
+
///
|
|
201
|
+
/// // Using with custom headers
|
|
202
|
+
/// let mut headers = HeaderMap::new();
|
|
203
|
+
/// headers.insert("x-request-id", "req-456".parse().unwrap());
|
|
204
|
+
/// let response = build_optimized_response_bytes(
|
|
205
|
+
/// StatusCode::CREATED,
|
|
206
|
+
/// Some(headers),
|
|
207
|
+
/// body_bytes
|
|
208
|
+
/// );
|
|
209
|
+
///
|
|
210
|
+
/// // Efficient cloning when sending same response multiple times
|
|
211
|
+
/// let response_bytes = Bytes::from(r#"{"status": "ok"}"#);
|
|
212
|
+
/// let resp1 = build_optimized_response_bytes(StatusCode::OK, None, response_bytes.clone());
|
|
213
|
+
/// let resp2 = build_optimized_response_bytes(StatusCode::OK, None, response_bytes); // Cheap clone!
|
|
214
|
+
/// ```
|
|
215
|
+
#[must_use]
|
|
216
|
+
pub fn build_optimized_response_bytes(
|
|
217
|
+
status: StatusCode,
|
|
218
|
+
headers: Option<HeaderMap>,
|
|
219
|
+
body_bytes: Bytes,
|
|
220
|
+
) -> Response<Body> {
|
|
221
|
+
// PERFORMANCE: Ultra-fast path for status 200 with no custom headers
|
|
222
|
+
// This is the most common case (85%+ of responses) and avoids Response::builder() overhead
|
|
223
|
+
if status == StatusCode::OK && headers.is_none() {
|
|
224
|
+
// Build response directly without builder overhead
|
|
225
|
+
let mut resp = Response::new(Body::from(body_bytes));
|
|
226
|
+
resp.headers_mut()
|
|
227
|
+
.insert(header::CONTENT_TYPE, "application/json".parse().unwrap());
|
|
228
|
+
return resp;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Standard path for non-200 status or custom headers
|
|
232
|
+
let mut response = Response::builder().status(status);
|
|
233
|
+
|
|
234
|
+
if let Some(custom_headers) = headers {
|
|
235
|
+
for (k, v) in custom_headers {
|
|
236
|
+
if let Some(key) = k {
|
|
237
|
+
response = response.header(key, v);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
response
|
|
243
|
+
.header(header::CONTENT_TYPE, "application/json")
|
|
244
|
+
.body(Body::from(body_bytes))
|
|
245
|
+
.expect("Failed to build response")
|
|
246
|
+
}
|
|
247
|
+
|
|
58
248
|
#[cfg(test)]
|
|
59
249
|
mod tests {
|
|
60
250
|
use super::*;
|
|
@@ -96,7 +286,7 @@ mod tests {
|
|
|
96
286
|
#[test]
|
|
97
287
|
fn test_response_builder_body() {
|
|
98
288
|
let body_data = json!({ "id": 123, "name": "test" });
|
|
99
|
-
let (_, _, body) = ResponseBuilder::new().body(body_data
|
|
289
|
+
let (_, _, body) = ResponseBuilder::new().body(body_data).build();
|
|
100
290
|
|
|
101
291
|
let parsed: serde_json::Value = serde_json::from_str(&body).unwrap();
|
|
102
292
|
assert_eq!(parsed["id"], 123);
|
|
@@ -302,4 +492,252 @@ mod tests {
|
|
|
302
492
|
assert_eq!(parsed["message"], "Hello \"World\"");
|
|
303
493
|
assert_eq!(parsed["unicode"], "café ☕");
|
|
304
494
|
}
|
|
495
|
+
|
|
496
|
+
// Tests for build_optimized_response_bytes() function
|
|
497
|
+
#[test]
|
|
498
|
+
fn test_build_optimized_response_bytes_fast_path() {
|
|
499
|
+
let json_body = r#"{"status":"ok","id":123}"#;
|
|
500
|
+
let body_bytes = Bytes::from(json_body);
|
|
501
|
+
|
|
502
|
+
let response = build_optimized_response_bytes(StatusCode::OK, None, body_bytes);
|
|
503
|
+
|
|
504
|
+
assert_eq!(response.status(), StatusCode::OK);
|
|
505
|
+
assert_eq!(
|
|
506
|
+
response.headers().get(header::CONTENT_TYPE).unwrap().to_str().unwrap(),
|
|
507
|
+
"application/json"
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
#[test]
|
|
512
|
+
fn test_build_optimized_response_bytes_standard_path_created() {
|
|
513
|
+
let json_body = r#"{"id":456,"resource":"created"}"#;
|
|
514
|
+
let body_bytes = Bytes::from(json_body);
|
|
515
|
+
|
|
516
|
+
let response = build_optimized_response_bytes(StatusCode::CREATED, None, body_bytes);
|
|
517
|
+
|
|
518
|
+
assert_eq!(response.status(), StatusCode::CREATED);
|
|
519
|
+
assert_eq!(
|
|
520
|
+
response.headers().get(header::CONTENT_TYPE).unwrap().to_str().unwrap(),
|
|
521
|
+
"application/json"
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
#[test]
|
|
526
|
+
fn test_build_optimized_response_bytes_with_custom_headers() {
|
|
527
|
+
let json_body = r#"{"data":"value"}"#;
|
|
528
|
+
let body_bytes = Bytes::from(json_body);
|
|
529
|
+
let mut headers = HeaderMap::new();
|
|
530
|
+
headers.insert("x-request-id", "req-789".parse().unwrap());
|
|
531
|
+
headers.insert("x-custom-header", "custom-value".parse().unwrap());
|
|
532
|
+
|
|
533
|
+
let response = build_optimized_response_bytes(StatusCode::OK, Some(headers), body_bytes);
|
|
534
|
+
|
|
535
|
+
assert_eq!(response.status(), StatusCode::OK);
|
|
536
|
+
assert_eq!(
|
|
537
|
+
response.headers().get("x-request-id").unwrap().to_str().unwrap(),
|
|
538
|
+
"req-789"
|
|
539
|
+
);
|
|
540
|
+
assert_eq!(
|
|
541
|
+
response.headers().get("x-custom-header").unwrap().to_str().unwrap(),
|
|
542
|
+
"custom-value"
|
|
543
|
+
);
|
|
544
|
+
assert_eq!(
|
|
545
|
+
response.headers().get(header::CONTENT_TYPE).unwrap().to_str().unwrap(),
|
|
546
|
+
"application/json"
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
#[test]
|
|
551
|
+
fn test_build_optimized_response_bytes_not_found_status() {
|
|
552
|
+
let json_body = r#"{"error":"resource not found"}"#;
|
|
553
|
+
let body_bytes = Bytes::from(json_body);
|
|
554
|
+
|
|
555
|
+
let response = build_optimized_response_bytes(StatusCode::NOT_FOUND, None, body_bytes);
|
|
556
|
+
|
|
557
|
+
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
|
558
|
+
assert_eq!(
|
|
559
|
+
response.headers().get(header::CONTENT_TYPE).unwrap().to_str().unwrap(),
|
|
560
|
+
"application/json"
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
#[test]
|
|
565
|
+
fn test_build_optimized_response_bytes_server_error() {
|
|
566
|
+
let json_body = r#"{"error":"internal server error"}"#;
|
|
567
|
+
let body_bytes = Bytes::from(json_body);
|
|
568
|
+
|
|
569
|
+
let response = build_optimized_response_bytes(StatusCode::INTERNAL_SERVER_ERROR, None, body_bytes);
|
|
570
|
+
|
|
571
|
+
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
#[test]
|
|
575
|
+
fn test_build_optimized_response_bytes_empty_body() {
|
|
576
|
+
let body_bytes = Bytes::from("");
|
|
577
|
+
|
|
578
|
+
let response = build_optimized_response_bytes(StatusCode::OK, None, body_bytes);
|
|
579
|
+
|
|
580
|
+
assert_eq!(response.status(), StatusCode::OK);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
#[test]
|
|
584
|
+
fn test_build_optimized_response_bytes_large_json() {
|
|
585
|
+
let large_json = r#"{"users":[{"id":1,"name":"Alice","email":"alice@example.com","roles":["admin","user"],"active":true},{"id":2,"name":"Bob","email":"bob@example.com","roles":["user"],"active":true},{"id":3,"name":"Charlie","email":"charlie@example.com","roles":["user","moderator"],"active":false}],"pagination":{"page":1,"limit":10,"total":3}}"#;
|
|
586
|
+
let body_bytes = Bytes::from(large_json);
|
|
587
|
+
|
|
588
|
+
let response = build_optimized_response_bytes(StatusCode::OK, None, body_bytes);
|
|
589
|
+
|
|
590
|
+
assert_eq!(response.status(), StatusCode::OK);
|
|
591
|
+
assert_eq!(
|
|
592
|
+
response.headers().get(header::CONTENT_TYPE).unwrap().to_str().unwrap(),
|
|
593
|
+
"application/json"
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
#[test]
|
|
598
|
+
fn test_build_optimized_response_bytes_unicode_content() {
|
|
599
|
+
let unicode_json = r#"{"message":"Hello 世界 🌍","emoji":"😀💻🚀","accents":"café naïve résumé"}"#;
|
|
600
|
+
let body_bytes = Bytes::from(unicode_json);
|
|
601
|
+
|
|
602
|
+
let response = build_optimized_response_bytes(StatusCode::OK, None, body_bytes);
|
|
603
|
+
|
|
604
|
+
assert_eq!(response.status(), StatusCode::OK);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
#[test]
|
|
608
|
+
fn test_build_optimized_response_bytes_static_str() {
|
|
609
|
+
let json_static = r#"{"type":"static","source":"string literal"}"#;
|
|
610
|
+
let body_bytes = Bytes::from_static(json_static.as_bytes());
|
|
611
|
+
|
|
612
|
+
let response = build_optimized_response_bytes(StatusCode::OK, None, body_bytes);
|
|
613
|
+
|
|
614
|
+
assert_eq!(response.status(), StatusCode::OK);
|
|
615
|
+
assert_eq!(
|
|
616
|
+
response.headers().get(header::CONTENT_TYPE).unwrap().to_str().unwrap(),
|
|
617
|
+
"application/json"
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
#[test]
|
|
622
|
+
fn test_build_optimized_response_bytes_cloning() {
|
|
623
|
+
let json_body = r#"{"reusable":"true","copies":"cheap"}"#;
|
|
624
|
+
let body_bytes = Bytes::from(json_body);
|
|
625
|
+
|
|
626
|
+
// Clone Bytes multiple times - should be cheap (reference-counted)
|
|
627
|
+
let resp1 = build_optimized_response_bytes(StatusCode::OK, None, body_bytes.clone());
|
|
628
|
+
let resp2 = build_optimized_response_bytes(StatusCode::OK, None, body_bytes.clone());
|
|
629
|
+
let resp3 = build_optimized_response_bytes(StatusCode::OK, None, body_bytes);
|
|
630
|
+
|
|
631
|
+
assert_eq!(resp1.status(), StatusCode::OK);
|
|
632
|
+
assert_eq!(resp2.status(), StatusCode::OK);
|
|
633
|
+
assert_eq!(resp3.status(), StatusCode::OK);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
#[test]
|
|
637
|
+
fn test_build_optimized_response_bytes_accepted_status() {
|
|
638
|
+
let json_body = r#"{"status":"processing"}"#;
|
|
639
|
+
let body_bytes = Bytes::from(json_body);
|
|
640
|
+
|
|
641
|
+
let response = build_optimized_response_bytes(StatusCode::ACCEPTED, None, body_bytes);
|
|
642
|
+
|
|
643
|
+
assert_eq!(response.status(), StatusCode::ACCEPTED);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
#[test]
|
|
647
|
+
fn test_build_optimized_response_bytes_bad_request() {
|
|
648
|
+
let json_body = r#"{"error":"bad request","details":"invalid payload"}"#;
|
|
649
|
+
let body_bytes = Bytes::from(json_body);
|
|
650
|
+
|
|
651
|
+
let response = build_optimized_response_bytes(StatusCode::BAD_REQUEST, None, body_bytes);
|
|
652
|
+
|
|
653
|
+
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
#[test]
|
|
657
|
+
fn test_build_optimized_response_bytes_unauthorized() {
|
|
658
|
+
let json_body = r#"{"error":"unauthorized","code":"MISSING_TOKEN"}"#;
|
|
659
|
+
let body_bytes = Bytes::from(json_body);
|
|
660
|
+
|
|
661
|
+
let response = build_optimized_response_bytes(StatusCode::UNAUTHORIZED, None, body_bytes);
|
|
662
|
+
|
|
663
|
+
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
#[test]
|
|
667
|
+
fn test_build_optimized_response_bytes_forbidden() {
|
|
668
|
+
let json_body = r#"{"error":"forbidden","reason":"insufficient permissions"}"#;
|
|
669
|
+
let body_bytes = Bytes::from(json_body);
|
|
670
|
+
|
|
671
|
+
let response = build_optimized_response_bytes(StatusCode::FORBIDDEN, None, body_bytes);
|
|
672
|
+
|
|
673
|
+
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
#[test]
|
|
677
|
+
fn test_build_optimized_response_bytes_multiple_headers() {
|
|
678
|
+
let json_body = r#"{"data":"value"}"#;
|
|
679
|
+
let body_bytes = Bytes::from(json_body);
|
|
680
|
+
let mut headers = HeaderMap::new();
|
|
681
|
+
headers.insert("x-request-id", "req-123".parse().unwrap());
|
|
682
|
+
headers.insert("x-custom", "custom1".parse().unwrap());
|
|
683
|
+
headers.insert("cache-control", "no-cache".parse().unwrap());
|
|
684
|
+
headers.insert("x-another", "custom2".parse().unwrap());
|
|
685
|
+
|
|
686
|
+
let response = build_optimized_response_bytes(StatusCode::OK, Some(headers), body_bytes);
|
|
687
|
+
|
|
688
|
+
assert_eq!(response.status(), StatusCode::OK);
|
|
689
|
+
assert_eq!(response.headers().len(), 5); // 4 custom + 1 content-type
|
|
690
|
+
assert_eq!(
|
|
691
|
+
response.headers().get("x-request-id").unwrap().to_str().unwrap(),
|
|
692
|
+
"req-123"
|
|
693
|
+
);
|
|
694
|
+
assert_eq!(
|
|
695
|
+
response.headers().get("cache-control").unwrap().to_str().unwrap(),
|
|
696
|
+
"no-cache"
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
#[test]
|
|
701
|
+
fn test_build_optimized_response_bytes_parity_with_vec() {
|
|
702
|
+
// Test that Vec<u8> and Bytes produce identical responses (except internally)
|
|
703
|
+
let json_data = br#"{"test":"parity","value":42}"#;
|
|
704
|
+
|
|
705
|
+
let response_vec = build_optimized_response(StatusCode::CREATED, None, json_data.to_vec());
|
|
706
|
+
let response_bytes =
|
|
707
|
+
build_optimized_response_bytes(StatusCode::CREATED, None, Bytes::copy_from_slice(json_data));
|
|
708
|
+
|
|
709
|
+
assert_eq!(response_vec.status(), response_bytes.status());
|
|
710
|
+
assert_eq!(
|
|
711
|
+
response_vec.headers().get(header::CONTENT_TYPE),
|
|
712
|
+
response_bytes.headers().get(header::CONTENT_TYPE)
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
#[test]
|
|
717
|
+
fn test_build_optimized_response_bytes_status_codes() {
|
|
718
|
+
let statuses = vec![
|
|
719
|
+
StatusCode::OK,
|
|
720
|
+
StatusCode::CREATED,
|
|
721
|
+
StatusCode::ACCEPTED,
|
|
722
|
+
StatusCode::BAD_REQUEST,
|
|
723
|
+
StatusCode::UNAUTHORIZED,
|
|
724
|
+
StatusCode::FORBIDDEN,
|
|
725
|
+
StatusCode::NOT_FOUND,
|
|
726
|
+
StatusCode::INTERNAL_SERVER_ERROR,
|
|
727
|
+
StatusCode::SERVICE_UNAVAILABLE,
|
|
728
|
+
];
|
|
729
|
+
|
|
730
|
+
let json_body = r#"{"status":"ok"}"#;
|
|
731
|
+
|
|
732
|
+
for status in statuses {
|
|
733
|
+
let body_bytes = Bytes::from(json_body);
|
|
734
|
+
let response = build_optimized_response_bytes(status, None, body_bytes);
|
|
735
|
+
|
|
736
|
+
assert_eq!(response.status(), status);
|
|
737
|
+
assert_eq!(
|
|
738
|
+
response.headers().get(header::CONTENT_TYPE).unwrap().to_str().unwrap(),
|
|
739
|
+
"application/json"
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
305
743
|
}
|